使用 EF Core 正确实现存储库模式

Gur*_*ler 11 c# domain-driven-design ddd-repositories repository-pattern entity-framework-core

笔记

我没有问,如果我要使用Repository模式,我关心的如何。将与持久性相关的对象注入域类对我来说不是一个选择:它使单元测试变得不可能(不,使用内存数据库的测试不是单元测试,因为它们没有隔离地覆盖了许多不同的类),它耦合了域逻辑使用 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; }
}
Run Code Online (Sandbox Code Playgroud)

我在网上看到的大多数 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

更改在另一个类中提交,在工作单元模式的实现中。我在这个实现中遇到的问题是它违反了存储库作为“类集合”对象的定义。此类的用户必须知道数据保存在外部存储中并Save()自己调用该方法。以下代码段不起作用:

var entity = new ConcreteEntity(id: Guid.NewGuid());
repository.Add(entity);
var result = await repository.FindByIdAsync(entity.Id); // Will return null
Run Code Online (Sandbox Code Playgroud)

显然不应该在每次调用 之后提交更改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);
Run Code Online (Sandbox Code Playgroud)

当事务范围结束时,更改可以提交到数据库,但除了处理事务的低级代码之外,我不希望域逻辑关心事务何时提交。我们可以用这种方式实现接口Local,除了常规的 DB 查询之外,还可以使用 DbSet 的集合。那将是:

...
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);
}
Run Code Online (Sandbox Code Playgroud)

这是有效的,但是这个通用实现将在具体的存储库中使用许多其他查询数据的方法派生。所有这些查询都必须Local在考虑集合的情况下实现,我还没有找到一种干净的方法来强制执行具体的存储库而不忽略本地更改。所以我的问题实际上归结为:

  1. 我对 Repository 模式的解释是否正确?为什么网上其他实现中没有提到这个问题?甚至微软在官方文档网站中的实现(有点过时,但思路是一样的)在查询时也忽略了本地的变化。
  2. 在 EF Core 中包含本地更改是否有比每次手动查询数据库和Local集合更好的解决方案?

更新 - 我的解决方案

我最终实施了@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...
}
Run Code Online (Sandbox Code Playgroud)

注意 的使用IAsyncQueryProvider,这迫使我使用了一个小的 Reflection hack。这是支持 EF Core 附带的异步 LINQ 方法所必需的。

afh*_*afh 5

您可以从 Microsoft 支持的EShopOnWeb项目中查看此存储库实现方法:

根据领域驱动设计的规则,存储库专用于处理聚合的集合。此示例解决方案中的界面如下所示:

public interface IAsyncRepository<T> where T : BaseEntity, IAggregateRoot
{
    Task<T> GetByIdAsync(int id, CancellationToken cancellationToken = default);
    Task<IReadOnlyList<T>> ListAllAsync(CancellationToken cancellationToken = default);
    Task<IReadOnlyList<T>> ListAsync(ISpecification<T> spec, CancellationToken cancellationToken = default);
    Task<T> AddAsync(T entity, CancellationToken cancellationToken = default);
    Task UpdateAsync(T entity, CancellationToken cancellationToken = default);
    Task DeleteAsync(T entity, CancellationToken cancellationToken = default);
    Task<int> CountAsync(ISpecification<T> spec, CancellationToken cancellationToken = default);
    Task<T> FirstAsync(ISpecification<T> spec, CancellationToken cancellationToken = default);
    Task<T> FirstOrDefaultAsync(ISpecification<T> spec, CancellationToken cancellationToken = default);
}
Run Code Online (Sandbox Code Playgroud)

接口本身位于域层(在此项目中称为应用程序核心)。

具体的实现存储库实现(此处为 EFCore)驻留在基础设施层中。

有一个通用的 EFCore 存储库实现用于涵盖常见的存储库方法:

public class EfRepository<T> : IAsyncRepository<T> where T : BaseEntity, IAggregateRoot
{
    protected readonly CatalogContext _dbContext;

    public EfRepository(CatalogContext dbContext)
    {
        _dbContext = dbContext;
    }

    public virtual async Task<T> GetByIdAsync(int id, CancellationToken cancellationToken = default)
    {
        var keyValues = new object[] { id };
        return await _dbContext.Set<T>().FindAsync(keyValues, cancellationToken);
    }

    public async Task<T> AddAsync(T entity, CancellationToken cancellationToken = default)
    {
        await _dbContext.Set<T>().AddAsync(entity);
        await _dbContext.SaveChangesAsync(cancellationToken);

        return entity;
    }

    public async Task UpdateAsync(T entity, CancellationToken cancellationToken = default)
    {
        _dbContext.Entry(entity).State = EntityState.Modified;
        await _dbContext.SaveChangesAsync(cancellationToken);
    }

    public async Task DeleteAsync(T entity, CancellationToken cancellationToken = default)
    {
        _dbContext.Set<T>().Remove(entity);
        await _dbContext.SaveChangesAsync(cancellationToken);
    }
}
Run Code Online (Sandbox Code Playgroud)

我只是在这里引用了一些方法。

对于符合要求的更具体的存储库方法,您可以在域层中实现更具体的存储库接口,这些接口再次在由通用IAsyncRepository和该特定接口派生的基础设施层中实现。请参阅此处的示例(尽管提供的方法不是最好的示例,但我认为您可以理解)。

使用这种方法,实际保存到数据库完全由存储库实现处理,而不是存储库接口的一部分。

另一方面的事务不应位于域层或存储库实现中。因此,如果您需要多个聚合更新在同一用例中保持一致,则应在应用程序层中处理此事务处理。

这也符合 Eric Evans 在他的《领域驱动设计》一书中提出的规则。

将交易控制权留给客户。尽管 REPOSITORY 会在数据库中插入和删除,但它通常不会提交任何内容。例如,保存后提交很诱人,但客户端可能具有正确启动和提交工作单元的上下文。如果 REPOSITORY 放手,事务管理将会更加简单。

请参阅第六章,存储库。


Ash*_*uru -2

您需要使用 SaveChanges() 才能获取新的 id。

工作单元.cs

private readonly DbContext dbContext;
public UnitOfWork(DbContext dbContext)
{
    this.dbContext = dbContext;
}

public void Commit()
{
    dbContext.SaveChanges();
}
Run Code Online (Sandbox Code Playgroud)

var entity = new ConcreteEntity(id: Guid.NewGuid());
repository.Add(entity);
Commit();
var result = await repository.FindByIdAsync(entity.Id);
Run Code Online (Sandbox Code Playgroud)

编辑

工作单元.cs

var users = userRepository.GetAll(); // select
var roles = roleRepository.GetAll(); // select 
var entity = new ConcreteEntity(id: Guid.NewGuid());
repository.Add(entity);

var order = new Order()
{
    InvoiceNo = "00002",
    CustomerID = 1,
    Amount = 500.00, 
    OrderDetails = new OrderDetail()
                   {
                        ItemID = 1,
                        Quantity = 5,
                        Amount = 500.00
                   }
};

orderRepository.Add(order);

// can add more insert or update or delete here before commit

Commit();

var result = await repository.FindByIdAsync(entity.Id);
var orderresult = await orderRepository.FindByIdAsync(order.Id);
Run Code Online (Sandbox Code Playgroud)