在大型项目中使用通用存储库/工作单元模式
本文关键字:工作 单元 模式 大型项目 存储 | 更新日期: 2023-09-27 18:14:14
我正在开发一个相当大的应用程序。该领域有大约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推荐的一个基本不错的解决方案。我对动态存储库注册和"基于字典"方法的问题是,我想要享受对存储库的直接引用,因为一些存储库将具有特殊行为。因此,当我编写业务代码时,我希望能够像这样使用它例如:
using (var uow = new MyUnitOfWork()) {
var allowedUsers = uow.Users.GetUsersInRolw("myRole");
// ... or
var clothes = uow.Products.GetInCategories("scarf", "hat", "trousers");
}
所以在这里我受益于我有一个强类型的IRepository和IRepository引用,因此我可以使用特殊的方法(作为扩展方法实现或通过继承基接口)。如果我使用动态存储库注册和检索方法,我认为我将失去这个,或者至少必须一直做一些丑陋的强制类型转换。
对于DI而言,我会尝试将存储库工厂注入到我的实际工作单元中,这样它就可以惰性地实例化存储库。
建立在我上面的评论和这里的答案之上。
使用稍微修改过的工作抽象单元
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");
}
我倾向于采用的方法是将类型约束从存储库类移动到其中的方法。这就意味着:
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()
这样的特定于类型的方法。这可能会有问题,所以我的解决方案通常是两种方法之一。
关注点分离
首先,您可以重新考虑"数据访问"answers";以及"商业逻辑";因此,您有一个逻辑层类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);
}
如果你想要一个中央位置来保存常用的逻辑,比如"get user by role";或者"获取类别中的产品",而不是将其保存在存储库中(严格来说,这应该是纯粹的数据访问),然后您可以在对象本身上使用这些扩展方法。例如,Product
可以有一个方法或扩展方法InCategory(string)
,它返回Specification<Product>
,甚至只是一个过滤器,如Expression<Func<Product, bool>>
,允许您这样编写查询:
using (var uow = new MyUnitOfWork())
{
var productsInCategory = GetAll(Product.InCategories("scarf", "hat", "trousers");
}
(注意,这仍然是一个泛型方法,但类型推断将为您处理它。)
这保留了被查询对象(或该对象的扩展类)上的所有查询逻辑,这仍然使您的数据和逻辑代码按照类和文件很好地分开,同时允许您像以前共享IRepository<T>
扩展一样共享它。
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
,然后使用LINQ使用提供的Expression<Func<T, bool>>
进行查询。