在大型项目中使用通用存储库/工作单元模式

时间:2014-07-23 09:18:23

标签: c# entity-framework design-patterns repository-pattern unit-of-work

我正在处理一个非常大的应用程序。该域有大约20-30种类型,实现为ORM类(例如EF Code First或XPO,对于该问题并不重要)。我已经阅读了几篇关于存储库模式的通用实现的文章和建议,并将它与工作单元模式相结合,产生了类似这样的代码:

public interface IRepository<T> {
  IQueryable<T> AsQueryable();
  IEnumerable<T> GetAll(Expression<Func<T, bool>> filter);
  T GetByID(int id);

  T Create();
  void Save(T);
  void Delete(T);
}

public interface IMyUnitOfWork : IDisposable {
  void CommitChanges();
  void DropChanges();

  IRepository<Product> Products { get; }
  IRepository<Customer> Customers { get; }
}

这种模式适合大型应用吗?每个示例在工作单元中都有大约2个,最多3个存储库。据我了解的模式,在一天结束时,存储库引用的数量(在实现中初始化的延迟)与域实体类的数量相等(或几乎相等),因此可以使用工作单元复杂的业务逻辑实现。例如,让我们像这样扩展上面的代码:

public interface IMyUnitOfWork : IDisposable {
  ...

  IRepository<Customer> Customers { get; }
  IRepository<Product> Products { get; }
  IRepository<Orders> Orders { get; }

  IRepository<ProductCategory> ProductCategories { get; }
  IRepository<Tag> Tags { get; }

  IRepository<CustomerStatistics> CustomerStatistics  { get; }

  IRepository<User> Users { get; }
  IRepository<UserGroup> UserGroups { get; }
  IRepository<Event> Events { get; }

  ...   
}

在考虑代码味道之前,会引用多少个存储库?或者这种模式完全正常吗?我可以将这个接口分成2个或3个不同的接口,所有接口都实现了IUnitOfWork,但是使用起来会不那么舒服。

更新

我已经检查了@qujck推荐的基本上很好的解决方案here。我的动态存储库注册问题和基于&#34;字典的#34;方法是我想享受对我的存储库的直接引用,因为一些存储库将具有特殊行为。因此,当我编写业务代码时,我希望能够像这样使用它,例如:

using (var uow = new MyUnitOfWork()) {
  var allowedUsers = uow.Users.GetUsersInRolw("myRole");
  // ... or
  var clothes = uow.Products.GetInCategories("scarf", "hat", "trousers");
}

所以我在这里得益于我有一个强类型的IRepository和IRepository引用,因此我可以使用特殊方法(作为扩展方法实现或通过从基接口继承)。如果我使用动态存储库注册和检索方法,我想我会松开这个,或者至少不得不一直做一些丑陋的演员。

关于DI的问题,我会尝试将一个存储库工厂注入我真正的工作单元,因此它可以懒惰地实例化存储库。

2 个答案:

答案 0 :(得分:3)

根据我上面的评论和答案here

略微修改工作抽象单元

public interface IMyUnitOfWork
{
    void CommitChanges();
    void DropChanges();

    IRepository<T> Repository<T>();
}

您可以使用扩展方法公开命名存储库和特定存储库方法

public static class MyRepositories
{
    public static IRepository<User> Users(this IMyUnitOfWork uow)
    {
        return uow.Repository<User>();
    }

    public static IRepository<Product> Products(this IMyUnitOfWork uow)
    {
        return uow.Repository<Product>();
    }

    public static IEnumerable<User> GetUsersInRole(
        this IRepository<User> users, string role)
    {
        return users.AsQueryable().Where(x => true).ToList();
    }

    public static IEnumerable<Product> GetInCategories(
        this IRepository<Product> products, params string[] categories)
    {
        return products.AsQueryable().Where(x => true).ToList();
    }
}

根据需要提供访问数据

using(var uow = new MyUnitOfWork())
{
    var allowedUsers = uow.Users().GetUsersInRole("myRole");

    var result = uow.Products().GetInCategories("scarf", "hat", "trousers");
}

答案 1 :(得分:2)

我倾向于采用的方法是将类型约束从存储库类移动到其中的方法。这意味着,而不是:

public interface IMyUnitOfWork : IDisposable
{
    IRepository<Customer> Customers { get; }
    IRepository<Product> Products { get; }
    IRepository<Orders> Orders { get; }
    ...
}

我有这样的事情:

public interface IMyUnitOfWork : IDisposable
{
    Get<T>(/* some kind of filter expression in T */);
    Add<T>(T);
    Update<T>(T);
    Delete<T>(/* some kind of filter expression in T */);
    ...
}

这样做的主要好处是您的工作单元上只需要一个数据访问对象。缺点是你没有像Products.GetInCategories()这样的特定于类型的方法。这可能有问题,所以我的解决方案通常是两件事之一。

关注点分离

首先,您可以重新考虑“数据访问”和“业务逻辑”之间的分离,以便您拥有一个逻辑层类ProductService,其具有可以执行此操作的方法GetInCategory()

using (var uow = new MyUnitOfWork())
{
    var productsInCategory = GetAll<Product>(p => ["scarf", "hat", "trousers"].Contains(u.Category));
}

您的数据访问和业务逻辑代码仍然是独立的。

查询的封装

或者,您可以实现规范模式,因此您可以拥有一个名称空间MyProject.Specifications,其中有一个基类Specification<T>,它在内部的某处有一个过滤器表达式,因此您可以将其传递给工作单元的对象和UoW可以使用过滤器表达式。这使您可以获得可以传递的派生规范,现在您可以写下:

using (var uow = new MyUnitOfWork())
{
    var searchCategories = new Specifications.Products.GetInCategories("scarf", "hat", "trousers");
    var productsInCategories = GetAll<Product>(searchCategories);
}

如果您想要一个中心位置来保留常用的逻辑,例如“按角色获取用户”或“获取类别中的产品”,那么不要将其保存在您的存储库中(严格来说应该是纯数据访问)您可以在对象本身上使用这些扩展方法。例如,Product可以使用方法或扩展方法InCategory(string)返回Specification<Product>甚至只是Expression<Func<Product, bool>>等过滤器,允许您像这样编写查询:

using (var uow = new MyUnitOfWork())
{
    var productsInCategory = GetAll(Product.InCategories("scarf", "hat", "trousers");
}

(请注意,这仍然是一种通用方法,但类型推断将为您处理。)

这使得所查询对象的所有查询逻辑(或该对象的扩展类)保持不变,这仍然保持您的数据和逻辑代码很好地按类和文件分隔,同时允许您按照您的方式共享它之前一直在分享您的IRepository<T>扩展程序。

实施例

为了给出一个更具体的例子,我在EF中使用这种模式。我没有打扰规格;我只是在逻辑层中有服务类,它为每个逻辑操作使用单个工作单元(“添加新用户”,“获取产品类别”,“保存对产品的更改”等)。它的核心看起来像这样(为简洁而省略了实现,因为它们非常简单):

public class EFUnitOfWork: IUnitOfWork
{
    private DbContext _db;

    public EntityFrameworkSourceAdapter(DbContext context) {...}

    public void Add<T>(T item) where T : class, new() {...}
    public void AddAll<T>(IEnumerable<T> items) where T : class, new() {...}

    public T Get<T>(Expression<Func<T, bool>> filter) where T : class, new() {...}
    public IQueryable<T> GetAll<T>(Expression<Func<T, bool>> filter = null) where T : class, new() {...}

    public void Update<T>(T item) where T : class, new() {...}

    public void Remove<T>(Expression<Func<T, bool>> filter) where T : class, new() {...}

    public void Commit() {...}

    public void Dispose() {...}
}

大多数方法使用_db.Set<T>()来获取相关的DbSet,然后使用提供的Expression<Func<T, bool>>使用LINQ查询它。