ASP.NET MVC 应用程序中的存储库和工作单元模式详解及实现






4.90/5 (30投票s)
在本文中,我们将尝试了解 ASP.NET MVC 应用程序中的存储库(Repository)和工作单元(Unit of Work)模式。
引言
在本文中,我们将尝试了解 ASP.NET MVC 应用程序中的存储库(Repository)和工作单元(Unit of Work)模式。我们还将实现一个小型基础示例应用程序来理解它们。
背景
在我们的应用程序中使用 ORM 的可能性,为我们节省了大量用于创建实体和数据访问逻辑的代码。但是,使用像 Entity Framework 这样的 ORM 有时会导致数据访问逻辑/谓词分散在代码的各个地方。
存储库和工作单元模式提供了一种干净的数据访问方式,同时保持了应用程序的可测试性。让我们通过实现一个简单的 ASP.NET MVC 应用程序来尝试理解这一点。
使用代码
让我们先创建一个简单的数据库,我们将在其中执行 CRUD 操作。我们将在数据库中定义简单的表,如下所示:

在创建了数据库/表之后,我们将继续在我们的应用程序中为这些表生成 ADO.NET Entity Data Model
。生成的实体将如下所示:

执行简单的数据访问
现在,我们的 Entity Framework 已经准备好在应用程序中使用。我们完全可以在每个控制器中使用 Context 类来执行数据库操作。让我们尝试通过在 HomeController
的 Index 操作中检索数据来做到这一点。
public ActionResult Index()
{
List<Book> books = null;
using (SampleDatabaseEntities entities = new SampleDatabaseEntities())
{
books = entities.Books.ToList();
}
return View(books);
}
当我们尝试运行此应用程序时,我们将看到它正在从数据库中获取数据,如下所示:

注意:我们在这里不会进行其他 CRUD 操作,因为它们很容易以相同的方式完成。
为了可视化上述实现:
从代码和功能的角度来看,这样做没有什么不对。但这种方法存在两个问题。
- 数据访问代码分散在整个应用程序中,这是一个维护的噩梦。
Controller
中的Action
在其内部创建Context
。这使得该函数无法使用模拟数据进行测试,除非我们使用测试数据,否则我们永远无法验证结果。
注意:如果第二点不清楚,建议阅读有关使用 MVC 进行测试驱动开发的内容。否则,本文将变得离题。
创建存储库
那么我们如何解决这个问题呢?我们可以通过将所有 Entity Framework 的数据访问代码移动到一个地方来解决这个问题。所以,让我们定义一个类,它将包含 Books
表的所有数据访问逻辑。
但在创建这个类之前,让我们也暂时考虑一下第二个问题。如果我们创建一个简单的接口来定义访问图书数据的契约,然后在我们提议的类中实现这个接口,我们会有一个好处。然后我们可以有另一个类实现相同的接口,但使用模拟数据。现在,只要控制器使用接口,我们的测试项目就可以传递模拟数据类,而我们的控制器也不会抱怨。
所以,让我们先定义访问图书数据的契约。
// This interface will give define a contract for CRUD operations on
// Books entity
interface IBooksRepository
{
List<Book> GetAllBooks();
Book GetBookById(int id);
void AddBook(Book book);
void UpdateBook(int id, Book book);
void DeleteBook(Book book);
void Save();
}
该类的实现将包含对 Books
表执行 CRUD 操作的实际逻辑。
public class BooksRepository : IBooksRepository, IDisposable
{
SampleDatabaseEntities entities = new SampleDatabaseEntities();
#region IBooksRepository Members
BooksRepository()
{
entities = new SampleDatabaseEntities();
}
public List<Book> GetAllBooks()
{
return entities.Books.ToList();
}
public Book GetBookById(int id)
{
return entities.Books.SingleOrDefault(book => book.ID == id);
}
public void AddBook(Book book)
{
entities.Books.AddObject(book);
}
public void UpdateBook(int id, Book book)
{
Book b = GetBookById(id);
b = book;
}
public void DeleteBook(Book book)
{
entities.Books.DeleteObject(book);
}
public void Save()
{
entities.SaveChanges();
}
#endregion
#region IDisposable Members
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing == true)
{
entities = null;
}
}
~BooksRepository()
{
Dispose(false);
}
#endregion
}
现在,让我们创建一个简单的 Controller,其中我们将引用这个类来执行对 Books 表的 CRUD 操作。
public class BooksController : Controller
{
private IBooksRepository booksRepository = null;
public BooksController()
:this(new BooksRepository())
{
}
public BooksController(IBooksRepository bookRepo)
{
this.booksRepository = bookRepo;
}
public ActionResult Index()
{
List<Book> books = booksRepository.GetAllBooks();
return View(books);
}
}
在上面的代码中,当应用程序运行时,将运行默认的无参构造函数,该函数将创建一个 BooksRepository
对象,并在类中使用它。其结果是,应用程序将能够使用数据库中的实际数据。
现在,从我们的测试项目中,我们将调用带参数的构造函数,并传入一个包含模拟数据的模拟类对象。这样做的好处是,我们应该能够使用模拟数据来测试和验证控制器类。
让我们运行应用程序来查看输出。
注意:我们在这里不会进行其他 CRUD 操作,因为它们很容易以相同的方式完成。
让我们尝试可视化这个实现版本。
拥有多个存储库
现在想象一下,我们的数据库中有多个表。那么我们需要创建多个存储库来映射域模型到数据模型。现在,拥有多个存储库类会带来一个问题。
问题是关于 ObjectContext
对象。如果我们创建多个存储库,它们应该分别包含它们的 ObjectContext
吗?我们知道同时使用多个 ObjectContext
对象实例可能会有问题,所以我们真的应该允许每个存储库拥有自己的实例吗?
为了解决这个问题。为什么让每个存储库类实例拥有自己的 ObjectContext
实例呢?为什么不在某个中心位置创建 ObjectContext
实例,然后在实例化存储库类时将该实例传递给它们呢?现在,这个新类将被称为 UnitOfWork
,并且这个类将负责创建 ObjectContext
实例并将所有存储库实例交给控制器。
工作单元
所以,让我们创建一个独立的存储库,它将通过 UnitOfWork
类使用,并且 ObjectContext
将从外部传递给这个类。
public class BooksRepositoryEn
{
SampleDatabaseEntities entities = null;
public BooksRepositoryEn(SampleDatabaseEntities entities)
{
this.entities = entities;
}
public List<Book> GetAllBooks()
{
return entities.Books.ToList();
}
public Book GetBookById(int id)
{
return entities.Books.SingleOrDefault(book => book.ID == id);
}
public void AddBook(Book book)
{
entities.Books.AddObject(book);
}
public void UpdateBook(int id, Book book)
{
Book b = GetBookById(id);
b = book;
}
public void DeleteBook(Book book)
{
entities.Books.DeleteObject(book);
}
public void Save()
{
entities.SaveChanges();
}
}
现在,这个 Repository 类从外部接收 ObjectContext
对象(无论何时创建它)。此外,我们不需要在这里实现 IDisposable
,因为这个类没有创建实例,所以它不负责处理它。
现在,如果我们必须创建多个存储库,我们可以简单地让所有存储库在构造时接收 ObjectContext
对象。现在,让我们看看 UnitOfWork
类如何创建存储库并将其传递给 Controller。
public class UnitOfWork : IDisposable
{
private SampleDatabaseEntities entities = null;
// This will be called from controller default constructor
public UnitOfWork()
{
entities = new SampleDatabaseEntities();
BooksRepository = new BooksRepositoryEn(entities);
}
// This will be created from test project and passed on to the
// controllers parameterized constructors
public UnitOfWork(IBooksRepository booksRepo)
{
BooksRepository = booksRepo;
}
public IBooksRepository BooksRepository
{
get;
private set;
}
#region IDisposable Members
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing == true)
{
entities = null;
}
}
~UnitOfWork()
{
Dispose(false);
}
#endregion
}
现在我们有一个无参构造函数,它将由控制器默认构造函数调用(即,当我们的页面运行时)。我们还有一个带参构造函数,它将由测试项目创建并传递给控制器的带参构造函数。
Dispose
模式现在由 UnitOfWork
类实现,因为现在它负责创建 ObjectContext
,所以它应该是负责处理它的那个。
让我们看看 Controller 类的实现。
public class BookEnController : Controller
{
private UnitOfWork unitOfWork = null;
public BookEnController()
: this(new UnitOfWork())
{
}
public BookEnController(UnitOfWork uow)
{
this.unitOfWork = uow;
}
public ActionResult Index()
{
List<Book> books = unitOfWork.BooksRepository.GetAllBooks();
return View(books);
}
}
现在,通过默认和带参数构造函数的组合,这个控制器的可测试性仍然得以保持。此外,数据访问代码现在集中在一个地方,并且可以同时实例化多个存储库类。让我们运行应用程序。

注意:我们在这里不会进行其他 CRUD 操作,因为它们很容易以相同的方式完成。
最后,让我们可视化我们实现的工作单元。
关注点
在本文中,我们了解了存储库和工作单元模式。我们还在 ASP.NET MVC 应用程序中看到了它们的初步实现。项目的下一步将是把所有存储库类转换为一个通用的存储库,这样我们就无需创建多个存储库类。我希望这能为您带来信息。
历史
- 2013年4月12日:初版。