web-dev-qa-db-ja.com

RepositoryパターンをEF Coreで正しく実装する

ノート

私は尋ねていません if 私はリポジトリパターンを使うべきです、私は How 。ドメインクラスに永続性関連のオブジェクトを注入することは、単体テストを不可能にします(ノー、メモリ内データベースを使用したテストは単体テストではなく、単体テストではありません)、ドメインロジックを結合します。 ORMとITが私が困難の無知、懸念の分離など、私が練習している多くの重要な原則をブレーキします。 EFコアを「正しく」使用することは、外部の懸念から分離されたビジネスロジックを維持するのと同じくらい重要ではありません。リポジトリが漏洩しないことを意味するのであれば、「ハッキーな」使用法のために解決するのはなぜですか。もう抽象化。

元の質問

リポジトリのインタフェースが次のと仮定しましょう。

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コアの実装のほとんどは次のようになりました。

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()メソッド自体を呼び出すことを知っておく必要があります。次のスニペットが機能しません。

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にコミットできますが、トランザクションを扱う低レベルのコードとは別に、トランザクションがコミットされたときにドメインロジックが気にかけられたくない。この方法でインタフェースを実装するためにできることは、通常の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コレクションを念頭に置いて実装されなければならず、ローカルの変更を無視しないように具体的なリポジトリを強制するためのクリーンな方法を見つけていません。だから私の質問は本当に沸騰します:

  1. リポジトリパターンの解釈は正しいですか?オンラインで他の実装でこの問題について言及していないのはなぜですか? Official DocumentationのWebサイトでは、Qualy DocumentationのWebサイトで Microsoftの実装 (もちろん、アイデアは同じです)でさえ、問い合わせるとローカルの変更を無視します。
  2. 毎回DBとLocalコレクションの両方を手動でクエリするよりも、EFコアにローカル変更を含めるのに良いソリューションがありますか?

更新 - 私の解決策

私は@ Ronaldの答えによって提案された2番目の解決策を実装しました。リポジトリをデータベースへの変更を自動的に保存し、データベーストランザクションにすべての要求をラップしました。提案されたソリューションから私が変更したことの1つのことは、私が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コアに付属の非同期LINQメソッドをサポートするために必要でした。

9
Gur Galler

Microsoft Poweredからこのリポジトリ実装アプローチを調べることができます 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);
}
 _

interface それ自体はドメインレイヤにあります(このプロジェクトでは、アプリケーションコアというプロジェクト)。

具体的な実装リポジトリの実装(ここではEFCORE用)はインフラストラクチャ層にあります。

共通のリポジトリメソッドをカバーするための Generic Efcore Repository Implementation があります。

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);
    }
}
 _

ここでいくつかの方法を参照しました。

要件に合うより具体的なリポジトリメソッドについては、GenericIASyncRepositoryによって派生したインフラストラクチャレイヤで再度実装されているドメイン層内のより多くの特定のリポジトリインタフェースを実装できます。 - )そしてその特定のインタフェース。参照 ここで 例については(提供された方法は最良の例ではありませんが、アイデアを得ることができると思います)。

このアプローチでは、データベースへの実際の保存は、リポジトリインタフェースの一部ではなくリポジトリの実装によって完全に処理されます。

他方のトランザクションは、ドメイン層またはリポジトリの実装ではないはずです。したがって、同じユースケース内で一貫性を持つようにいくつかの集約アップデートが必要な場合は、このトランザクション処理をアプリケーション層で処理する必要があります。

これは彼の本からのEric Evansの規則にも適合します ドメイン駆動デザイン

トランザクション制御をクライアントに残します。リポジトリはデータベースに挿入され削除されますが、通常はコミットされません。たとえば、保存後にコミットすることは魅力的ですが、クライアントは、常に作業単位を正しく開始してコミットするというコンテキストを持っています。リポジトリが手を離した場合、トランザクション管理はより簡単になります。

第6章リポジトリを参照してください。

2
afh

ここでのリポジトリやエンティティについて誤解があるようです。まず第一に、DDDのエンティティとEntityFrameworkのエンティティはスライグリーに異なる概念です。 DDDでは、企業は基本的に、事業コンセプトインスタンスの残業の進化を追跡する方法であり、一方、エンティティフレームワークでは、企業は単なる恩恵懸念です。

リポジトリパターンは、DDDビューで、エンティティを直接操作しないが、むしろ集約します。ええ、クールな物語仲間、しかしそれは何を変えますか?ロングストーリーの短い、集計は、最終的な一貫性に反対する、厳格なドメイン不変量、トランス派の一貫性に準拠しなければならない不変性を保護するトランザクション境界として見られます。 DDDパースペクティブでは、リポジトリは集約のインスタンスをFECTHします。つまり、Aggregate Rootと呼ばれるDDDのエンティティが根付いたオブジェクトであり、オプションのエンティティとその中の値のオブジェクトです。
[。] EFでは、リポジトリが重いリフティングを行い、1つ以上のSQLテーブルからのデータを取得し、工場に依存して、完全に制限され、すぐに使用できる集合体を提供します。また、DB内の構造化されたレールファッションで集計(およびその内部コンポーネント)を保存するためにトランザクションの作業を行います。しかし、集計はリポジトリについて知らないはずです。コアモデルは永続的な詳細については気にしません。集約使用量は、ドメイン層ではなく、「アプリケーション層」または「Use Case」層に属します。

それを包み上げましょう。 ASP.NET ThinアプリでDDDリポジトリを実装したいとしましょう。

class OrderController
{
    private IOrderRepository _orderRepository;

    public OrderController(IOrderRepository orderRepository)
    {
        _orderRepository = orderRepository;
    }

    public async Task PlaceOrder(Guid orderId)
    {
        var aggregate = await _orderRepository.FindByIdAsync(orderId);
        aggregate.PlaceOrder();
        await _orderRepository.Save();
    }
}

internal interface IOrderRepository
{
    void Add(Order order);
    void Remove(Order order);
    Task<Order> FindByIdAsync(Guid id);
    Task Save();
}

internal class Order
{
    public Guid Id { get; }

    private IList<Item> items;
    public static Order CreateOrder(IList<Item> items)
    {
        return new Order(items);
    }

    private Order(IList<Item> items)
    {
        this.Id = Guid.NewGuid();
        this.items = items;
    }

    public void PlaceOrder()
    {
        // do stuff with aggregate sttus and items list
    }
}
 _

ここで何が起こりますか?コントローラは「ユースケース」レイヤーです。集計を解除する責任がある(Repoからの集約ルートを使用すると、そのジョブはそのジョブに保存してその変更を保存するようにします。その中の作業単位でより透明になる可能性があります。注入されたDBContextを保存するコントローラ(コンクリートレポは異なるDBSetにアクセスする必要があるため、注文と項目)
しかし、あなたはその考えを得ます。テーブルごとに1つのデータアクセスを保持することもできますが、それは集約専用リポジトリによって使用されます。

それが十分に鮮明だったことを願っています

1
Oinant