Menerapkan lapisan persistensi infrastruktur dengan Entity Framework Core

Tip

Konten ini adalah kutipan dari eBook, .NET Microservices Architecture for Containerized .NET Applications, tersedia di .NET Docs atau sebagai PDF yang dapat diunduh gratis dan dapat dibaca secara offline.

.NET Microservices Architecture for Containerized .NET Applications eBook cover thumbnail.

Saat Anda menggunakan database relasional seperti SQL Server, Oracle, atau PostgreSQL, pendekatan yang disarankan adalah menerapkan lapisan persistensi berdasarkan Entity Framework (EF). EF mendukung LINQ dan menyediakan objek berjenis kuat untuk model Anda, serta persistensi yang disederhanakan ke dalam database Anda.

Entity Framework memiliki riwayat panjang sebagai bagian dari .NET Framework. Saat Anda menggunakan .NET, Anda juga harus menggunakan Entity Framework Core, yang berjalan di Windows atau Linux dengan cara yang sama seperti .NET. EF Core adalah penulisan ulang lengkap Entity Framework yang diimplementasikan dengan jejak yang jauh lebih kecil dan peningkatan performa yang penting.

Pengantar Inti Kerangka Kerja Entitas

Entity Framework (EF) Core adalah versi ringan, dapat diperluas, dan lintas platform dari teknologi akses data Entity Framework yang populer. Ini diperkenalkan dengan .NET Core pada pertengahan 2016.

Karena pengenalan EF Core sudah tersedia dalam dokumentasi Microsoft, di sini kami hanya menyediakan tautan ke informasi tersebut.

Sumber daya tambahan

Infrastruktur dalam Entity Framework Core dari perspektif DDD

Dari sudut pandang DDD, kemampuan penting EF adalah kemampuan untuk menggunakan entitas domain POCO, juga dikenal dalam terminologi EF sebagai entitas kode-pertama POCO. Jika Anda menggunakan entitas domain POCO, kelas model domain Anda tidak tahu persistensi, mengikuti prinsip Ketidaktahuan Persistensi dan Infrastruktur.

Per pola DDD, Anda harus merangkum perilaku dan aturan domain dalam kelas entitas itu sendiri, sehingga dapat mengontrol invarian, validasi, dan aturan saat mengakses koleksi apa pun. Oleh karena itu, ini bukan praktik yang baik di DDD untuk memungkinkan akses publik ke kumpulan entitas anak atau objek nilai. Sebagai gantinya, Anda ingin mengekspos metode yang mengontrol bagaimana dan kapan bidang dan koleksi properti Anda dapat diperbarui, dan perilaku dan tindakan apa yang harus terjadi ketika itu terjadi.

Karena EF Core 1.1, untuk memenuhi persyaratan DDD tersebut, Anda dapat memiliki bidang biasa di entitas Anda alih-alih properti publik. Jika Anda tidak ingin bidang entitas dapat diakses secara eksternal, Anda bisa membuat atribut atau bidang alih-alih properti. Anda juga dapat menggunakan pengatur properti privat.

Dengan cara yang sama, Anda sekarang dapat memiliki akses baca-saja ke koleksi dengan menggunakan properti publik dengan jenis sebagai IReadOnlyCollection<T>, yang didukung oleh anggota bidang privat untuk koleksi (seperti List<T>) di entitas Anda yang bergantung pada EF untuk persistensi. Versi sebelumnya dari properti kumpulan Entity Framework yang diperlukan untuk mendukung ICollection<T>, yang berarti bahwa setiap pengembang yang menggunakan kelas entitas induk dapat menambahkan atau menghapus item melalui koleksi propertinya. Kemungkinan itu akan bertentangan dengan pola yang direkomendasikan dalam DDD.

Anda dapat menggunakan koleksi privat saat mengekspos objek baca-saja IReadOnlyCollection<T>, seperti yang ditunjukkan dalam contoh kode berikut:

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

Properti OrderItems hanya dapat diakses sebagai baca-saja menggunakan IReadOnlyCollection<OrderItem>. Jenis ini bersifat baca-saja sehingga terlindungi dari pembaruan eksternal reguler.

EF Core menyediakan cara untuk memetakan model domain ke database fisik tanpa "mencemari" model domain. Ini adalah kode POCO .NET murni, karena tindakan pemetaan diimplementasikan di lapisan persistensi. Dalam tindakan pemetaan tersebut, Anda perlu mengonfigurasi pemetaan bidang-ke-database. Dalam contoh OnModelCreating metode berikut dari OrderingContext dan OrderEntityTypeConfiguration kelas, panggilan untuk SetPropertyAccessMode memberi tahu EF Core untuk mengakses OrderItems properti melalui bidangnya.

// 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
    }
}

Saat Anda menggunakan bidang alih-alih properti, OrderItem entitas dipertahankan seolah-olah List<OrderItem> memiliki properti. Namun, ini mengekspos pengakses tunggal, AddOrderItem metode, untuk menambahkan item baru ke pesanan. Akibatnya, perilaku dan data diikat bersama-sama dan akan konsisten di seluruh kode aplikasi apa pun yang menggunakan model domain.

Menerapkan repositori kustom dengan Entity Framework Core

Pada tingkat implementasi, repositori hanyalah kelas dengan kode persistensi data yang dikoordinasikan oleh unit kerja (DBContext di EF Core) saat melakukan pembaruan, seperti yang ditunjukkan di kelas berikut:

// 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;
        }
    }
}

Antarmuka IBuyerRepository berasal dari lapisan model domain sebagai kontrak. Namun, implementasi repositori dilakukan pada lapisan persistensi dan infrastruktur.

EF DbContext hadir melalui konstruktor melalui Injeksi Dependensi. Ini dibagikan antara beberapa repositori dalam cakupan permintaan HTTP yang sama, berkat masa pakai defaultnya (ServiceLifetime.Scoped) dalam kontainer IoC (yang juga dapat diatur secara eksplisit dengan services.AddDbContext<>).

Metode untuk diterapkan dalam repositori (pembaruan atau transaksi versus kueri)

Dalam setiap kelas repositori, Anda harus menempatkan metode persistensi yang memperbarui status entitas yang terkandung oleh agregat terkait. Ingat ada hubungan satu-ke-satu antara agregat dan repositori terkait. Pertimbangkan bahwa objek entitas akar agregat mungkin memiliki entitas anak yang disematkan dalam grafik EF-nya. Misalnya, pembeli mungkin memiliki beberapa metode pembayaran sebagai entitas anak terkait.

Karena pendekatan untuk pemesanan layanan mikro di eShopOnContainers juga didasarkan pada CQS/CQRS, sebagian besar kueri tidak diimplementasikan dalam repositori kustom. Pengembang memiliki kebebasan untuk membuat kueri dan bergabung yang mereka butuhkan untuk lapisan presentasi tanpa batasan yang diberlakukan oleh agregat, repositori kustom per agregat, dan DDD secara umum. Sebagian besar repositori kustom yang disarankan oleh panduan ini memiliki beberapa metode pembaruan atau transaksi tetapi hanya metode kueri yang diperlukan untuk mendapatkan data yang akan diperbarui. Misalnya, repositori BuyerRepository menerapkan metode FindAsync, karena aplikasi perlu mengetahui apakah pembeli tertentu ada sebelum membuat pembeli baru yang terkait dengan pesanan.

Namun, metode kueri nyata untuk mendapatkan data untuk dikirim ke lapisan presentasi atau aplikasi klien diterapkan, seperti yang disebutkan, dalam kueri CQRS berdasarkan kueri fleksibel menggunakan Dapper.

Menggunakan repositori kustom versus menggunakan EF DbContext secara langsung

Kelas Entity Framework DbContext didasarkan pada pola Unit Kerja dan Repositori dan dapat digunakan langsung dari kode Anda, seperti dari pengontrol MVC Core ASP.NET. Pola Unit Kerja dan Repositori menghasilkan kode yang paling sederhana, seperti dalam layanan mikro katalog CRUD di eShopOnContainers. Dalam kasus ketika Anda menginginkan kode paling sederhana, Anda mungkin ingin langsung menggunakan kelas DbContext, seperti yang dilakukan banyak pengembang.

Namun, menerapkan repositori kustom memberikan beberapa manfaat saat menerapkan layanan mikro atau aplikasi yang lebih kompleks. Pola Unit Kerja dan Repositori dimaksudkan untuk merangkum lapisan persistensi infrastruktur sehingga dipisahkan dari lapisan aplikasi dan model domain. Menerapkan pola-pola ini dapat memfasilitasi penggunaan repositori tiruan yang mensimulasikan akses ke database.

Pada Gambar 7-18, Anda dapat melihat perbedaan antara tidak menggunakan repositori (langsung menggunakan EF DbContext) versus menggunakan repositori, yang membuatnya lebih mudah untuk menipu repositori tersebut.

Diagram showing the components and dataflow in the two repositories.

Gambar 7-18. Menggunakan repositori kustom versus DbContext biasa

Gambar 7-18 menunjukkan bahwa menggunakan repositori kustom menambahkan lapisan abstraksi yang dapat digunakan untuk memudahkan pengujian dengan menipu repositori. Ada beberapa alternatif saat meniru. Anda bisa meniru hanya repositori atau Anda bisa meniru seluruh unit kerja. Biasanya meniru hanya repositori saja sudah cukup, dan kompleksitas untuk mengabstraksi dan meniru seluruh unit kerja biasanya tidak diperlukan.

Nantinya, ketika kita fokus pada lapisan aplikasi, Anda akan melihat cara kerja Injeksi Dependensi di ASP.NET Core dan bagaimana penerapannya saat menggunakan repositori.

Singkatnya, repositori kustom memungkinkan Anda untuk menguji kode dengan lebih mudah dengan pengujian unit yang tidak terpengaruh oleh status tingkat data. Jika Anda menjalankan pengujian yang juga mengakses database aktual melalui Kerangka Kerja Entitas, pengujian tersebut bukan pengujian unit tetapi pengujian integrasi, yang jauh lebih lambat.

Jika Anda menggunakan DbContext secara langsung, Anda harus menirunya atau menjalankan pengujian unit dengan menggunakan SQL Server dalam memori dengan data yang dapat diprediksi untuk pengujian unit. Tetapi menipu DbContext atau mengontrol data palsu membutuhkan lebih banyak pekerjaan daripada menipu di tingkat repositori. Tentu saja, Anda selalu dapat menguji pengontrol MVC.

Masa pakai instans EF DbContext dan IUnitOfWork di kontainer IoC Anda

Objek DbContext (diekspos sebagai IUnitOfWork objek) harus dibagikan di antara beberapa repositori dalam cakupan permintaan HTTP yang sama. Misalnya, ini benar ketika operasi yang dijalankan harus berurusan dengan beberapa agregat, atau hanya karena Anda menggunakan beberapa instans repositori. Penting juga untuk disebutkan bahwa IUnitOfWork antarmuka adalah bagian dari lapisan domain Anda, bukan jenis EF Core.

Untuk melakukannya, instans DbContext objek harus mengatur masa pakai layanannya ke ServiceLifetime.Scoped. Ini adalah masa pakai default saat mendaftarkan DbContext dengan builder.Services.AddDbContext di kontainer IoC Anda dari file Program.cs di proyek ASP.NET Core Web API Anda. Kode berikut menggambarkan hal ini.

// 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.
    );

Mode instansiasi DbContext tidak boleh dikonfigurasi sebagai ServiceLifetime.Transient atau ServiceLifetime.Singleton.

Masa pakai instans repositori di kontainer IoC Anda

Dengan cara yang sama, masa pakai repositori biasanya harus ditetapkan sebagai cakupan (InstancePerLifetimeScope di Autofac). Ini juga bisa bersifat sementara (InstancePerDependency di Autofac), tetapi layanan Anda akan lebih efisien sehubungan dengan memori saat menggunakan masa pakai cakupan.

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

Menggunakan masa pakai singleton untuk repositori dapat menyebabkan Masalah konkurensi serius saat DbContext Anda diatur ke cakupan masa pakai (InstancePerLifetimeScope) (masa pakai default untuk DBContext). Selama masa pakai layanan Anda untuk repositori dan DbContext Anda tercakup, Anda akan menghindari masalah ini.

Sumber daya tambahan

Pemetaan tabel

Pemetaan tabel mengidentifikasi data tabel yang akan dikueri dan disimpan ke database. Sebelumnya Anda melihat bagaimana entitas domain (misalnya, domain produk atau pesanan) dapat digunakan untuk menghasilkan skema database terkait. EF dirancang dengan kuat sekeliling konsep konvensi. Konvensi menjawab pertanyaan seperti "Apa nama tabel?" atau "Properti apa yang merupakan kunci primer?" Konvensi biasanya didasarkan pada nama konvensional. Misalnya, biasanya kunci primer menjadi properti yang berakhiran dengan Id.

Menurut konvensi, setiap entitas akan disiapkan untuk memetakan ke tabel dengan nama DbSet<TEntity> yang sama dengan properti yang mengekspos entitas pada konteks turunan. Jika tidak ada DbSet<TEntity> nilai yang disediakan untuk entitas yang diberikan, nama kelas akan digunakan.

Anotasi Data versus API Fluent

Ada banyak konvensi EF Core tambahan, dan sebagian besar dapat diubah dengan menggunakan anotasi data atau API Fluent, yang diimplementasikan dalam metode OnModelCreating.

Anotasi data harus digunakan pada kelas model entitas itu sendiri, yang merupakan cara yang lebih mengganggu dari sudut pandang DDD. Ini karena Anda mencemari model Anda dengan anotasi data yang terkait dengan database infrastruktur. Di sisi lain, Fluent API adalah cara mudah untuk mengubah sebagian besar konvensi dan pemetaan dalam lapisan infrastruktur persistensi data Anda, sehingga model entitas akan bersih dan dipisahkan dari infrastruktur persistensi.

Fluent API dan metode OnModelCreating

Seperti disebutkan, untuk mengubah konvensi dan pemetaan, Anda dapat menggunakan metode OnModelCreating di kelas DbContext.

Layanan mikro pemesanan di eShopOnContainers mengimplementasikan pemetaan dan konfigurasi eksplisit, ketika diperlukan, seperti yang ditunjukkan dalam kode berikut.

// 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");
    }
}

Anda dapat mengatur semua pemetaan API Fluent dalam metode yang samaOnModelCreating, tetapi disarankan untuk mempartisi kode tersebut dan memiliki beberapa kelas konfigurasi, satu per entitas, seperti yang ditunjukkan dalam contoh. Khusus untuk model besar, disarankan untuk memiliki kelas konfigurasi terpisah untuk mengonfigurasi jenis entitas yang berbeda.

Kode dalam contoh menunjukkan beberapa deklarasi dan pemetaan eksplisit. Namun, konvensi EF Core melakukan banyak pemetaan tersebut secara otomatis, sehingga kode aktual yang Anda butuhkan dalam kasus Anda mungkin lebih kecil.

Algoritma Hi/Lo di EF Core

Aspek kode yang menarik dalam contoh sebelumnya adalah menggunakan algoritma Hi/Lo sebagai strategi pembuatan kunci.

Algoritma Hi/Lo berguna saat Anda memerlukan kunci unik sebelum melakukan perubahan. Sebagai ringkasan, algoritma Hi-Lo menetapkan pengidentifikasi unik ke baris tabel sementara tidak bergantung pada penyimpanan baris dalam database dengan segera. Ini memungkinkan Anda mulai menggunakan pengidentifikasi segera, seperti yang terjadi dengan ID database berurutan reguler.

Algoritma Hi/Lo menjelaskan mekanisme untuk mendapatkan batch ID unik dari urutan database terkait. ID ini aman digunakan karena database menjamin keunikan, sehingga tidak akan terjadi benturan antara pengguna. Algoritma ini menarik karena alasan berikut:

  • Ini tidak merusak pola Unit Kerja.

  • Ini mendapatkan ID urutan dalam batch, untuk meminimalkan perjalanan pulang pergi ke database.

  • Ini menghasilkan pengidentifikasi yang dapat dibaca manusia, tidak seperti teknik yang menggunakan GUID.

EF Core mendukung HiLo dengan UseHiLo metode, seperti yang ditunjukkan pada contoh sebelumnya.

Memetakan bidang alih-alih properti

Dengan fitur ini, tersedia sejak EF Core 1.1, Anda dapat langsung memetakan kolom ke bidang. Dimungkinkan untuk tidak menggunakan properti di kelas entitas, dan hanya untuk memetakan kolom dari tabel ke bidang. Penggunaan umum untuk itu adalah bidang privat untuk status internal apa pun yang tidak perlu diakses dari luar entitas.

Anda dapat melakukan ini dengan satu bidang atau juga dengan koleksi, seperti List<> bidang. Titik ini disebutkan sebelumnya ketika kita membahas pemodelan kelas model domain, tetapi di sini Anda dapat melihat bagaimana pemetaan itu dilakukan dengan konfigurasi yang PropertyAccessMode.Field disorot dalam kode sebelumnya.

Menggunakan properti bayangan di EF Core, tersembunyi di tingkat infrastruktur

Properti bayangan di EF Core adalah properti yang tidak ada di model kelas entitas Anda. Nilai dan status properti ini dipertahankan murni di kelas ChangeTracker di tingkat infrastruktur.

Menerapkan pola Spesifikasi Kueri

Seperti yang diperkenalkan sebelumnya di bagian desain, pola Spesifikasi Kueri adalah pola desain Domain-Driven yang dirancang sebagai tempat Anda dapat menempatkan definisi kueri dengan logika pengurutan dan penomoran opsional.

Pola Spesifikasi Kueri menentukan kueri dalam objek. Misalnya, untuk merangkum kueri halaman yang mencari beberapa produk, Anda dapat membuat spesifikasi PagedProduct yang mengambil parameter input yang diperlukan (pageNumber, pageSize, filter, dll.). Kemudian, dalam metode Repositori apa pun (biasanya overload List()) akan menerima IQuerySpecification dan menjalankan kueri yang diharapkan berdasarkan spesifikasi tersebut.

Contoh antarmuka Spesifikasi generik adalah kode berikut, yang mirip dengan kode yang digunakan dalam aplikasi referensi 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; }
}

Kemudian, implementasi kelas dasar spesifikasi generik adalah sebagai berikut.

// 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
    // e.g. Basket.Items.Product
    protected virtual void AddInclude(string includeString)
    {
        IncludeStrings.Add(includeString);
    }
}

Spesifikasi berikut memuat entitas ke basket tunggal yang diberikan ID keramaian atau ID pembeli yang menjadi milik kerang. Ini akan dengan senang hati memuat koleksi keramaian 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);
    }
}

Dan akhirnya, Anda dapat melihat di bawah ini bagaimana Repositori EF generik dapat menggunakan spesifikasi seperti itu untuk memfilter dan memuat data yang bersemangat terkait dengan jenis entitas T tertentu.

// 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();
}

Selain merangkum logika pemfilteran, spesifikasi dapat menentukan bentuk data yang akan dikembalikan, termasuk properti mana yang akan diisi.

Meskipun kami tidak menyarankan untuk kembali IQueryable dari repositori, sangat baik untuk menggunakannya dalam repositori untuk membangun serangkaian hasil. Anda dapat melihat pendekatan ini digunakan dalam metode Daftar di atas, yang menggunakan ekspresi perantara IQueryable untuk menyusun daftar kueri yang disertakan sebelum menjalankan kueri dengan kriteria spesifikasi pada baris terakhir.

Pelajari bagaimana pola spesifikasi diterapkan dalam sampel eShopOnWeb.

Sumber daya tambahan