次の方法で共有


Entity Framework Core を使用してインフラストラクチャ永続化レイヤーを実装する

ヒント

このコンテンツは、.NET Docs で入手できる、またはオフラインで読み取ることができる無料のダウンロード可能な PDF として入手できる、コンテナー化された .NET アプリケーションの電子ブックである .NET マイクロサービス アーキテクチャからの抜粋です。

コンテナー化された .NET アプリケーションの .NET マイクロサービス アーキテクチャの電子ブックの表紙サムネイル。

SQL Server、Oracle、PostgreSQL などのリレーショナル データベースを使用する場合は、Entity Framework (EF) に基づいて永続化レイヤーを実装することをお勧めします。 EF は LINQ をサポートし、モデルに対して厳密に型指定されたオブジェクトと、データベースへの簡略化された永続化を提供します。

Entity Framework には、.NET Framework の一部として長い歴史があります。 .NET を使用する場合は、.NET と同じ方法で Windows または Linux 上で実行される Entity Framework Core も使用する必要があります。 EF Core は Entity Framework の完全な書き換えであり、実装されるフットプリントが大幅に小さくなり、パフォーマンスが重要に向上します。

Entity Framework Core の概要

Entity Framework (EF) Core は、一般的な Entity Framework データ アクセス テクノロジの軽量で拡張可能なクロスプラットフォーム バージョンです。 2016 年半ばに .NET Core で導入されました。

EF Core の概要は Microsoft のドキュメントで既に提供されているため、ここではその情報へのリンクを提供するだけです。

その他のリソース

DDD の観点から見た Entity Framework Core のインフラストラクチャ

DDD の観点からは、EF の重要な機能は、POCO コード優先エンティティとも呼ばれる EF 用語で POCO ドメイン エンティティを使用する機能です。 POCO ドメイン エンティティを使用する場合、永続化の無視とインフラストラクチャの無視の原則に従って、ドメイン モデル クラスは 永続化無視 します。

DDD パターンごとに、ドメインの動作とルールをエンティティ クラス自体にカプセル化して、コレクションにアクセスするときにインバリアント、検証、ルールを制御できるようにする必要があります。 したがって、DDD では、子エンティティまたは値オブジェクトのコレクションへのパブリック アクセスを許可することはお勧めしません。 代わりに、フィールドとプロパティ コレクションを更新する方法とタイミング、およびその場合に発生する動作とアクションを制御するメソッドを公開する必要があります。

EF Core 1.1 以降では、これらの DDD 要件を満たすために、パブリック プロパティではなくエンティティにプレーン フィールドを含めることができます。 エンティティ フィールドに外部からアクセスできるようにする必要がない場合は、プロパティではなく属性またはフィールドを作成するだけです。 プライベート プロパティ セッターを使用することもできます。

同様に、永続化のために EF に依存するエンティティ内のコレクションのプライベート フィールド メンバー (IReadOnlyCollection<T> など) によってサポートされる、List<T>として型指定されたパブリック プロパティを使用して、コレクションへの読み取り専用アクセスを持つようになりました。 以前のバージョンの Entity Framework では、 ICollection<T>をサポートするためにコレクション プロパティが必要でした。つまり、親エンティティ クラスを使用する開発者は、プロパティ コレクションを使用して項目を追加または削除できました。 その可能性は、DDD が推奨するパターンに反するものです。

次のコード例に示すように、読み取り専用 IReadOnlyCollection<T> オブジェクトを公開するときにプライベート コレクションを使用できます。

public class Order : Entity
{
    // Using private fields, allowed since EF Core 1.1
    private DateTime _orderDate;
    // Other fields ...

    private readonly List<OrderItem> _orderItems;
    public IReadOnlyCollection<OrderItem> OrderItems => _orderItems;

    protected Order() { }

    public Order(int buyerId, int paymentMethodId, Address address)
    {
        // Initializations ...
    }

    public void AddOrderItem(int productId, string productName,
                             decimal unitPrice, decimal discount,
                             string pictureUrl, int units = 1)
    {
        // Validation logic...

        var orderItem = new OrderItem(productId, productName,
                                      unitPrice, discount,
                                      pictureUrl, units);
        _orderItems.Add(orderItem);
    }
}

OrderItems プロパティには、IReadOnlyCollection<OrderItem>を使用して読み取り専用としてのみアクセスできます。 この型は読み取り専用であるため、通常の外部更新プログラムから保護されます。

EF Core では、ドメイン モデルを "汚染" することなく、ドメイン モデルを物理データベースにマップする方法が提供されます。 マッピング アクションは永続化レイヤーに実装されているため、純粋な .NET POCO コードです。 そのマッピング アクションでは、フィールドからデータベースへのマッピングを構成する必要があります。 OnModelCreating および OrderingContext クラスからのOrderEntityTypeConfiguration メソッドの次の例では、SetPropertyAccessModeの呼び出しによって、フィールドを介してOrderItems プロパティにアクセスするように EF Core に指示します。

// At OrderingContext.cs from eShopOnContainers
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
   // ...
   modelBuilder.ApplyConfiguration(new OrderEntityTypeConfiguration());
   // Other entities' configuration ...
}

// At OrderEntityTypeConfiguration.cs from eShopOnContainers
class OrderEntityTypeConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> orderConfiguration)
    {
        orderConfiguration.ToTable("orders", OrderingContext.DEFAULT_SCHEMA);
        // Other configuration

        var navigation =
              orderConfiguration.Metadata.FindNavigation(nameof(Order.OrderItems));

        //EF access the OrderItem collection property through its backing field
        navigation.SetPropertyAccessMode(PropertyAccessMode.Field);

        // Other configuration
    }
}

プロパティの代わりにフィールドを使用すると、 OrderItem エンティティは、 List<OrderItem> プロパティがあるかのように永続化されます。 ただし、注文に新しい項目を追加するための 1 つのアクセサー ( AddOrderItem メソッド) が公開されます。 その結果、動作とデータは関連付けられ、ドメイン モデルを使用するアプリケーション コード全体で一貫性が保たれます。

Entity Framework Core を使用してカスタム リポジトリを実装する

実装レベルでは、リポジトリは、次のクラスに示すように、更新を実行するときに作業単位 (EF Core の DBContext) によって調整されたデータ永続化コードを持つクラスです。

// using directives...
namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Repositories
{
    public class BuyerRepository : IBuyerRepository
    {
        private readonly OrderingContext _context;
        public IUnitOfWork UnitOfWork
        {
            get
            {
                return _context;
            }
        }

        public BuyerRepository(OrderingContext context)
        {
            _context = context ?? throw new ArgumentNullException(nameof(context));
        }

        public Buyer Add(Buyer buyer)
        {
            return _context.Buyers.Add(buyer).Entity;
        }

        public async Task<Buyer> FindAsync(string buyerIdentityGuid)
        {
            var buyer = await _context.Buyers
                .Include(b => b.Payments)
                .Where(b => b.FullName == buyerIdentityGuid)
                .SingleOrDefaultAsync();

            return buyer;
        }
    }
}

IBuyerRepository インターフェイスは、コントラクトとしてドメイン モデル レイヤーから取得されます。 ただし、リポジトリの実装は永続化とインフラストラクチャレイヤーで行われます。

EF DbContext は、依存関係の挿入を通じてコンストラクターを経由します。 IoC コンテナー内の既定の有効期間 (ServiceLifetime.Scoped) により、同じ HTTP 要求スコープ内の複数のリポジトリ間で共有されます ( services.AddDbContext<>で明示的に設定することもできます)。

リポジトリに実装するメソッド (更新またはトランザクションとクエリ)

各リポジトリ クラス内に、関連する集計に含まれるエンティティの状態を更新する永続化メソッドを配置する必要があります。 集計とその関連リポジトリの間に 1 対 1 のリレーションシップがあることに注意してください。 集約ルート エンティティ オブジェクトに EF グラフ内に子エンティティが埋め込まれている可能性があるとします。 たとえば、購入者が関連する子エンティティとして複数の支払い方法を持っている場合があります。

eShopOnContainers での注文マイクロサービスのアプローチも CQS/CQRS に基づいているため、ほとんどのクエリはカスタム リポジトリに実装されません。 開発者は、集計、集計ごとのカスタム リポジトリ、DDD 全般によって課される制限なしに、プレゼンテーション レイヤーに必要なクエリと結合を自由に作成できます。 このガイドで提案されているほとんどのカスタム リポジトリには、いくつかの更新またはトランザクション メソッドがありますが、更新するデータを取得するために必要なクエリ メソッドだけです。 たとえば、BuyerRepository リポジトリは FindAsync メソッドを実装します。これは、注文に関連する新しい購入者を作成する前に、特定の購入者が存在するかどうかをアプリケーションが認識する必要があるためです。

ただし、前述のように、Dapper を使用した柔軟なクエリに基づく CQRS クエリでは、プレゼンテーション 層またはクライアント アプリに送信するデータを取得する実際のクエリ メソッドが実装されます。

カスタム リポジトリの使用と EF DbContext の直接使用

Entity Framework DbContext クラスは作業単位とリポジトリ のパターンに基づいており、ASP.NET Core MVC コントローラーなど、コードから直接使用できます。 作業単位とリポジトリのパターンは、eShopOnContainers の CRUD カタログ マイクロサービスのように、最も単純なコードになります。 可能な限り最も単純なコードが必要な場合は、多くの開発者が行うのと同じように、DbContext クラスを直接使用できます。

ただし、カスタム リポジトリを実装すると、より複雑なマイクロサービスまたはアプリケーションを実装する場合にいくつかの利点があります。 Unit of Work および Repository パターンは、インフラストラクチャの永続化レイヤーをカプセル化して、アプリケーションレイヤーとドメイン モデル レイヤーから切り離すことを目的としています。 これらのパターンを実装すると、データベースへのアクセスをシミュレートするモック リポジトリの使用を容易にすることができます。

図 7-18 では、リポジトリを使用しない (EF DbContext を直接使用する) とリポジトリの使用の違いを確認できます。これにより、これらのリポジトリを簡単にモックできます。

2 つのリポジトリのコンポーネントとデータフローを示す図。

図 7-18 カスタム リポジトリとプレーンな DbContext の使用

図 7-18 は、カスタム リポジトリを使用すると、リポジトリをモックしてテストを容易にするために使用できる抽象化レイヤーを追加することを示しています。 モックにはさまざまな代替手法があります。 リポジトリだけをモックすることも、作業単位全体をモックすることもできます。 通常、リポジトリだけをモックするだけで十分です。通常、作業単位全体を抽象化してモック化する複雑さは必要ありません。

後でアプリケーション レイヤーに焦点を当てると、ASP.NET Core での依存関係の挿入のしくみと、リポジトリを使用するときの実装方法が表示されます。

つまり、カスタム リポジトリを使用すると、データ層の状態の影響を受けにくい単体テストを使用して、コードをより簡単にテストできます。 Entity Framework を介して実際のデータベースにもアクセスするテストを実行すると、単体テストではなく統合テストになります。これは大幅に遅くなります。

DbContext を直接使用していた場合は、単体テスト用の予測可能なデータを含むインメモリ SQL Server を使用して、それをモックしたり、単体テストを実行したりする必要があります。 しかし、DbContext をモックしたり、偽のデータを制御したりするには、リポジトリ レベルでモックするよりも多くの作業が必要です。 もちろん、MVC コントローラーはいつでもテストできます。

IoC コンテナー内の EF DbContext と IUnitOfWork インスタンスの有効期間

DbContext オブジェクト (IUnitOfWork オブジェクトとして公開) は、同じ HTTP 要求スコープ内の複数のリポジトリ間で共有する必要があります。 たとえば、これは、実行中の操作が複数の集計を処理する必要がある場合、または単に複数のリポジトリ インスタンスを使用しているために当てはまります。 また、 IUnitOfWork インターフェイスは EF Core の種類ではなく、ドメイン レイヤーの一部であることを言及することも重要です。

そのためには、 DbContext オブジェクトのインスタンスのサービス有効期間を ServiceLifetime.Scoped に設定する必要があります。 これは、ASP.NET Core Web API プロジェクトのProgram.cs ファイルから IoC コンテナー内のDbContextbuilder.Services.AddDbContextを登録するときの既定の有効期間です。 次のコードは、これを示しています。

// Add framework services.
builder.Services.AddMvc(options =>
{
    options.Filters.Add(typeof(HttpGlobalExceptionFilter));
}).AddControllersAsServices();

builder.Services.AddEntityFrameworkSqlServer()
    .AddDbContext<OrderingContext>(options =>
    {
        options.UseSqlServer(Configuration["ConnectionString"],
                            sqlOptions => sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().
                                                                                Assembly.GetName().Name));
    },
    ServiceLifetime.Scoped // Note that Scoped is the default choice
                            // in AddDbContext. It is shown here only for
                            // pedagogic purposes.
    );

DbContext インスタンス化モードは、ServiceLifetime.Transient または ServiceLifetime.Singleton として構成しないでください。

IoC コンテナー内のリポジトリ インスタンスの有効期間

同様に、リポジトリの有効期間は通常、スコープとして設定する必要があります (Autofac の InstancePerLifetimeScope)。 一時的な (Autofac の InstancePerDependency) 場合もありますが、スコープ付き有効期間を使用する場合、サービスはメモリに関してより効率的になります。

// Registering a Repository in Autofac IoC container
builder.RegisterType<OrderRepository>()
    .As<IOrderRepository>()
    .InstancePerLifetimeScope();

リポジトリにシングルトンの有効期間を使用すると、DbContext がスコープ付き (InstancePerLifetimeScope) の有効期間 (DBContext の既定の有効期間) に設定されていると、コンカレンシーの重大な問題が発生する可能性があります。 リポジトリと DbContext の両方のサービス有効期間がスコープ設定されている限り、これらの問題を回避できます。

その他のリソース

テーブル マッピング

テーブル マッピングは、クエリを実行してデータベースに保存するテーブル データを識別します。 以前は、ドメイン エンティティ (製品ドメインや注文ドメインなど) を使用して、関連するデータベース スキーマを生成する方法を確認しました。 EF は、規則の概念を中心に強く設計 されています。 規則では、"テーブルの名前は何になりますか" や "主キーのプロパティは何ですか?" などの質問に対処します。通常、規則は従来の名前に基づいています。 たとえば、主キーが Id で終わるプロパティであることが一般的です。

慣例により、各エンティティは、派生コンテキストでエンティティを公開する DbSet<TEntity> プロパティと同じ名前のテーブルにマップするように設定されます。 指定されたエンティティに DbSet<TEntity> 値が指定されていない場合は、クラス名が使用されます。

データ注釈とFluent API

他にも多くの EF Core 規則があります。そのほとんどは、OnModelCreating メソッド内に実装されたデータ注釈または Fluent API を使用して変更できます。

データ注釈は、エンティティ モデル クラス自体で使用する必要があります。これは、DDD の観点から見た場合、より侵入的な方法です。 これは、インフラストラクチャ データベースに関連するデータ注釈を使用してモデルを汚染しているためです。 一方、Fluent API は、データ永続化インフラストラクチャ レイヤー内のほとんどの規則とマッピングを変更する便利な方法であるため、エンティティ モデルはクリーンになり、永続化インフラストラクチャから切り離されます。

Fluent API と OnModelCreating メソッド

前述のように、規則とマッピングを変更するには、DbContext クラスで OnModelCreating メソッドを使用できます。

eShopOnContainers の注文マイクロサービスでは、次のコードに示すように、必要に応じて明示的なマッピングと構成が実装されます。

// At OrderingContext.cs from eShopOnContainers
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
   // ...
   modelBuilder.ApplyConfiguration(new OrderEntityTypeConfiguration());
   // Other entities' configuration ...
}

// At OrderEntityTypeConfiguration.cs from eShopOnContainers
class OrderEntityTypeConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> orderConfiguration)
    {
        orderConfiguration.ToTable("orders", OrderingContext.DEFAULT_SCHEMA);

        orderConfiguration.HasKey(o => o.Id);

        orderConfiguration.Ignore(b => b.DomainEvents);

        orderConfiguration.Property(o => o.Id)
            .UseHiLo("orderseq", OrderingContext.DEFAULT_SCHEMA);

        //Address value object persisted as owned entity type supported since EF Core 2.0
        orderConfiguration
            .OwnsOne(o => o.Address, a =>
            {
                a.WithOwner();
            });

        orderConfiguration
            .Property<int?>("_buyerId")
            .UsePropertyAccessMode(PropertyAccessMode.Field)
            .HasColumnName("BuyerId")
            .IsRequired(false);

        orderConfiguration
            .Property<DateTime>("_orderDate")
            .UsePropertyAccessMode(PropertyAccessMode.Field)
            .HasColumnName("OrderDate")
            .IsRequired();

        orderConfiguration
            .Property<int>("_orderStatusId")
            .UsePropertyAccessMode(PropertyAccessMode.Field)
            .HasColumnName("OrderStatusId")
            .IsRequired();

        orderConfiguration
            .Property<int?>("_paymentMethodId")
            .UsePropertyAccessMode(PropertyAccessMode.Field)
            .HasColumnName("PaymentMethodId")
            .IsRequired(false);

        orderConfiguration.Property<string>("Description").IsRequired(false);

        var navigation = orderConfiguration.Metadata.FindNavigation(nameof(Order.OrderItems));

        // DDD Patterns comment:
        //Set as field (New since EF 1.1) to access the OrderItem collection property through its field
        navigation.SetPropertyAccessMode(PropertyAccessMode.Field);

        orderConfiguration.HasOne<PaymentMethod>()
            .WithMany()
            .HasForeignKey("_paymentMethodId")
            .IsRequired(false)
            .OnDelete(DeleteBehavior.Restrict);

        orderConfiguration.HasOne<Buyer>()
            .WithMany()
            .IsRequired(false)
            .HasForeignKey("_buyerId");

        orderConfiguration.HasOne(o => o.OrderStatus)
            .WithMany()
            .HasForeignKey("_orderStatusId");
    }
}

同じ OnModelCreating メソッド内ですべての Fluent API マッピングを設定できますが、この例に示すように、そのコードをパーティション分割し、エンティティごとに 1 つずつ、複数の構成クラスを使用することをお勧めします。 特に大規模なモデルの場合は、異なるエンティティ型を構成するための個別の構成クラスを用意することをお勧めします。

この例のコードは、いくつかの明示的な宣言とマッピングを示しています。 ただし、EF Core 規則ではこれらのマッピングの多くが自動的に実行されるため、実際に必要なコードは小さくなる可能性があります。

EF Core の Hi/Lo アルゴリズム

前の例のコードの興味深い点は、 Hi/Lo アルゴリズム をキー生成戦略として使用することです。

Hi/Lo アルゴリズムは、変更をコミットする前に一意のキーが必要な場合に便利です。 まとめると、Hi-Lo アルゴリズムでは、テーブル行に一意の識別子が割り当てられますが、データベースに行をすぐに格納することに依存しません。 これにより、通常のシーケンシャル データベース ID と同様に、識別子の使用をすぐに開始できます。

Hi/Lo アルゴリズムは、関連するデータベース シーケンスから一意の ID のバッチを取得するためのメカニズムを記述します。 データベースが一意性を保証するため、これらの ID は安全に使用できるため、ユーザー間の競合は発生しません。 このアルゴリズムは、次の理由から興味深いものです。

  • Unit of Work パターンは壊されていません。

  • データベースへのラウンド トリップを最小限に抑えるために、シーケンス ID をバッチで取得します。

  • GUID を使用する手法とは異なり、人間が判読できる識別子が生成されます。

EF Core では、前の例に示すように、 メソッドで UseHiLo がサポートされています。

プロパティの代わりにフィールドをマップする

EF Core 1.1 以降で使用できるこの機能を使用すると、列をフィールドに直接マップできます。 エンティティ クラスではプロパティを使用せず、テーブルからフィールドに列をマップするためだけに使用できます。 その一般的な用途は、エンティティの外部からアクセスする必要のない内部状態のプライベート フィールドです。

これを行うには、単一のフィールドを使用することも、 List<> フィールドなどのコレクションを使用することもできます。 この点は、ドメイン モデル クラスのモデル化について説明したときに既に説明しましたが、ここでは、前のコードで強調表示された PropertyAccessMode.Field 構成を使用してそのマッピングがどのように実行されるかを確認できます。

インフラストラクチャ レベルで非表示になっている EF Core でシャドウ プロパティを使用する

EF Core のシャドウ プロパティは、エンティティ クラス モデルに存在しないプロパティです。 これらのプロパティの値と状態は、インフラストラクチャ レベルの ChangeTracker クラスに純粋に保持されます。

クエリ仕様パターンを実装する

デザイン セクションで前に説明したように、クエリ仕様パターンは、オプションの並べ替えとページング ロジックを使用してクエリの定義を配置できる場所として設計された Domain-Driven デザイン パターンです。

クエリ仕様パターンは、オブジェクト内のクエリを定義します。 たとえば、一部の製品を検索するページ クエリをカプセル化するために、必要な入力パラメーター (pageNumber、pageSize、filter など) を受け取る PagedProduct 仕様を作成できます。 次に、任意の Repository メソッド (通常は List() オーバーロード) 内で、IQuerySpecification を受け入れ、その仕様に基づいて予想されるクエリを実行します。

ジェネリックな Specification インターフェイスの例として、 eShopOnWeb 参照アプリケーションで使用されるコードに似た次のコードがあります。

// GENERIC SPECIFICATION INTERFACE
// https://github.com/dotnet-architecture/eShopOnWeb

public interface ISpecification<T>
{
    Expression<Func<T, bool>> Criteria { get; }
    List<Expression<Func<T, object>>> Includes { get; }
    List<string> IncludeStrings { get; }
}

次に、ジェネリック仕様の基底クラスの実装を次に示します。

// GENERIC SPECIFICATION IMPLEMENTATION (BASE CLASS)
// https://github.com/dotnet-architecture/eShopOnWeb

public abstract class BaseSpecification<T> : ISpecification<T>
{
    public BaseSpecification(Expression<Func<T, bool>> criteria)
    {
        Criteria = criteria;
    }
    public Expression<Func<T, bool>> Criteria { get; }

    public List<Expression<Func<T, object>>> Includes { get; } =
                                           new List<Expression<Func<T, object>>>();

    public List<string> IncludeStrings { get; } = new List<string>();

    protected virtual void AddInclude(Expression<Func<T, object>> includeExpression)
    {
        Includes.Add(includeExpression);
    }

    // string-based includes allow for including children of children
    // for example, Basket.Items.Product
    protected virtual void AddInclude(string includeString)
    {
        IncludeStrings.Add(includeString);
    }
}

次の仕様では、バスケットの ID またはバスケットが属する購入者の ID を指定して、1 つのバスケット エンティティを読み込みます。 買い物かごの コレクションがItems

// SAMPLE QUERY SPECIFICATION IMPLEMENTATION

public class BasketWithItemsSpecification : BaseSpecification<Basket>
{
    public BasketWithItemsSpecification(int basketId)
        : base(b => b.Id == basketId)
    {
        AddInclude(b => b.Items);
    }

    public BasketWithItemsSpecification(string buyerId)
        : base(b => b.BuyerId == buyerId)
    {
        AddInclude(b => b.Items);
    }
}

最後に、一般的な EF リポジトリでこのような仕様を使用して、特定のエンティティ型 T に関連するデータをフィルター処理して一括読み込みする方法を次に示します。

// GENERIC EF REPOSITORY WITH SPECIFICATION
// https://github.com/dotnet-architecture/eShopOnWeb

public IEnumerable<T> List(ISpecification<T> spec)
{
    // fetch a Queryable that includes all expression-based includes
    var queryableResultWithIncludes = spec.Includes
        .Aggregate(_dbContext.Set<T>().AsQueryable(),
            (current, include) => current.Include(include));

    // modify the IQueryable to include any string-based include statements
    var secondaryResult = spec.IncludeStrings
        .Aggregate(queryableResultWithIncludes,
            (current, include) => current.Include(include));

    // return the result of the query using the specification's criteria expression
    return secondaryResult
                    .Where(spec.Criteria)
                    .AsEnumerable();
}

フィルター処理ロジックをカプセル化するだけでなく、どのプロパティを設定するかなど、返されるデータの形状を指定できます。

リポジトリから IQueryable を返すのはお勧めしませんが、リポジトリ内でそれらを使用して結果のセットを構築することは完全に問題ありません。 この方法は、上記の List メソッドで使用されています。この方法では、中間 IQueryable 式を使用して、最後の行で仕様の条件を使用してクエリを実行する前に、クエリのインクルードの一覧を作成します。

eShopOnWeb サンプルで仕様パターンを適用する方法について説明します。

その他のリソース