65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (30投票s)

2013年4月12日

CPOL

6分钟阅读

viewsIcon

144734

downloadIcon

2999

在本文中,我们将尝试了解 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 操作,因为它们很容易以相同的方式完成。

为了可视化上述实现:

 

从代码和功能的角度来看,这样做没有什么不对。但这种方法存在两个问题。

  1. 数据访问代码分散在整个应用程序中,这是一个维护的噩梦。
  2. 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日:初版。
© . All rights reserved.