存储库模式是创建企业级应用程序的最流行模式之一。它限制了我们直接处理应用程序中的数据,并为数据库操作、业务逻辑和应用程序的UI创建了新的层。如果应用程序不遵循存储库模式,则可能会出现以下问题:
- 重复的数据库操作代码
- 需要UI来单元测试数据库操作和业务逻辑
- 需要外部依赖来单元测试业务逻辑
- 难以实现数据库缓存等。
使用存储库模式有很多优点:
- 您的业务逻辑可以进行单元测试,而无需数据访问逻辑;
- 数据库访问代码可以重复使用;
- 您的数据库访问代码是集中管理的,因此很容易实现任何数据库访问策略,如缓存;
- 很容易实现领域逻辑;
- 您的域实体或业务实体是带有注释的强类型; 等等。
在互联网上,有数百万篇关于存储库模式的文章,但在这篇文章中,我们将重点介绍如何在ASP.NET MVC应用程序中实现它。所以让我们开始吧!
在您继续学习ASP.NET MVC中的存储库模式之前,我建议您查看基于Ignite UI的Infragistics jQuery库,它可以帮助您更快地编写和运行web应用程序。您可以使用Ignite UI for JavaScript库来帮助快速解决HTML5、jQuery、Angular、React或ASP.NET MVC中复杂的LOB要求。你可以下载一个免费试用的Ignite UI在这里。
项目结构
让我们从创建应用程序的项目结构开始。我们将创建四个项目:
- 核心项目
- 基础设施项目
- 测试项目
- MVC项目
每个项目都有自己的目的。您可能可以通过项目的名称猜出它们将包含的内容: 核心和基础设施项目是类库,Web项目是MVC项目,测试项目是单元测试项目。最终,解决方案资源管理器中的项目将如下图所示:
随着我们在这篇文章中的进展,我们将详细了解每个项目的目的,但是,首先我们可以将每个项目的主要目标总结如下:
到目前为止,我们对不同项目的理解是清楚的。现在让我们继续下去,逐一实施每个项目。在实施过程中,我们将详细探讨每个项目的职责。
核心项目
在核心项目中,我们保留实体和存储库接口或数据库操作接口。核心项目包含有关域实体和域实体所需的数据库操作的信息。在理想情况下,核心项目不应该对外部库有任何依赖。它不能有任何业务逻辑,数据库操作代码等。
简而言之,核心项目应包含:
- 域实体
- 存储库接口或数据库操作接口在域实体
- 域特定数据注释
核心项目不能包含:
- 任何外部库用于数据库操作
- 业务逻辑
- 数据库操作代码
在创建域实体时,我们还需要决定对域实体属性的限制,例如:
- 是否需要特定的属性。例如,对于产品实体,产品的名称应该是必需属性。
- 特定属性的值是否在给定范围内。例如,对于产品实体,价格属性应在给定范围内。
- 特定属性的最大长度是否不应被赋予值。例如,对于Product实体,name属性值应小于最大长度。
域实体属性上可能有许多这样的数据注释。我们可以通过两种方式来考虑这些数据注释:
- 作为域实体
- 作为数据库操作逻辑
的一部分
这完全取决于我们如何看待数据注释。如果我们认为它们是数据库操作的一部分,那么我们可以使用数据库操作库API来应用限制。我们将在基础设施项目中使用实体框架进行数据库操作,因此我们可以使用实体框架Fluent API来注释数据。
如果我们认为它们是域的一部分,那么我们可以使用库System.ComponentModel.DataAnnotations来注释数据。要使用此功能,请右键单击核心项目的引用文件夹,然后单击添加引用。从 “框架” 选项卡中,选择 “ System.ComponentModel.DataAnnotations ”,然后将其添加到项目中。
我们正在创建一个ProductApp,所以让我们从创建产品实体开始。要添加实体类,请右键单击核心项目并添加一个类,然后将该类命名为Product。
使用
System.ComponentModel.DataAnnotations;ProductApp.Core命名空间{ 公共类产品{ public int Id {get; set; }[必填][最大长度 (100)] 公共字符串名称 {get; set; }[必填] 公共双倍价格 {get; set; } 公共bool inStock {get; set; }}}
我们已经用Required和MaxLength注释了Product实体属性。这两个注释都是System.ComponentModel.DataAnnotations的一部分。在这里,我们将限制视为域的一部分,因此在核心项目本身中使用了数据注释。
我们已经创建了产品实体类,并且还应用了数据注释。现在让我们继续创建存储库接口。但是在我们创建它之前,让我们了解一下,什么是存储库接口?
存储库接口定义了域实体上所有可能的数据库操作。所有可以在域实体上执行的数据库操作都是域信息的一部分,因此我们将把存储库接口放在核心项目中。如何执行这些操作将是基础设施项目的一部分。
要创建存储库接口,请右键单击核心项目并添加一个名为Interfaces的文件夹。创建Interfaces文件夹后,右键单击Interface文件夹并选择add a new item,然后从Code选项卡中选择Interface。将接口命名为IProductRepository
使用System.Collections.Generic的
;ProductApp.Core.Interfaces命名空间{ 公共接口IProductRepository{ 无效添加 (产品p); 编辑无效 (产品p); void Remove (int Id);IEnumerable GetProducts(); 产品FindById (int Id); } }
现在我们已经创建了一个产品实体类和一个产品存储库接口。在这一点上,核心项目应该是这样的:
让我们继续构建核心项目,以验证一切就绪,并继续创建基础设施项目。
基础设施项目
Infrastructure project的主要目的是执行数据库操作。除了数据库操作,它还可以使用web服务,执行IO操作等。所以主要是,基础设施项目可以执行以下操作:
- 数据库操作
- 使用WCF和Web服务
- IO操作
我们可以使用任何数据库技术来执行数据库操作。在这篇文章中,我们将使用实体框架。因此,我们将使用代码优先方法创建一个数据库。在代码优先的方法中,数据库基于类创建。这里数据库将在核心项目的域实体的基础上创建。
要从核心项目域实体创建数据库,我们需要执行以下任务:
- 创建DataContext类
- 配置连接字符串
- 创建数据库初始化器类以将数据存储在数据库
- 实现IProductRepsitory接口
中
添加引用
首先,让我们添加对实体框架和ProductApp.Core项目的引用。要添加实体框架,右键单击基础设施项目,然后单击管理Nuget包。在 “包管理器” 窗口中,搜索Entity Framework并安装最新的稳定版本。
要添加ProductApp.Core项目的引用,请右键单击基础结构项目,然后单击 “添加引用”。在 “引用” 窗口中,单击 “项目” 选项卡,然后选择 “ProductApp.Core”。
DataContext类
DataContext类的目标是在实体框架代码第一种方法中创建数据库。我们在DataContext类的构造函数中传递连接字符串。通过读取连接字符串,实体框架创建数据库。如果未指定连接字符串,则实体框架将在本地数据库服务器中创建数据库。
在DataContext类中:
- 创建DbSet类型属性。这负责为Product实体创建表
- 在DataContext类的构造函数中,传递连接字符串以指定创建数据库的信息,例如,服务器名称,数据库名称,登录信息等。我们需要传递连接字符串的名称。将在其中创建数据库的名称
- 如果未传递连接字符串,则Entity Framework将使用本地数据库服务器中的数据上下文类的名称创建。
- productdatacontext类继承DbContext类
可以创建ProductDataContext类,如下面的清单所示:
使用ProductApp.Core的
;System.Data.Entity使用;ProductApp.Infrastructure命名空间{ ProductContext: DbContext公共类{ 公共ProductContext (): base ("name = ProductAppConnectionString"){} 公共DbSet产品 {get; set; }
接下来,我们需要处理连接字符串。如前所述,我们可以传递连接字符串以指定数据库创建信息,也可以在实体框架上回复以在默认位置为我们创建默认数据库。我们将指定连接字符串,这是我们在ProductDataContext类的构造函数中传递连接字符串名称ProductAppConnectionString的原因。在App.Config文件中,可以创建ProductAppConnectionString连接字符串,如下所示:
<添加 名称 ="ProductAppConnectionString" connectionString ="数据源 =(LocalDb)\ v11.0; 初始目录 = ProductAppJan; 集成安全性 = True;MultipleActiveResultSets = true" providerName ="System.Data.SqlClient"/>
数据库初始值设定项类
我们创建一个数据库初始化器类,以便在创建时使用一些初始值为数据库提供种子。若要创建数据库初始值设定项类,请创建一个从DropCreateDatabaseIfModelChnages继承的类。还有其他类选项可用于继承,以便创建数据库初始化器类。如果我们继承DropCreateDatabaseIfModelChnages类,则每次在模型上创建新数据库时都会更改。因此,例如,如果我们从产品实体类中添加或删除属性,实体框架将删除现有数据库并创建一个新数据库。当然,这不是一个很好的选择,因为数据也会丢失,所以我建议你探索其他选项来继承数据库初始化器类。
可以创建数据库初始化器类,如下面的清单所示。在这里,我们用两行播种product表。要为数据设定种子:
- 重写种子方法
- 将产品添加到上下文。产品
- 调用Context.SaveChanges()
使用ProductApp.Core的
;System.Data.Entity使用;ProductApp.Infrastructure命名空间{ ProductInitalizeDB公共类: DropCreateDatabaseIfModelChanges {protected重写void Seed (ProductContext) { context.Products.Add( new Product { Id = 1 ,Name = "Rice" ,inStock = true ,price = 30 }); context.Products.Add( new Product { Id = 2 ,Name = "Sugar" ,inStock = false ,Price = 40 }); context.SaveChanges(); base .Seed(context); }
到目前为止,我们已经完成了所有实体框架代码首先创建数据库的相关工作。现在,让我们在一个具体的ProductRepository类中从核心项目实现一个IProductRepository接口。
存储库类
这是将对Product实体执行数据库操作的类。在这个类中,我们将从核心项目实现IProductRepository接口。让我们从向基础结构项目添加类ProductRepository开始,并实现IProductRepository接口。为了执行数据库操作,我们将编写简单的LINQ To Entity查询。可以创建产品存储库类,如下面的列表所示:
使用ProductApp.Core.Interfaces的
;System.Collections.Generic使用;System.Linq使用;ProductApp.Core使用;ProductApp.Infrastructure命名空间{ ProductRepository公共类: IProductRepository{ProductContext context = 新ProductContext(); 添加公共无效 (产品p){添加 (p);context.SaveChanges();} 编辑 (产品p) 公共无效{context.Entry(p).State = System.Data.Entity.EntityState.Modified;} 公共产品FindById (int Id){ var结果 = (来自context.Products中的r.Id = = Id选择r)。FirstOrDefault(); 返回结果;} public IEnumerable GetProducts() {return context.Products; } public void Remove (int Id) { Product p = context.Products.Find(Id); context.Products.Remove(p); context.SaveChanges(); } } }
到目前为止,我们已经创建了一个数据上下文类,一个数据库初始化器类和存储库类。让我们构建基础设施项目,以确保一切就绪。ProductApp.Infrastructure项目的外观如下图所示:
现在我们完成了基础设施项目的创建。我们已经在基础设施项目中编写了所有与数据库操作相关的类,所有与数据库相关的逻辑都在一个中心位置。每当需要对数据库逻辑进行任何更改时,我们只需要更改基础结构项目。
测试项目
Repository模式的最大优点是可测试性。这允许我们对各种组件进行单元测试,而不依赖于项目的其他组件。例如,我们创建了执行数据库操作的存储库类,以验证功能的正确性,因此我们应该对其进行单元测试。我们还应该能够为存储库类编写测试,而不依赖于web项目或UI。由于我们遵循存储库模式,因此我们可以为基础设施项目编写单元测试,而无需依赖MVC项目 (UI)。
要为ProductRepository类编写单元测试,让我们在测试项目中添加以下引用。
- 引用ProductApp.Core项目
- 参考ProductApp.Infrastructure项目
- 实体框架包
要添加实体框架,请右键单击测试项目,然后单击管理Nuget包。在包管理器窗口中,搜索Entity Framework并安装最新的稳定版本。
要添加ProductApp.Core项目的引用,请右键单击测试项目,然后单击 “添加引用”。在 “引用” 窗口中,单击 “项目” 选项卡,然后选择 “ProductApp.Core”。
要添加ProductApp.Infrastructure项目的引用,请右键单击测试项目,然后单击 “添加引用”。在 “引用” 窗口中,单击 “项目” 选项卡,然后选择 “ProductApp.Infrastructure”。
复制连接字符串
Visual Studio始终读取正在运行的项目的配置文件。为了测试基础设施项目,我们将运行测试项目。因此,连接字符串应该是测试项目的App.Config的一部分。让我们将基础结构项目中的连接字符串复制并粘贴到测试项目中。
我们已经添加了所有必需的引用并复制了连接字符串。让我们现在开始设置测试课。我们将创建一个名为ProductRepositoryTest的测试类。Test Initialize函数是在执行测试之前执行的函数。在运行测试之前,我们需要创建ProductRepository类的实例,并调用ProductDbInitalize类为数据设定种子。测试初始值设定项可以如下所示编写:
[TestClass] 公共类productrepositorittest{产品库回购;[TestInitialize] TestSetup () 公共无效{ProductInitalizeDB db = 新的ProductInitalizeDB();System.Data.Entity.Database.SetInitializer(db);Repo = 新ProductRepository();}}
现在我们已经编写了测试初始值设定项。现在让我们编写第一个测试来验证ProductInitalizeDB类是否在Product表中产生两行种子。由于这是我们将执行的第一个测试,它还将验证数据库是否被创建。所以基本上我们正在写一个测试:
- 验证数据库创建
- 验证seed方法插入的行数Product数据库初始值设定项
[测试方法]
公共无效IsRepositoryInitalizeWithValidNumberOfData (){ var结果 = Repo.GetProducts();Assert.IsNotNull(result); var numberOfRecords = result.ToList().Count;Assert.AreEqual( 2 ,numberOfRecords);}
如您所见,我们正在调用存储库GetProducts() 函数来获取在创建数据库时插入的所有产品。这个测试实际上是验证GetProducts() 是否按预期工作,并验证数据库的创建。在测试浏览器窗口中,我们可以运行测试进行验证。
要运行测试,首先生成测试项目,然后从顶部菜单中选择test->Windows-Test Explorer。在测试资源管理器中,我们将找到列出的所有测试。选择测试并单击Run。
让我们继续编写一个测试来验证存储库上的添加产品操作:
[测试方法]
公共无效isrepositoryaddproduct (){Product productToInsert = 新产品{Id = 3,inStock = true,Name = "Salt",价格 = 17};Repo.Add(productToInsert); // 如果产品插入成功, // 记录数将增加到3 var结果 = Repo.GetProducts(); var numberOfRecords = result.ToList().Count;Assert.AreEqual( 3 ,numberOfRecords);}
为了验证产品的插入,我们在存储库上调用Add函数。如果产品成功添加,记录数将从2增加到3,我们正在验证。在运行测试时,我们会发现测试已经通过。
这样,我们可以从产品存储库类中为所有数据库操作编写测试。现在我们确信我们已经正确地实现了存储库类,因为测试正在通过,这意味着基础设施和核心项目可以与任何UI (在本例中为MVC) 项目一起使用。
MVC或Web项目
最后,我们得到了MVC项目!与测试项目一样,我们需要添加以下引用
- 引用ProductApp.Core项目
- 参考ProductApp.Infrastructure项目
要添加ProductApp.Core项目的引用,请右键单击MVC项目,然后单击 “添加引用”。在 “引用” 窗口中,单击 “项目” 选项卡,然后选择 “ProductApp.Core”。
要添加ProductApp.Infrastructure项目的引用,请右键单击MVC项目,然后单击 “添加引用”。在 “引用” 窗口中,单击 “项目” 选项卡,然后选择 “ProductApp.Infrastructure”。
复制连接字符串
Visual Studio始终读取正在运行的项目的配置文件。为了测试基础设施项目,我们将运行测试项目,因此连接字符串应该是测试项目的App.Config的一部分。为了使它更容易,让我们复制和粘贴连接字符串从测试项目中的基础设施项目。
基架应用程序
我们应该有一切到位的支架MVC控制器。要脚手架,右键单击控制器文件夹并选择MVC 5控制器与视图,使用实体框架如下图所示:
接下来,我们将看到添加控制器窗口。这里我们需要提供模型类和数据上下文类信息。在我们的项目中,模型类是核心项目中的产品类,数据上下文类是基础结构项目中的ProductDataContext类。让我们从下拉列表中选择两个类,如下图所示:
此外,我们应该确保选择了 “生成视图” 、 “引用脚本库” 和 “使用布局页面” 选项。
单击 “添加”,Visual Studio将在 “视图/产品” 文件夹中创建ProductsController和视图。MVC项目应该具有如下图所示的结构:
此时,如果我们继续运行应用程序,我们将能够对Product实体执行CRUD操作。
问题与脚手架
但是我们还没有完成!让我们打开ProductsController类并检查代码。在第一行,我们会发现问题。由于我们使用了MVC基架,MVC正在创建ProductContext类的对象来执行数据库操作。
对上下文类的任何依赖项都将UI项目和数据库紧密地绑定在一起。我们知道Datacontext类是一个实体框架组件。我们不希望MVC项目知道基础设施项目中正在使用哪种数据库技术。另一方面,我们还没有测试Datacontext类; 我们已经测试了ProductRepository类。理想情况下,我们应该使用ProductRepository类而不是ProductContext类在MVC控制器中执行数据库操作。总结一下,
- MVC脚手架使用数据上下文类来执行数据库操作。数据上下文类是一个实体框架组件,因此它的使用将UI (MVC) 与数据库 (EF) 技术紧密耦合。
- 数据上下文类不是单元测试,所以使用它不是一个好主意。
- 我们有一个经过测试的ProductRepository类。我们应该使用这个内部控制器来执行数据库操作。此外,ProductRepository类不会向UI公开数据库技术。
要使用ProductRepository类进行数据库操作,我们需要重构ProductsController类。为此,我们需要遵循两个步骤:
- 创建ProductRepository类而不是ProductContext类的对象。
- 调用ProductRepository类的方法以对Product实体执行数据库操作,而不是ProductContext类的方法。
在下面的清单中,我使用ProductContext对代码进行了注释,并调用了ProductRepository方法。重构后,ProductController类将如下所示:
使用系统;System.Net使用;System.Web.Mvc使用;ProductApp.Core使用;ProductApp.Infrastructure使用;ProductApp.Web.Controllers命名空间{ ProductsController: Controller公共类{ // private ProductContext db = new ProductContext(); 私有ProductRepository db = 新ProductRepository(); 公共ActionResult索引 (){ // 返回视图 (db.Products.ToList()); 返回视图 (db.GetProducts ());} 公共ActionResult详细信息 (int? id){ if (id = = null){ 返回新的HttpStatusCodeResult (HttpStatusCode.BadRequest);} // 产品产品 = db.Products.Find(id);产品产品 = db.FindById(Convert.ToInt32(id)); 如果 (product = = null){ 返回HttpNotFound ();} 退货视图 (产品);} 公共ActionResult创建 (){ 返回视图 ();}[HttpPost][ValidateAntiForgeryToken] 公共ActionResult创建 ([Bind(Include = "Id,Name,Price,inStock" )] 产品产品){ if (ModelState.IsValid){ // db.Products.Add(product); // db.SaveChanges();db.Add (产品); 返回 “Index” (RedirectToAction );} 退货视图 (产品);} 公共ActionResult编辑 (int? id){ 如果 (id = = null){ 返回新的HttpStatusCodeResult (HttpStatusCode.BadRequest);}产品产品 = db.FindById(Convert.ToInt32(id)); 如果 (product = = null){ 返回HttpNotFound ();} 退货视图 (产品);}[HttpPost][ValidateAntiForgeryToken] 公共ActionResult编辑 ([Bind(Include = "Id,Name,Price,inStock" )] 产品产品){ if (ModelState.IsValid){ // db.Entry(product).State = EntityState.Modified; // db.SaveChanges();db.Edit (产品); 返回 “Index” (RedirectToAction );} 退货视图 (产品);} public ActionResult Delete (int? id){ 如果 (id = = null){ 返回新的HttpStatusCodeResult (HttpStatusCode.BadRequest);}产品产品 = db.FindById(Convert.ToInt32(id)); 如果 (product = = null){ 返回HttpNotFound ();} 退货视图 (产品);}[HttpPost,ActionName("删除")][ValidateAntiForgeryToken] 公共ActionResult DeleteConfirmed (int id){ // 产品产品 = db.FindById(Convert.ToInt32(id)); // db.Products.Remove(product); // db.SaveChanges();db.Remove(id); 返回 “Index” (RedirectToAction );} void Dispose (bool disposing) 受保护的重写{ 如果 (处置){ // db.Dispose();} 基地。Dispose (处置);}}}
重构后,让我们继续构建和运行应用程序-我们应该能够这样做并执行CRUD操作。
注入依赖项
现在我们很高兴应用程序已经启动并运行,并且它是使用存储库模式创建的。但是仍然存在一个问题: 我们直接在ProductsController类内部创建ProductRepository类的对象,我们不希望这样。我们想要反转依赖关系,并将注入依赖关系的任务委托给第三方,通常称为DI容器。实际上,ProductsController将要求DI容器返回IProductRepository的实例。
有许多DI容器可用于MVC应用程序。在这个例子中,我们将使用最简单的Unity DI容器。为此,请右键单击MVC项目,然后单击Manage Nuget Package。在NuGet包管理器中搜索Unity.Mvc并安装包。
一旦安装了Unity.Mvc包,让我们继续打开一个App_Start文件夹。在App_Start文件夹中,我们将找到UnityConfig.cs文件。在UnityConfig类中,我们必须注册类型。为此,在UnityConfig类中打开RegisterTypes函数并注册类型,如下面的清单所示:
公共静态void RegisterTypes (IUnityContainer容器){ // TODO: 注册你的类型在这里container.RegisterType<iproductrepository, productrepository = "">(); }
我们已将类型注册到Unity DI容器。现在让我们继续在ProductsController类中做一些重构。在ProductsController的构造函数中,我们将引用传递给存储库接口。每当应用程序需要时,Unity DI容器将通过解析类型在应用程序中注入ProductRepository的具体对象。我们需要重构ProductsController,如下面的清单所示:
ProductsController: Controller公共类{IProductRepository db; 公共ProductsController (IProductRepository db){ 这个 .db = db;}
让我们继续构建和运行应用程序。我们应该让应用程序启动并运行,我们应该能够使用存储库模式和依赖注入来执行CRUD操作!
结论
在本文中,我们逐步学习了如何按照存储库模式创建MVC应用程序。在这样做的时候,我们可以把所有的数据库逻辑放在一个地方,只要需要,我们只需要改变存储库和测试。存储库模式还将应用程序UI与数据库逻辑和域实体松散地耦合,并使您的应用程序更具可测试性。
另外,不要忘记查看Ignite UI ,您可以将其与HTML5,Angular,React或ASP.NET MVC一起使用来创建富Internet应用程序。感谢您的阅读!