在 Code First 数据访问 Web API 中使用 Entity Framework Designer 处理返回多个结果集的存储过程
在使用 Code First 方法构建的数据访问 Web API 应用程序中使用基于 EF 设计器的数据上下文处理返回多个结果集的存储过程
引言
在使用 Entity Framework (EF) Code First 开发的应用程序中,我们可能会使用 SqlQuery
或 SqlCommand
函数在数据上下文中执行存储过程。如果存储过程返回多个结果集,目前唯一的选择是调用 SqlCommand.ExecuteReader()
并结合 ObjectContext.Translate()
方法。然而,在我的项目中,当这样的存储过程返回大量记录时,使用 Translate()
方法导致了严重的性能问题,这可能是由于开销很大的即时类型映射造成的。我尝试添加一个 EF 设计器,并为 Code First 的 Web API 应用程序中返回多个结果集的存储过程生成一个额外的数据上下文。以下是完整的实现概述。
- 构建一个结构良好的 Web API 应用程序,使用 EF Code First。
- 在数据访问层 (DAL) 中添加一个 EF 设计器,以创建一个额外的数据上下文和单元工作模块。
- 编辑 EF 设计器和 EDMX 文件中的项目,以设置存储过程数据类型和函数导入映射。
- 共享业务逻辑层 (BLL) 类以访问多个 DAL 存储库。
- 使用 Unity 依赖注入和单元工作设置配置连接字符串。
- 使用 Fiddler、AngularJS 网页和 Web API 客户端库测试从调用存储过程的 Web API 资源中检索数据。
您需要安装 Visual Studio 2012/2013,并拥有内置的 IIS Express,以及访问 NuGet 以在本地运行示例应用程序。
Web API 项目
从下载的源文件中获取的解决方案和项目在此屏幕截图中显示。
应用程序结构的一些要点概述如下:
-
Web API 控制器位于独立的库项目
SM.Store.Api
中,与宿主项目分开。尽管在本示例中 Web API 通过 IIS Express 托管,但切换到 OWIN 宿主或 OWIN 自托管可以轻松完成,而无需修改主 API 项目。 -
Web API 配置为使用特性路由映射,这仅由 Web API 2.0 及以上版本支持。
-
实体和自定义模型对象也位于独立的项目
SM.Store.Entities
和SM.Store.Models
中。编译后的程序集可以由客户端应用程序共享,以便在需要时匹配模型类型。 -
主数据上下文对象是使用标准的 EF Data First 方法创建的。数据库初始化器已在 DAL 项目中编写,并通过宿主项目的 Web.config 文件中的设置执行。
-
使用 Microsoft Unity 的依赖注入模式来访问 DAL 存储库、单元工作和 BLL 对象。设计时配置设置在宿主项目的 Unity.config 文件中,而运行时实例解析逻辑则在主 API 项目中。
-
除了托管 Web API 外,
SM.Store.Api.Web
项目还包含使用 AngularJS、Bootstrap 和 HTML 5 编写的纯客户端代码页面,用于访问 Web API 资源。 -
TestApiClientConsole
项目使用 Web API 客户端库作为 Web API 客户端应用程序。
在 Code First 数据库中准备存储过程
在 Code First 中,我们可以通过以下选项将存储过程添加到数据库:
- 在数据初始化器的
Seed()
方法中编写 SQL 字符串。protected override void Seed(StoreDataContext context) { //DB table data initialized here... //Add stored procedures. context.Database.ExecuteSqlCommand( @"CREATE PROCEDURE dbo.GetAllCategorisAndProducts AS BEGIN --Main content here... END " ); }
- 执行 SQL 脚本或使用任何 SQL 数据工具(如 SQL Server Management Studio 或 Visual Studio Server Explorer)的 UI 来创建存储过程。在使用此选项之前,您需要完成所有 Code First 项目,并成功运行任何数据访问过程以创建连接字符串中定义的数据库。
示例应用程序在数据库初始化期间使用 seeding 选项添加存储过程。与向表中添加数据类似,此操作不会更改数据库架构。此处任何 SQL 代码的更改都不会更新数据库中的存储过程。要更新存储过程,您需要直接更改现有数据库中的存储过程,或者编辑 Seed()
方法中的代码,然后删除并重新初始化一个新数据库。
向现有的 Code First DAL 添加 EF 设计器和数据上下文
Code First DAL 可以使用所有标准文档化的方法创建。完成所有相关项目并使用其中一个数据访问调用启动应用程序后,您可以通过右键单击 DAL 项目名称,选择 **添加**,**新建项…**,**Visual C# 项**,**数据**,以及 **ADO.NET Entity Data Model** 来添加 EF 设计器和额外的数据上下文对象到 DAL。在后续的向导屏幕上,使用所有默认设置,但以下两项除外:
-
**选择或输入 Code First 之前创建的数据库** 屏幕:在 **选择数据连接** 屏幕上。
-
**选择数据库对象和设置** 屏幕:在该数据库中选择存储过程。
您可以更改这些名称输入框中的默认值,但需要仔细考虑这次的最终名称。与项目和类名称不同,之后重命名与 EF 设计器相关的某些现有项可能会成为一场噩梦。
-
**名称**框在 **添加新项** 屏幕上:这适用于 EDMX 文件以及整个 EDMX 组中相关文件的主要部分。如果您之后在代码中的任何地方重命名它,应用程序将正常工作,但某些文件名将不会被更改,除非手动更改物理文件名并仔细编辑项目 XML 文件。在示例应用程序中的值是
StoreDataSp
。 -
**在 App.config 中保存连接设置** 框在 **选择数据连接** 屏幕上:这适用于数据上下文类名和连接字符串名。之后可以更改值而不影响功能。在示例应用程序中,它是
StoreDataContext
。 -
**模型命名空间** 框在 **选择数据库对象和设置** 屏幕上。该值广泛用于 EDMX 文件中的命名空间以及连接字符串中的元数据定义。之后重命名它可能会导致元数据错误,除非您用新的 EF 设计器和数据上下文替换现有的。示例应用程序使用的是名称
StoreDataSpModal
。
将存储过程与 EF 设计器映射
当 EF 设计器准备就绪后,任何存储过程的函数导入映射都可以像数据库优先场景一样进行。有关如何使用变通方法将返回多个结果集的存储过程与 EF 设计器和 EDMX 文件进行映射,请参阅我之前的 文章。
完成映射后,您可以将设计器生成的 T4 模板 [model-name].tt 和依赖文件移动到独立的实体或自定义数据模型类库项目中。以下是我为将模型类型模板组从 SM.Store.Api.DAL
移动到示例应用程序中的 SM.Store.Api.Models
项目所做的操作。
-
打开 Windows 资源管理器并找到 SM.Store.Api.DAL 文件夹。
-
选择 StoreDataSp.tt,然后将其拖放到 Visual Studio 的解决方案资源管理器中的
SM.Store.Api.Models
项目上。 -
从 Visual Studio 打开新的 StoreDataSp.tt 并编辑该行,将输入文件指向原始 EDMX 文件。
编辑前
const string inputFile = @"StoreDataSp.edmx";
编辑后
const string inputFile = @"../SM.Store.Api.DAL/StoreDataSp.edmx";
-
保存更新后的 StoreDataSp.tt 文件。这将自动重新填充新的模板组,其中包含映射的类型文件,在我们的案例中是存储过程的 Category_Result.cs 和 Product_Result.cs。如果之后使用设计器进行了任何更改,可以通过在解决方案资源管理器中右键单击
SM.Store.Api.Models
项目中的 StoreDataSp.tt 并选择 **运行自定义工具** 命令来自动更新模型类型文件。 -
从 Visual Studio 解决方案资源管理器中删除
SM.Store.Api.DAL
项目中的 StoreDataSp.tt 组。
移动 StoreDataSp.tt 组之前的 DAL 和模型项目
移动 StoreDataSp.tt 组之后的 DAL 和模型项目
这种重构的好处是,独立程序集中的模型类型文件可以被所有引用项目或应用程序轻松访问。稍后在 Web API 客户端库项目中的示例中您将看到。
连接字符串
对于设计器生成的用于数据上下文的数据库连接字符串,其外观与 Code First 数据上下文的连接字符串不同,因为前者包含附加的元数据定义。在示例应用程序中,两个连接字符串的设置都放在 Web API 宿主项目的 Web.config 文件中。
<connectionStrings>
<!--Code First data context connection string-->
<add name="StoreDataContext" connectionString="Data Source=(LocalDb)\v11.0;
Initial Catalog=StoreCF6;Integrated Security=SSPI;
AttachDBFilename=|DataDirectory|\StoreCF6.mdf;integrated security=True;
MultipleActiveResultSets=True" providerName="System.Data.SqlClient" />
<!--Designer based data context connection string-->
<add name="StoreDataSpContext"
connectionString="metadata=res://*/StoreDataSp.csdl|res://*/StoreDataSp.ssdl|
res://*/StoreDataSp.msl;provider=System.Data.SqlClient;
provider connection string="data source=(localdb)\v11.0;
initial catalog=StoreCF6;integrated security=True;MultipleActiveResultSets=True;
App=EntityFramework"" providerName="System.Data.EntityClient" />
</connectionStrings>
在 Unity.config 文件中,连接字符串占位符设置在单元工作构造函数节点中。还有两组类似的配置和相关项,分别针对 Code First 和基于设计器的数据上下文对象。Unity.config 文件中用于基于设计器上下文连接字符串的配置设置如下:
<register type="SM.Store.Api.DAL.IStoreDataSpUnitOfWork"
mapTo="SM.Store.Api.DAL.StoreDataSpUnitOfWork">
<lifetime type="singleton" />
<constructor>
<!--Set placeholder for value attribute and replace it at runtime-->
<param name="connectionString" value="{connectionString_SP}" />
</constructor>
</register>
在基于设计器的数据上下文的单元工作的构造函数中,连接字符串值是根据传入的占位符名称从 Web.config 文件中检索的。然后,实际的连接字符串值将被进一步注入到基于设计器的数据上下文构造函数中。
public StoreDataUnitOfWork(string connectionString)
{
if (connectionString == "{connectionString_SP}")
{
connectionString = ConfigurationManager.ConnectionStrings
["StoreDataSpContext"].ConnectionString;
}
this.context = new StoreDataSpContext(connectionString);
}
从 BLL 访问多个存储库
将 EF 设计器添加到 Code First 会产生多种编程结构。在示例应用程序中,我将多个数据上下文对象、单元工作项和存储库保留在 DAL 中,但使用共享的 BLL 对象来调用多个存储库。这种操作是通过重载 BLL 对象构造函数来实现的。ProductBS
类可以通过传递 IProductRepository
或 IProductSpRepository
,或两者兼有来实例化。
public class ProductBS : IProductBS
{
private IProductRepository _productRepository;
private IProductSpRepository _productRepository_SP;
public ProductBS(IProductRepository productRepository)
{
if (productRepository != null)
this._productRepository = productRepository;
}
public ProductBS(IProductSpRepository productRepository_SP)
{
if (productRepository_SP != null)
this._productRepository_SP = productRepository_SP;
}
public ProductBS(IProductRepository productRepository,
IProductSpRepository productRepository_SP)
{
if (productRepository != null)
this._productRepository = productRepository;
if (productRepository_SP != null)
this._productRepository_SP = productRepository_SP;
}
//- - - Other code
}
使用映射的存储过程检索数据
以下是在示例应用程序中调用 GetCategoriesAndProducts
存储过程进行数据检索的代码示例。
在 SM.Store.Api.ProductsController
中
[Route("/api/getmultiplesets")]
public CategoriesProducts GetCategoriesAndProducts()
{
var resp = new CategoriesProducts();
IProductBS bs = DIFactoryDesigntime.GetInstance<IProductBS>();
resp = bs.GetCategoriesAndProducts();
return resp;
}
在 SM.Store.Api.BLL.ProductBS
中
public CategoriesProducts GetCategoriesAndProducts()
{
Return this._productRepository_SP.GetCategoriesAndProducts();
}
在 SM.Store.Api.DAL.ProductSpRepository
中
public CategoriesProducts GetCategoriesAndProducts()
{
CategoriesProducts categProd = new CategoriesProducts();
categProd.Categories = new List<Category_Result>();
categProd.Products = new List<Product_Result>();
//Call stored procedure and get all result sets
var results = this.UnitOfWork_SP.Context.GetAllCategorisAndProducts();
//Get first enumerate result set
categProd.Categories.AddRange(results);
//Get second result set
var products = results.GetNextResult<Product_Result>();
categProd.Products.AddRange(products);
//Return all result sets
return categProd;
}
在 SM.Store.Api.DAL.StoreDataSpContext
中
public virtual ObjectResult<Category_Result> GetAllCategorisAndProducts()
{
return ((IObjectContextAdapter)this).ObjectContext.ExecuteFunction<Category_Result>
("GetAllCategorisAndProducts");
}
启动 Web API 宿主
在启动 Web API 并运行示例应用程序之前,请编译 Visual Studio 解决方案。这也会从 NuGet 下载所有必需的库文件。
通过在命令提示符中执行以下行,可以以非调试模式启动 IIS Express Web 宿主:
C:\Program Files (x86)\IIS Express\iisexpress.exe" /site:SM.Store.Api.Web
或执行下载源中包含的 SM.Store.WebApi_SiteStart.bat 文件。
示例应用程序将虚拟的 index2.html 设置为默认的启动页面,用于以调试模式启动 Web 宿主。按 F5 启动 IIS Express Web API 宿主以进行调试会话。
使用 Fiddler 测试 Web API
如果您在重建下载的源并启动 IIS Express 后立即在 Fiddler 的 **Composer** 选项卡中执行 "https://:5611/api/getmultiplesets",您将收到 "500 Internal Server Error…The underlying provider failed on open" 错误消息。
这发生的原因是 Web API 调用连接到 EF 设计器生成的数据上下文,而该上下文的底层数据库尚未创建。数据库文件和连接字符串中指定的 SQL Server 数据库将在 Code First 数据上下文中的数据访问方法被调用时生成。因此,让我们首先调用一个与 Code First 数据上下文相关的 API 资源 "https://:5611/api/products/2"。
只要数据库、表和存储过程存在,我们就可以成功调用与基于设计器的数据上下文相关的 Web API。
纯 AngularJS MVC 客户端网站
示例应用程序包含一个简单的纯客户端代码网站,使用 AngularJS、Bootstrap 和 HTML 5 编写,用于消费 Web API。尽管它与 Web API 宿主共享网站,但您可以将根目录下的 index.html 以及 Content、Pages 和 Scripts 文件夹中的所有文件分离出来,形成一个新的独立网站(如果需要)。
该网站使用标准的 AngularJS MVC 结构和 MVVM 模式,没有任何服务器端代码(如 Razor)语法。以下是 controllers.js 中用于直接调用 Web API 并从返回两个结果集的存储过程中获取数据的代码行:
$http({
url: 'api/getmultiplesets',
method: "GET"
}).
success(function (data, status, headers, config) {
$scope.model.categories = data.Categories;
$scope.model.products = data.Products;
}).
error(function (data, status, headers, config) {
$scope.model.errorMessage = "Error occurred status:" + status;
});
在此,多个结果集的客户端模型类型是根据从响应内容反序列化的数据对象和子对象的结构自动映射的。无需进一步的努力。然后,这些数据模型被绑定到视图(HTML 文件)中的 HTML 元素以供显示。
您需要将 index.html 页面设置为 SM.Store.Api.Web
项目中的启动页面,并在调试模式下启动它,或者简单地使用 **在浏览器中查看** 上下文菜单命令以非调试模式运行 index.html。当单击 **Category and Product Lists** 菜单链接时,将会在页面上显示 `Category` 和 `Product` 网格。
使用 Web API 客户端库进行测试
示例应用程序中的 TestApiClientConsole
项目展示了如何使用 Web API 客户端库从相同的方法获取数据,以处理返回两个结果集的存储过程。该项目使用此本地方法调用 Web API 并显示数据。
private static void GetCategoriesAndProducts()
{
var uri = apiBaseUri + "getmultiplesets";
CategoriesProducts resp = default(CategoriesProducts);
using (HttpClient client = new HttpClient())
{
HttpResponseMessage result = client.GetAsync(uri).Result;
resp = result.Content.ReadAsAsync<CategoriesProducts>().Result;
}
//Display data using custom genetic List extension ToString<>.
var strResult = resp.Categories.ToString<Category_Result>() + "\n\n";
strResult += resp.Products.ToString<Product_Result>
(include:"ProductName,CategoryId,UnitPrice");
Console.Write(strResult);
}
与在前面的示例中从网站进行直接 AJAX 调用访问 Web API 不同,使用 Web API 客户端库需要客户端中定义的所有必需模型类型,并且这些模型类型需要与 API 服务器中的模型类型匹配。您当然可以在客户端应用程序中创建模型类,但是使用共享程序集来获取模型类型是高效且可靠的。如文章前面所述,Category_Result
和 Product_Result
类文件已从 SM.Store.Api.DAL
移动到独立的 SM.Store.Api.Models
项目。客户端应用程序只需包含并引用 SM.Store.Api.Models
项目或编译的程序集即可调用 getmultiplesets
API 资源。
要查看此控制台应用程序的运行效果,首先请确保 Code First 生成的数据库已存在,并且 IIS Express Web 宿主已按上述任一方式启动。然后,在 Visual Studio 解决方案资源管理器中右键单击 TestApiClientConsole
项目,然后选择 **调试**,**开始新实例**。
摘要
我们当然可以将基于 EF 设计器和 Code First 的数据上下文对象混合在同一个应用程序中,例如数据访问 Web API。这在我们需要与返回多个结果集的存储过程交互时特别有用,除非 EF 或开发工具的任何新版本支持对此类存储过程进行编程代码映射。
历史
- 2014 年 8 月 8 日:初始版本