跳至正文
首页 » 博客 » How to Implement the Repository Pattern in ASP.NET MVC Application

How to Implement the Repository Pattern in ASP.NET MVC Application

存储库模式是创建企业级应用程序的最流行模式之一。它限制了我们直接处理应用程序中的数据,并为数据库操作、业务逻辑和应用程序的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在这里

项目结构

让我们从创建应用程序的项目结构开始。我们将创建四个项目:

  1. 核心项目
  2. 基础设施项目
  3. 测试项目
  4. MVC项目

每个项目都有自己的目的。您可能可以通过项目的名称猜出它们将包含的内容: 核心和基础设施项目是类库,Web项目是MVC项目,测试项目是单元测试项目。最终,解决方案资源管理器中的项目将如下图所示:

随着我们在这篇文章中的进展,我们将详细了解每个项目的目的,但是,首先我们可以将每个项目的主要目标总结如下:

到目前为止,我们对不同项目的理解是清楚的。现在让我们继续下去,逐一实施每个项目。在实施过程中,我们将详细探讨每个项目的职责。

核心项目

在核心项目中,我们保留实体和存储库接口或数据库操作接口。核心项目包含有关域实体和域实体所需的数据库操作的信息。在理想情况下,核心项目不应该对外部库有任何依赖。它不能有任何业务逻辑,数据库操作代码等。

简而言之,核心项目应包含:

  • 域实体
  • 存储库接口或数据库操作接口在域实体
  • 域特定数据注释

核心项目不能包含:

  • 任何外部库用于数据库操作
  • 业务逻辑
  • 数据库操作代码

在创建域实体时,我们还需要决定对域实体属性的限制,例如:

  • 是否需要特定的属性。例如,对于产品实体,产品的名称应该是必需属性。
  • 特定属性的值是否在给定范围内。例如,对于产品实体,价格属性应在给定范围内。
  • 特定属性的最大长度是否不应被赋予值。例如,对于Product实体,name属性值应小于最大长度。

域实体属性上可能有许多这样的数据注释。我们可以通过两种方式来考虑这些数据注释:

  1. 作为域实体
  2. 作为数据库操作逻辑
  3. 的一部分

这完全取决于我们如何看待数据注释。如果我们认为它们是数据库操作的一部分,那么我们可以使用数据库操作库API来应用限制。我们将在基础设施项目中使用实体框架进行数据库操作,因此我们可以使用实体框架Fluent API来注释数据。

如果我们认为它们是域的一部分,那么我们可以使用库System.ComponentModel.DataAnnotations来注释数据。要使用此功能,请右键单击核心项目的引用文件夹,然后单击添加引用。从 “框架” 选项卡中,选择 “ System.ComponentModel.DataAnnotations ”,然后将其添加到项目中。

我们正在创建一个ProductApp,所以让我们从创建产品实体开始。要添加实体类,请右键单击核心项目并添加一个类,然后将该类命名为Product。

使用

System.ComponentModel.DataAnnotationsProductApp.Core命名空间{    公共产品{        public int Id {get; set; }[必填][最大长度 (100)]        公共字符串名称 {get; set; }[必填]        公共双倍价格 {get; set; }        公共bool inStock {get; set; }}}

我们已经用RequiredMaxLength注释了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操作

我们可以使用任何数据库技术来执行数据库操作。在这篇文章中,我们将使用实体框架。因此,我们将使用代码优先方法创建一个数据库。在代码优先的方法中,数据库基于类创建。这里数据库将在核心项目的域实体的基础上创建。

要从核心项目域实体创建数据库,我们需要执行以下任务:

  1. 创建DataContext类
  2. 配置连接字符串
  3. 创建数据库初始化器类以将数据存储在数据库
  4. 实现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表。要为数据设定种子:

  1. 重写种子方法
  2. 将产品添加到上下文。产品
  3. 调用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类编写单元测试,让我们在测试项目中添加以下引用。

  1. 引用ProductApp.Core项目
  2. 参考ProductApp.Infrastructure项目
  3. 实体框架包

要添加实体框架,请右键单击测试项目,然后单击管理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表中产生两行种子。由于这是我们将执行的第一个测试,它还将验证数据库是否被创建。所以基本上我们正在写一个测试:

  1. 验证数据库创建
  2. 验证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项目!与测试项目一样,我们需要添加以下引用

  1. 引用ProductApp.Core项目
  2. 参考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控制器中执行数据库操作。总结一下,

  1. MVC脚手架使用数据上下文类来执行数据库操作。数据上下文类是一个实体框架组件,因此它的使用将UI (MVC) 与数据库 (EF) 技术紧密耦合。
  2. 数据上下文类不是单元测试,所以使用它不是一个好主意。
  3. 我们有一个经过测试的ProductRepository类。我们应该使用这个内部控制器来执行数据库操作。此外,ProductRepository类不会向UI公开数据库技术。

要使用ProductRepository类进行数据库操作,我们需要重构ProductsController类。为此,我们需要遵循两个步骤:

  1. 创建ProductRepository类而不是ProductContext类的对象。
  2. 调用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应用程序。感谢您的阅读!

</p