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

理解存储库和工作单元模式,并在ASP.NET MVC中使用Entity Framework实现通用存储库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (57投票s)

2014年5月7日

CPOL

7分钟阅读

viewsIcon

287150

downloadIcon

7816

在本文中,我们将尝试理解存储库和工作单元模式的基础知识,并将创建一个小型ASP.NET MVC示例应用程序,以使用Entity Framework实现通用存储库和工作单元类。

引言

在本文中,我们将尝试理解存储库和工作单元模式的基础知识,并将创建一个小型ASP.NET MVC示例应用程序,以使用Entity Framework实现通用存储库和工作单元类。

背景

我记得.NET 1.1时代,我们不得不花费大量时间为每个应用程序编写数据访问代码。尽管代码的性质几乎相同,但数据库架构的差异使我们不得不为每个应用程序编写单独的数据访问层。随着.NET框架新版本的推出,在应用程序中使用ORM(对象关系映射器)的可能性使我们免于编写大量以前需要编写的数据访问代码。

由于ORM使数据访问变得如此直接,因此数据访问逻辑/谓词有可能分散在整个应用程序中。例如,每个控制器都可以拥有所需的ObjectContext实例并执行数据访问。

存储库和工作单元模式提供了一种使用ORM访问数据的简洁方法,将所有数据访问逻辑集中在一个位置,同时保持应用程序的可测试性。与其讨论什么是存储库和工作单元,不如通过实现一个简单的ASP.NET MVC应用程序来理解它们。

使用代码

我们首先尝试创建一个简单的数据库,我们将对其执行CRUD操作。我们将在数据库中定义一个简单的联系人表,如下所示:

现在,数据库/表已创建,我们将使用Entity Framework在应用程序中为这些表生成ADO.NET实体数据模型。生成的实体将如下所示:

使用MVC Scaffolding执行简单数据访问

现在我们已经准备好在应用程序中使用Entity Framework,接下来添加一个控制器,该控制器将使用Entity Framework对Contact表执行CRUD操作。

public class ContactsController : Controller
{
    private SampleDbEntities db = new SampleDbEntities();

    //
    // GET: /Contacts/

    public ActionResult Index()
    {
        return View(db.Contacts.ToList());
    }

    //
    // GET: /Contacts/Details/5

    public ActionResult Details(int id = 0)
    {
        Contact contact = db.Contacts.Single(c => c.ID == id);
        if (contact == null)
        {
            return HttpNotFound();
        }
        return View(contact);
    }

    //
    // GET: /Contacts/Create

    public ActionResult Create()
    {
        return View();
    }

    //
    // POST: /Contacts/Create

    [HttpPost]
    public ActionResult Create(Contact contact)
    {
        if (ModelState.IsValid)
        {
            db.Contacts.AddObject(contact);
            db.SaveChanges();
            return RedirectToAction("Index");
        }

        return View(contact);
    }

    //
    // GET: /Contacts/Edit/5

    public ActionResult Edit(int id = 0)
    {
        Contact contact = db.Contacts.Single(c => c.ID == id);
        if (contact == null)
        {
            return HttpNotFound();
        }
        return View(contact);
    }

    //
    // POST: /Contacts/Edit/5

    [HttpPost]
    public ActionResult Edit(Contact contact)
    {
        if (ModelState.IsValid)
        {
            db.Contacts.Attach(contact);
            db.ObjectStateManager.ChangeObjectState(contact, EntityState.Modified);
            db.SaveChanges();
            return RedirectToAction("Index");
        }
        return View(contact);
    }

    //
    // GET: /Contacts/Delete/5

    public ActionResult Delete(int id = 0)
    {
        Contact contact = db.Contacts.Single(c => c.ID == id);
        if (contact == null)
        {
            return HttpNotFound();
        }
        return View(contact);
    }

    //
    // POST: /Contacts/Delete/5

    [HttpPost, ActionName("Delete")]
    public ActionResult DeleteConfirmed(int id)
    {
        Contact contact = db.Contacts.Single(c => c.ID == id);
        db.Contacts.DeleteObject(contact);
        db.SaveChanges();
        return RedirectToAction("Index");
    }

    protected override void Dispose(bool disposing)
    {
        db.Dispose();
        base.Dispose(disposing);
    }
}

当我们运行此应用程序时,我们将能够对Contacts表执行CRUD操作。

从代码和功能角度来看,这样做并没有错。但是这种方法存在两个问题。

  1. 数据访问代码分散在整个应用程序(控制器)中,这是一个维护噩梦。
  2. Controller中的Action在其内部创建Context。这使得此函数无法使用虚拟数据进行测试,并且除非我们使用测试数据,否则我们无法验证结果。

注意:如果第二点不清楚,建议阅读有关使用MVC进行测试驱动开发的内容。我们不能在本文中讨论它,否则文章会离题。

创建存储库

现在我们如何解决这个问题。我们可以通过将所有Entity Framework的数据访问代码移动到一个地方来解决这个问题。因此,让我们定义一个类,该类将包含Contacts表的所有数据访问逻辑。

但在创建此课程之前,我们先考虑一下第二个问题。如果我们创建一个简单的接口,定义访问Contacts数据的契约,然后在我们提议的类中实现此接口,我们将获得一个好处。然后,我们可以有另一个类实现相同的接口,但使用虚拟数据。现在,只要控制器使用接口,我们的测试项目就可以传递虚拟数据类,并且我们的控制器不会抱怨。

因此,我们首先定义访问Contacts数据的契约。

public interface IRepository<T> where T : class
{
    IEnumerable<T> GetAll(Func<T, bool> predicate = null);
    T Get(Func<T, bool> predicate);
    void Add(T entity);
    void Attach(T entity);
    void Delete(T entity);
}

此类的实现将包含对Contacts表执行CRUD操作的实际逻辑。

public class ContactsRepository : IRepository<Contact>
{
    private SampleDbEntities entities = new SampleDbEntities();

    public IEnumerable<Contact> GetAll(Func<Contact, bool> predicate = null)
    {
        if (predicate != null)
        {
            if (predicate != null)
            {
                return entities.Contacts.Where(predicate);
            }
        }

        return entities.Contacts;
    }

    public Contact Get(Func<Contact, bool> predicate)
    {
        return entities.Contacts.FirstOrDefault(predicate);
    }

    public void Add(Contact entity)
    {
        entities.Contacts.AddObject(entity);
    }

    public void Attach(Contact entity)
    {
        entities.Contacts.Attach(entity);
        entities.ObjectStateManager.ChangeObjectState(entity, EntityState.Modified);
    }

    public void Delete(Contact entity)
    {
        entities.Contacts.DeleteObject(entity);
    }

    internal void SaveChanges()
    {
        entities.SaveChanges();
    }
}

现在让我们创建另一个控制器,它将使用此存储库类对Contacts表执行CRUD操作。我们将其命名为Contacts2Controller

public class Contacts2Controller : Controller
{
    private ContactsRepository repo = new ContactsRepository();
    //
    // GET: /Contacts/

    public ActionResult Index()
    {
        return View(repo.GetAll().ToList());
    }

    //
    // GET: /Contacts/Details/5

    public ActionResult Details(int id = 0)
    {
        Contact contact = repo.Get(c => c.ID == id);
        if (contact == null)
        {
            return HttpNotFound();
        }
        return View(contact);
    }

    //
    // GET: /Contacts/Create

    public ActionResult Create()
    {
        return View();
    }

    //
    // POST: /Contacts/Create

    [HttpPost]
    public ActionResult Create(Contact contact)
    {
        if (ModelState.IsValid)
        {
            repo.Add(contact);
            repo.SaveChanges();
            return RedirectToAction("Index");
        }

        return View(contact);
    }

    //
    // GET: /Contacts/Edit/5

    public ActionResult Edit(int id = 0)
    {
        Contact contact = repo.Get(c => c.ID == id);
        if (contact == null)
        {
            return HttpNotFound();
        }
        return View(contact);
    }

    //
    // POST: /Contacts/Edit/5

    [HttpPost]
    public ActionResult Edit(Contact contact)
    {
        if (ModelState.IsValid)
        {
            repo.Attach(contact);
            repo.SaveChanges();
            return RedirectToAction("Index");
        }
        return View(contact);
    }

    //
    // GET: /Contacts/Delete/5

    public ActionResult Delete(int id = 0)
    {
        Contact contact = repo.Get(c => c.ID == id);
        if (contact == null)
        {
            return HttpNotFound();
        }
        return View(contact);
    }

    //
    // POST: /Contacts/Delete/5

    [HttpPost, ActionName("Delete")]
    public ActionResult DeleteConfirmed(int id)
    {
        Contact contact = repo.Get(c => c.ID == id);
        repo.Delete(contact);
        repo.SaveChanges();
        return RedirectToAction("Index");
    }
}

现在这种方法的优点是我的ORM数据访问代码没有分散在控制器中。它被封装在一个存储库类中。

拥有多个存储库

现在想象一下,如果数据库中有多个表。那么我们需要创建多个存储库才能将领域模型映射到数据模型。现在,拥有多个存储库类会带来一个问题。

问题在于ObjectContext对象。如果创建多个存储库,它们是否应该单独包含它们的ObjectContext?我们知道同时使用多个ObjectContext对象实例是一个问题,那么我们是否真的应该允许每个存储库包含自己的实例呢?

为了解决这个问题。为什么让每个存储库类实例都有自己的ObjectContext实例?为什么不在某个中心位置创建ObjectContext实例,然后在实例化存储库类时将此实例传递给它们。现在,这个新类将被命名为UnitOfWork,该类将负责创建ObjectContext实例并将所有存储库实例交给控制器。

工作单元

因此,让我们创建一个单独的存储库,它将通过UnitOfWork类使用,并且ObjectContext将从外部传递给该类。

public class ContactsRepositoryWithUow : IRepository<Contact>
{
    private SampleDbEntities entities = null;

    public ContactsRepositoryWithUow(SampleDbEntities _entities)
    {
        entities = _entities;
    }

    public IEnumerable<Contact> GetAll(Func<Contact, bool> predicate = null)
    {
        if (predicate != null)
        {
            if (predicate != null)
            {
                return entities.Contacts.Where(predicate);
            }
        }

        return entities.Contacts;
    }

    public Contact Get(Func<Contact, bool> predicate)
    {
        return entities.Contacts.FirstOrDefault(predicate);
    }

    public void Add(Contact entity)
    {
        entities.Contacts.AddObject(entity);
    }

    public void Attach(Contact entity)
    {
        entities.Contacts.Attach(entity);
        entities.ObjectStateManager.ChangeObjectState(entity, EntityState.Modified);
    }

    public void Delete(Contact entity)
    {
        entities.Contacts.DeleteObject(entity);
    }       
}

现在,这个存储库类从外部(无论何时创建)获取ObjectContext对象。

现在,如果我们要创建多个存储库,我们可以简单地让所有存储库在构造时获取ObjectContext对象。现在让我们看看UnitOfWork类如何创建存储库并将其传递给控制器。

public class UnitOfWork : IDisposable
{
    private SampleDbEntities entities = null;
    public UnitOfWork()
    {
        entities = new SampleDbEntities();
    }

    // Add all the repository handles here
    IRepository<Contact> contactRepository = null;

    // Add all the repository getters here
    public IRepository<Contact> ContactRepository
    {
        get
        {
            if (contactRepository == null)
            {
                contactRepository = new ContactsRepositoryWithUow(entities);
            }
            return contactRepository;
        }
    }

    public void SaveChanges()
    {
        entities.SaveChanges();
    }


    private bool disposed = false;

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                entities.Dispose();
            }
        }
        this.disposed = true;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

}

现在让我们再创建一个控制器ContactsUowController,它将使用工作单元类对contact表执行CRUD操作。

public class ContactUowController : Controller
{
    private UnitOfWork uow = null; 
    //
    // GET: /Contacts/

    public ContactUowController()
    {
        uow = new UnitOfWork();
    }

    public ContactUowController(UnitOfWork uow_)
    {
        this.uow = uow_;
    }

    public ActionResult Index()
    {
        return View(uow.ContactRepository.GetAll().ToList());
    }

    //
    // GET: /Contacts/Details/5

    public ActionResult Details(int id = 0)
    {
        Contact contact = uow.ContactRepository.Get(c => c.ID == id);
        if (contact == null)
        {
            return HttpNotFound();
        }
        return View(contact);
    }

    //
    // GET: /Contacts/Create

    public ActionResult Create()
    {
        return View();
    }

    //
    // POST: /Contacts/Create

    [HttpPost]
    public ActionResult Create(Contact contact)
    {
        if (ModelState.IsValid)
        {
            uow.ContactRepository.Add(contact);
            uow.SaveChanges();
            return RedirectToAction("Index");
        }

        return View(contact);
    }

    //
    // GET: /Contacts/Edit/5

    public ActionResult Edit(int id = 0)
    {
        Contact contact = uow.ContactRepository.Get(c => c.ID == id);
        if (contact == null)
        {
            return HttpNotFound();
        }
        return View(contact);
    }

    //
    // POST: /Contacts/Edit/5

    [HttpPost]
    public ActionResult Edit(Contact contact)
    {
        if (ModelState.IsValid)
        {
            uow.ContactRepository.Attach(contact);
            uow.SaveChanges();
            return RedirectToAction("Index");
        }
        return View(contact);
    }

    //
    // GET: /Contacts/Delete/5

    public ActionResult Delete(int id = 0)
    {
        Contact contact = uow.ContactRepository.Get(c => c.ID == id);
        if (contact == null)
        {
            return HttpNotFound();
        }
        return View(contact);
    }

    //
    // POST: /Contacts/Delete/5

    [HttpPost, ActionName("Delete")]
    public ActionResult DeleteConfirmed(int id)
    {
        Contact contact = uow.ContactRepository.Get(c => c.ID == id);
        uow.ContactRepository.Delete(contact);
        uow.SaveChanges();
        return RedirectToAction("Index");
    }
}

现在,通过结合默认和参数化构造函数,该控制器的可测试性仍然得以保持。即,测试项目可以传递在虚拟数据而不是实际数据中运行的UnitOfWork。此外,数据访问代码现在集中在一个地方,并且可以同时实例化多个存储库类。

通用存储库和工作单元

现在我们已经有了存储库和UnitOfWork类。但这里的症结是,如果我的数据库包含很多表,那么我将不得不创建同样多的存储库类,并且我的UnitOfWork类需要有同样多的访问器属性来访问这些存储库。

如果我们让我们的存储库和UnitOfwork通用,它们将适用于所有模型类,那不是更好吗?所以让我们继续实现一个通用存储库类。

class GenericRepository<T> : IRepository<T> where T : class
{
    private SampleDbEntities entities = null;
    IObjectSet<T> _objectSet;

    public GenericRepository(SampleDbEntities _entities)
    {
        entities = _entities;
        _objectSet = entities.CreateObjectSet<T>();
    }

    public IEnumerable<T> GetAll(Func<T, bool> predicate = null)
    {
        if (predicate != null)
        {
            return _objectSet.Where(predicate);
        }

        return _objectSet.AsEnumerable();
    }

    public T Get(Func<T, bool> predicate)
    {
        return _objectSet.First(predicate);
    }

    public void Add(T entity)
    {
        _objectSet.AddObject(entity);
    }

    public void Attach(T entity)
    {
        _objectSet.Attach(entity);
    }

    public void Delete(T entity)
    {
        _objectSet.DeleteObject(entity);
    }
}

更新:发现了一篇关于这篇文章的非常有趣的评论,我认为值得放到文章中。

'Where'方法至少有2个重载

public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate);
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);

当我们使用

Func<T, bool>

查询将使用“IEnumerable”版本。在这种情况下,整个表记录将首先从数据库中获取,然后将谓词应用于最终结果。为了证明这一点,只需检查生成的SQL。它没有where子句。

为了解决这个问题,我们需要将“Func”更改为“Expression Func”。

Expression<Func<T, bool>> predicate 

现在将使用“IQueryable”版本的“Where”方法。

注意:因此,使用Expression Func可能比使用Func更好。

现在有了这个通用存储库,我们将创建一个通用工作单元类,它将与这个通用存储库一起工作。这个工作单元类将检查特定类型的存储库类是否已经创建,如果已创建,将返回相同的实例。否则,将返回一个新的实例。

public class GenericUnitOfWork : IDisposable
{
    private SampleDbEntities entities = null;
        
    public GenericUnitOfWork()
    {
        entities = new SampleDbEntities();
    }

    public Dictionary<Type, object> repositories = new Dictionary<Type, object>();

    public IRepository<T> Repository<T>() where T : class
    {
        if (repositories.Keys.Contains(typeof(T)) == true)
        {
            return repositories[typeof(T)] as IRepository<T>
        }
        IRepository<T> repo = new GenericRepository<T>(entities);
        repositories.Add(typeof(T), repo);
        return repo;
    }

    public void SaveChanges()
    {
        entities.SaveChanges();
    }

    private bool disposed = false;

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                entities.Dispose();
            }
        }
        this.disposed = true;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

现在让我们再创建一个控制器GenericContactsController,它将使用GenericUnitOfWork类对contact表执行CRUD操作。

public class GenericContactsController : Controller
{
    private GenericUnitOfWork uow = null; 
    //
    // GET: /Contacts/

    public GenericContactsController()
    {
        uow = new GenericUnitOfWork();
    }

    public GenericContactsController(GenericUnitOfWork uow_)
    {
        this.uow = uow_;
    }

    public ActionResult Index()
    {
        return View(uow.Repository<Contact>().GetAll().ToList());
    }

    //
    // GET: /Contacts/Details/5

    public ActionResult Details(int id = 0)
    {
        Contact contact = uow.Repository<Contact>().Get(c => c.ID == id);
        if (contact == null)
        {
            return HttpNotFound();
        }
        return View(contact);
    }

    //
    // GET: /Contacts/Create

    public ActionResult Create()
    {
        return View();
    }

    //
    // POST: /Contacts/Create

    [HttpPost]
    public ActionResult Create(Contact contact)
    {
        if (ModelState.IsValid)
        {
            uow.Repository<Contact>().Add(contact);
            uow.SaveChanges();
            return RedirectToAction("Index");
        }

        return View(contact);
    }

    //
    // GET: /Contacts/Edit/5

    public ActionResult Edit(int id = 0)
    {
        Contact contact = uow.Repository<Contact>().Get(c => c.ID == id);
        if (contact == null)
        {
            return HttpNotFound();
        }
        return View(contact);
    }

    //
    // POST: /Contacts/Edit/5

    [HttpPost]
    public ActionResult Edit(Contact contact)
    {
        if (ModelState.IsValid)
        {
            uow.Repository<Contact>().Attach(contact);
            uow.SaveChanges();
            return RedirectToAction("Index");
        }
        return View(contact);
    }

    //
    // GET: /Contacts/Delete/5

    public ActionResult Delete(int id = 0)
    {
        Contact contact = uow.Repository<Contact>().Get(c => c.ID == id);
        if (contact == null)
        {
            return HttpNotFound();
        }
        return View(contact);
    }

    //
    // POST: /Contacts/Delete/5

    [HttpPost, ActionName("Delete")]
    public ActionResult DeleteConfirmed(int id)
    {
        Contact contact = uow.Repository<Contact>().Get(c => c.ID == id);
        uow.Repository<Contact>().Delete(contact);
        uow.SaveChanges();
        return RedirectToAction("Index");
    }
}

现在我们有了一个带工作单元的通用存储库类。

关注点

在本文中,我们了解了什么是存储库和工作单元模式。我们还看到了在ASP.NET MVC应用程序中使用Entity Framework对存储库和工作单元模式的初步实现。然后我们看到了如何实现一个通用存储库类,并使其与工作单元类一起工作,这样我们就不需要创建多个存储库类。我希望这篇文章能对您有所帮助。

历史

  • 2014年5月7日:第一版
© . All rights reserved.