【发布时间】:2020-11-22 17:06:17
【问题描述】:
注意
我不是问是否我应该使用存储库模式,我关心的是如何。将与持久性相关的对象注入域类对我来说不是一个选择:它使单元测试成为不可能(不,使用内存数据库的测试不是单元测试,因为它们涵盖了许多不同的类而没有隔离),它结合了域逻辑使用 ORM,它打破了我实践的许多重要原则,例如持久性无知、关注点分离等,欢迎您在线搜索其好处。 “正确”使用 EF Core 对我来说并不像保持业务逻辑与外部关注点隔离一样重要,这就是为什么如果这意味着存储库不会泄漏,我将满足于 EF Core 的“hacky”使用抽象了。
原始问题
假设存储库的界面如下:
public interface IRepository<TEntity>
where TEntity : Entity
{
void Add(TEntity entity);
void Remove(TEntity entity);
Task<TEntity?> FindByIdAsync(Guid id);
}
public abstract class Entity
{
public Entity(Guid id)
{
Id = id;
}
public Guid Id { get; }
}
我在网上看到的大部分 EF Core 实现都是这样的:
public class EFCoreRepository<TEntity> : IRepository<TEntity>
where TEntity : Entity
{
private readonly DbSet<TEntity> entities;
public EFCoreRepository(DbContext dbContext)
{
entities = dbContext.Set<TEntity>();
}
public void Add(TEntity entity)
{
entities.Add(entity);
}
public void Remove(TEntity entity)
{
entities.Remove(entity);
}
public async Task<TEntity?> FindByIdAsync(Guid id)
{
return await entities.FirstOrDefaultAsync(e => e.Id == id);
}
}
更改在另一个类中提交,在工作单元模式的实现中。
我对这个实现的问题是它违反了将存储库定义为“类集合”对象的定义。此类的用户必须知道数据保存在外部存储中并自己调用Save() 方法。以下 sn-p 不起作用:
var entity = new ConcreteEntity(id: Guid.NewGuid());
repository.Add(entity);
var result = await repository.FindByIdAsync(entity.Id); // Will return null
显然不应该在每次调用 Add() 后提交更改,因为它违背了工作单元的目的,所以我们最终得到了一个奇怪的、不是非常类似于集合的存储库接口。
在我看来,我们应该能够像对待常规内存中的集合一样对待存储库:
var list = new List<ConcreteEntity>();
var entity = new ConcreteEntity(id: Guid.NewGuid());
list.Add(entity);
// No need to save here
var result = list.FirstOrDefault(e => e.Id == entity.Id);
当事务范围结束时,可以将更改提交到数据库,但除了处理事务的低级代码之外,我不希望域逻辑关心何时提交事务。除了常规的 DB 查询之外,我们可以用这种方式实现接口的方法是使用 DbSet 的 Local 集合。那将是:
...
public async Task<TEntity?> FindByIdAsync(Guid id)
{
var entity = entities.Local.FirstOrDefault(e => e.Id == id);
return entity ?? await entities.FirstOrDefaultAsync(e => e.Id == id);
}
这行得通,但是这个通用实现将在具体存储库中派生,并使用许多其他查询数据的方法。所有这些查询都必须在考虑Local 集合的情况下实现,而且我还没有找到一种干净的方法来强制具体存储库不忽略本地更改。所以我的问题真的归结为:
- 我对存储库模式的解释是否正确?为什么在线其他实现中没有提到这个问题?甚至官方文档网站中的Microsoft's implementation(有点过时了,但思路都是一样的)在查询时忽略了本地的变化。
- 有没有比每次手动查询 DB 和
Local集合更好的解决方案来包含 EF Core 中的本地更改?
更新 - 我的解决方案
我最终实施了@Ronald 的回答建议的第二个解决方案。我使存储库自动保存对数据库的更改,并将每个请求包装在数据库事务中。我从提议的解决方案中改变的一件事是我在每次读取时调用SaveChangesAsync,而不是写入。这类似于 Hibernate 已经在做的事情(在 Java 中)。这是一个简化的实现:
public abstract class EFCoreRepository<TEntity> : IRepository<TEntity>
where TEntity : Entity
{
private readonly DbSet<TEntity> dbSet;
public EFCoreRepository(DbContext dbContext)
{
dbSet = dbContext.Set<TEntity>();
Entities = new EntitySet<TEntity>(dbContext);
}
protected IQueryable<TEntity> Entities { get; }
public void Add(TEntity entity)
{
dbSet.Add(entity);
}
public async Task<TEntity?> FindByIdAsync(Guid id)
{
return await Entities.SingleOrDefaultAsync(e => e.Id == id);
}
public void Remove(TEntity entity)
{
dbSet.Remove(entity);
}
}
internal class EntitySet<TEntity> : IQueryable<TEntity>
where TEntity : Entity
{
private readonly DbSet<TEntity> dbSet;
public EntitySet(DbContext dbContext)
{
dbSet = dbContext.Set<TEntity>();
Provider = new AutoFlushingQueryProvider<TEntity>(dbContext);
}
public Type ElementType => dbSet.AsQueryable().ElementType;
public Expression Expression => dbSet.AsQueryable().Expression;
public IQueryProvider Provider { get; }
// GetEnumerator() omitted...
}
internal class AutoFlushingQueryProvider<TEntity> : IAsyncQueryProvider
where TEntity : Entity
{
private readonly DbContext dbContext;
private readonly IAsyncQueryProvider internalProvider;
public AutoFlushingQueryProvider(DbContext dbContext)
{
this.dbContext = dbContext;
var dbSet = dbContext.Set<TEntity>().AsQueryable();
internalProvider = (IAsyncQueryProvider)dbSet.Provider;
}
public TResult ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken = default)
{
var internalResultType = typeof(TResult).GenericTypeArguments.First();
// Calls this.ExecuteAsyncCore<internalResultType>(expression, cancellationToken)
object? result = GetType()
.GetMethod(nameof(ExecuteAsyncCore), BindingFlags.NonPublic | BindingFlags.Instance)
?.MakeGenericMethod(internalResultType)
?.Invoke(this, new object[] { expression, cancellationToken });
if (result is not TResult)
throw new Exception(); // This should never happen
return (TResult)result;
}
private async Task<TResult> ExecuteAsyncCore<TResult>(Expression expression, CancellationToken cancellationToken)
{
await dbContext.SaveChangesAsync(cancellationToken);
return await internalProvider.ExecuteAsync<Task<TResult>>(expression, cancellationToken);
}
// Other interface methods omitted...
}
注意IAsyncQueryProvider 的使用,这迫使我使用了一个小的反射黑客。这是支持 EF Core 附带的异步 LINQ 方法所必需的。
【问题讨论】:
-
EF 本身是存储库和工作单元模式的实现。在此之上分层附加抽象通常会增加复杂性、降低可维护性、降低可重用性并降低运行时效率。
-
您的应用程序的真实来源是什么 - 是数据库还是应用程序内存?如果它是数据库,则您的存储库将按预期运行,并且仅返回已保存的对象。我认为这些考虑比尝试实现“类似集合”的存储库更重要。
-
数据库是一个实现细节,领域模型不应该知道它的存在。如果有什么是“真相的来源”,那就是内存中的聚合,但即使我们确实关心细节,我说的是在同一事务中进行的本地更改,因此数据库将始终保持一致。跨度>
-
我明白你的观点@GurGaller。我认为
database是实现细节,但persistence不是,但我在这里可能错了。我认为这个问题很有趣,会寻找一个体面的答案:) -
是的,让我们看看是否有人有更好的想法。我同意,似乎“作为集合的存储库”正在泄露它持久存在的事实。
标签: c# entity-framework-core domain-driven-design repository-pattern ddd-repositories