注意
我不是在问我是否应该使用仓储模式,我关心的是“如何”。将与持久化相关的对象注入到领域类中对我来说不是一个选项:它会使单元测试变得不可能(不,使用内存数据库的测试不是单元测试,因为它们涵盖了许多不同的类而没有隔离),它将领域逻辑与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()
方法。以下代码片段将无法正常工作:
更改是在另一个类中提交的,它实现了工作单元模式。我对这种实现的问题在于它违反了仓储作为“类似集合”的对象的定义。该类的用户必须知道数据已持久化在外部存储中,并自行调用Save()
方法。以下代码片段将无法正常工作:
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);
当事务范围结束时,更改可以提交到数据库,但是除了处理事务的低级代码之外,我不希望领域逻辑关心事务何时被提交。为了以这种方式实现接口,我们可以使用DbSet的Local
集合,以及常规的DB查询。代码如下:
...
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官方文档(有点过时,但是思想是一样的)也会在查询时忽略本地更改。
- 是否有更好的解决方案可以包含EF Core中的本地更改,而无需每次手动查询数据库和
Local
集合?
更新 - 我的解决方案
最终我采用了@Ronald答案建议的第二种解决方案。我让仓库自动保存更改到数据库,并将每个请求封装在数据库事务中。我从建议的解决方案中改变的一件事是,我在每次读取时调用了SaveChangesAsync
。这类似于Java中的Hibernate
的做法。这是一个简化的实现:
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 方法而必须的。