Implementowanie warstwy trwałości infrastruktury za pomocą platformy Entity Framework Core

Napiwek

Ta zawartość jest fragmentem książki eBook, architektury mikrousług platformy .NET dla konteneryzowanych aplikacji platformy .NET dostępnych na platformie .NET Docs lub jako bezpłatnego pliku PDF, który można odczytać w trybie offline.

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

W przypadku korzystania z relacyjnych baz danych, takich jak SQL Server, Oracle lub PostgreSQL, zalecane jest zaimplementowanie warstwy trwałości na podstawie programu Entity Framework (EF). Platforma EF obsługuje LINQ i udostępnia silnie typizowane obiekty dla modelu, a także uproszczoną trwałość w bazie danych.

Program Entity Framework ma długą historię w ramach programu .NET Framework. W przypadku korzystania z platformy .NET należy również użyć platformy Entity Framework Core, która działa w systemie Windows lub Linux w taki sam sposób jak platforma .NET. EF Core to kompletna zmiana struktury Entity Framework, która jest implementowana z znacznie mniejszym zużyciem i ważnymi ulepszeniami wydajności.

Wprowadzenie do platformy Entity Framework Core

Platforma Entity Framework (EF) Core to uproszczona, rozszerzalna i międzyplatformowa wersja popularnej technologii dostępu do danych programu Entity Framework. Został on wprowadzony z platformą .NET Core w połowie 2016 roku.

Ponieważ wprowadzenie do platformy EF Core jest już dostępne w dokumentacji firmy Microsoft, tutaj po prostu udostępniamy linki do tych informacji.

Dodatkowe zasoby

Infrastruktura w programie Entity Framework Core z perspektywy DDD

Z punktu widzenia DDD ważną funkcją ef jest możliwość korzystania z jednostek domeny POCO, znanych również w terminologii EF jako jednostek poco code-first. Jeśli używasz jednostek domeny POCO, klasy modelu domeny są trwałe ignorowane, zgodnie z zasadami Niewiedza trwałości i Ignorancja infrastruktury.

Wzorce DDD należy hermetyzować zachowanie i reguły domeny w samej klasie jednostki, aby można było kontrolować niezmienne, walidacje i reguły podczas uzyskiwania dostępu do dowolnej kolekcji. W związku z tym nie jest dobrym rozwiązaniem w DDD, aby umożliwić publiczny dostęp do kolekcji jednostek podrzędnych lub obiektów wartości. Zamiast tego chcesz uwidocznić metody, które kontrolują, jak i kiedy można aktualizować pola i kolekcje właściwości oraz jakie zachowanie i akcje powinny wystąpić w takim przypadku.

Ponieważ program EF Core 1.1, aby spełnić te wymagania DDD, można mieć zwykłe pola w jednostkach zamiast właściwości publicznych. Jeśli nie chcesz, aby pole jednostki było dostępne zewnętrznie, możesz utworzyć atrybut lub pole zamiast właściwości. Można również użyć zestawów właściwości prywatnych.

W podobny sposób można teraz mieć dostęp tylko do odczytu do kolekcji przy użyciu właściwości publicznej wpisanej jako IReadOnlyCollection<T>, która jest wspierana przez element członkowski pola prywatnego dla kolekcji (na List<T>przykład ) w jednostce, która opiera się na ef dla trwałości. Poprzednie wersje programu Entity Framework wymagały obsługi właściwości ICollection<T>kolekcji , co oznaczało, że każdy deweloper korzystający z klasy jednostek nadrzędnych może dodawać lub usuwać elementy za pośrednictwem kolekcji właściwości. Taka możliwość byłaby sprzeczna z zalecanymi wzorcami w DDD.

Można użyć kolekcji prywatnej podczas uwidaczniania obiektu tylko do IReadOnlyCollection<T> odczytu, jak pokazano w poniższym przykładzie kodu:

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

Dostęp OrderItems do właściwości można uzyskać tylko jako tylko do odczytu przy użyciu polecenia IReadOnlyCollection<OrderItem>. Ten typ jest tylko do odczytu, więc jest chroniony przed regularnymi aktualizacjami zewnętrznymi.

Program EF Core umożliwia mapowanie modelu domeny na fizyczną bazę danych bez "zakażania" modelu domeny. Jest to czysty kod POCO platformy .NET, ponieważ akcja mapowania jest implementowana w warstwie trwałości. W tej akcji mapowania należy skonfigurować mapowanie pól do bazy danych. W poniższym przykładzie OnModelCreating metody z OrderingContext i OrderEntityTypeConfiguration klasy wywołanie polecenia programu EF Core w celu SetPropertyAccessMode uzyskania dostępu do OrderItems właściwości za pośrednictwem pola.

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

W przypadku używania pól zamiast właściwości jednostka jest utrwalana tak, OrderItem jakby miała List<OrderItem> właściwość. Uwidacznia jednak pojedynczą metodę dostępu AddOrderItem do dodawania nowych elementów do zamówienia. W związku z tym zachowanie i dane są ze sobą powiązane i będą spójne w całym kodzie aplikacji, który używa modelu domeny.

Implementowanie repozytoriów niestandardowych za pomocą platformy Entity Framework Core

Na poziomie implementacji repozytorium jest po prostu klasą z kodem trwałości danych koordynowanym przez jednostkę pracy (DBContext w programie EF Core) podczas wykonywania aktualizacji, jak pokazano w następującej klasie:

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

Interfejs IBuyerRepository pochodzi z warstwy modelu domeny jako kontraktu. Jednak implementacja repozytorium jest wykonywana w warstwie trwałości i infrastruktury.

Funkcja EF DbContext przechodzi przez konstruktor za pomocą wstrzykiwania zależności. Jest on współużytkowany między wieloma repozytoriami w tym samym zakresie żądania HTTP, dzięki domyślnemu okresowi istnienia () w kontenerze IoC (ServiceLifetime.Scopedktóry można również jawnie ustawić za pomocą services.AddDbContext<>polecenia ).

Metody implementowania w repozytorium (aktualizacje lub transakcje w porównaniu z zapytaniami)

W każdej klasie repozytorium należy umieścić metody trwałości, które aktualizują stan jednostek zawartych w powiązanej agregacji. Pamiętaj, że istnieje relacja jeden do jednego między agregacji a powiązanym repozytorium. Należy wziąć pod uwagę, że zagregowany obiekt jednostki głównej może mieć osadzone jednostki podrzędne w ramach grafu EF. Na przykład kupujący może mieć wiele form płatności jako powiązane jednostki podrzędne.

Ponieważ podejście do zamawiania mikrousługi w eShopOnContainers jest również oparte na CQS/CQRS, większość zapytań nie jest implementowana w repozytoriach niestandardowych. Deweloperzy mają swobodę tworzenia zapytań i sprzężeń potrzebnych do warstwy prezentacji bez ograniczeń narzuconych przez agregacje, repozytoria niestandardowe na agregację i DDD w ogóle. Większość repozytoriów niestandardowych sugerowanych przez ten przewodnik zawiera kilka metod aktualizacji lub transakcyjnych, ale tylko metody zapytań potrzebne do zaktualizowania danych. Na przykład repozytorium Repozytorium Nabywców implementuje metodę FindAsync, ponieważ aplikacja musi wiedzieć, czy dany nabywca istnieje przed utworzeniem nowego nabywcy powiązanego z zamówieniem.

Jednak rzeczywiste metody zapytań służące do pobierania danych wysyłanych do warstwy prezentacji lub aplikacji klienckich są implementowane, jak wspomniano, w zapytaniach CQRS opartych na elastycznych zapytaniach przy użyciu języka Dapper.

Bezpośrednie używanie repozytorium niestandardowego w porównaniu z używaniem interfejsu EF DbContext

Klasa DbContext programu Entity Framework jest oparta na wzorcach Unit of Work i Repository i może być używana bezpośrednio z kodu, na przykład z kontrolera MVC platformy ASP.NET Core. Wzorce Unit of Work i Repository powodują najprostszy kod, tak jak w mikrousłudze wykazu CRUD w eShopOnContainers. W przypadkach, gdy potrzebujesz najprostszego kodu, możesz chcieć bezpośrednio użyć klasy DbContext, jak wielu deweloperów.

Jednak implementacja repozytoriów niestandardowych zapewnia kilka korzyści podczas implementowania bardziej złożonych mikrousług lub aplikacji. Wzorce Unit of Work and Repository mają na celu hermetyzowanie warstwy trwałości infrastruktury, dzięki czemu jest ona oddzielona od warstw aplikacji i modelu domeny. Zaimplementowanie tych wzorców może ułatwić korzystanie z pozornych repozytoriów symulujących dostęp do bazy danych.

Na rysunku 7–18 widać różnice między brakiem używania repozytoriów (bezpośrednio przy użyciu programu EF DbContext) a użyciem repozytoriów, co ułatwia pozorowanie tych repozytoriów.

Diagram showing the components and dataflow in the two repositories.

Rysunek 7–18. Używanie repozytoriów niestandardowych w porównaniu do zwykłego elementu DbContext

Rysunek 7–18 pokazuje, że użycie niestandardowego repozytorium dodaje warstwę abstrakcji, która może służyć do ułatwienia testowania przez wyśmiewanie repozytorium. Istnieje wiele alternatyw podczas szyderstwa. Możesz wyśmiewać tylko repozytoria lub wyśmiewać całą jednostkę pracy. Zwykle wyśmiewanie tylko repozytoriów jest wystarczające, a złożoność abstrakcji i pozorowania całej jednostki pracy zwykle nie jest potrzebna.

Później, gdy skupimy się na warstwie aplikacji, zobaczysz, jak działa wstrzykiwanie zależności w ASP.NET Core i jak jest implementowane podczas korzystania z repozytoriów.

Krótko mówiąc, repozytoria niestandardowe umożliwiają łatwiejsze testowanie kodu przy użyciu testów jednostkowych, które nie mają wpływu na stan warstwy danych. Jeśli uruchamiasz testy, które również uzyskują dostęp do rzeczywistej bazy danych za pośrednictwem programu Entity Framework, nie są to testy jednostkowe, ale testy integracji, które są znacznie wolniejsze.

Jeśli używasz programu DbContext bezpośrednio, musisz go wyśmiewać lub uruchamiać testy jednostkowe przy użyciu programu SQL Server w pamięci z przewidywalnymi danymi na potrzeby testów jednostkowych. Jednak wyśmiewanie elementu DbContext lub kontrolowanie fałszywych danych wymaga więcej pracy niż pozorowanie na poziomie repozytorium. Oczywiście zawsze można przetestować kontrolery MVC.

Okres istnienia wystąpienia EF DbContext i IUnitOfWork w kontenerze IoC

DbContext Obiekt (uwidoczniony jako IUnitOfWork obiekt) powinien być współużytkowany między wieloma repozytoriami w tym samym zakresie żądania HTTP. Na przykład jest to prawda, gdy wykonywana operacja musi obsługiwać wiele agregacji lub po prostu dlatego, że używasz wielu wystąpień repozytorium. Należy również wspomnieć, że IUnitOfWork interfejs jest częścią warstwy domeny, a nie typu EF Core.

Aby to zrobić, wystąpienie DbContext obiektu musi mieć ustawiony okres istnienia usługi na ServiceLifetime.Scoped. Jest to domyślny okres istnienia podczas rejestrowania DbContextbuilder.Services.AddDbContext elementu w kontenerze IoC z pliku Program.cs w projekcie ASP.NET Core Web API. Ilustruje to poniższy kod.

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

Tryb tworzenia wystąpienia dbContext nie powinien być skonfigurowany jako ServiceLifetime.Transient lub ServiceLifetime.Singleton.

Okres istnienia wystąpienia repozytorium w kontenerze IoC

W podobny sposób okres istnienia repozytorium powinien być zwykle ustawiany jako zakres (InstancePerLifetimeScope w autofac). Może to być również przejściowe (WystąpieniePerDependency w autofac), ale usługa będzie bardziej wydajna w odniesieniu do pamięci w przypadku korzystania z okresu istnienia o określonym zakresie.

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

Użycie okresu istnienia pojedynczego repozytorium może spowodować poważne problemy ze współbieżnością, gdy właściwość DbContext jest ustawiona na okres istnienia zakresu (InstancePerLifetimeScope) (domyślne okresy istnienia elementu DBContext). O ile okresy istnienia usługi dla repozytoriów i elementu DbContext są objęte zakresem, należy uniknąć tych problemów.

Dodatkowe zasoby

Mapowanie tabeli

Mapowanie tabeli identyfikuje dane tabeli do odpytowania i zapisywane w bazie danych. Wcześniej pokazano, jak jednostki domeny (na przykład domena produktu lub zamówienia) mogą służyć do generowania powiązanego schematu bazy danych. Program EF jest silnie zaprojektowany zgodnie z koncepcją konwencji. Konwencje dotyczą pytań, takich jak "Jaka będzie nazwa tabeli?" lub "Jaka właściwość jest kluczem podstawowym?" Konwencje są zwykle oparte na konwencjonalnych nazwach. Na przykład zazwyczaj klucz podstawowy jest właściwością kończącą się ciągiem Id.

Zgodnie z konwencją każda jednostka zostanie skonfigurowana do mapowania na tabelę o takiej samej nazwie jak DbSet<TEntity> właściwość, która uwidacznia jednostkę w kontekście pochodnym. Jeśli dla danej jednostki nie DbSet<TEntity> podano żadnej wartości, używana jest nazwa klasy.

Adnotacje danych a interfejs API Fluent

Istnieje wiele dodatkowych konwencji platformy EF Core i większość z nich można zmienić przy użyciu adnotacji danych lub interfejsu API Fluent zaimplementowanego w metodzie OnModelCreating.

Adnotacje danych muszą być używane w samych klasach modelu jednostek, co jest bardziej uciążliwym sposobem z punktu widzenia DDD. Dzieje się tak, ponieważ model jest zakłócany adnotacjami danych związanymi z bazą danych infrastruktury. Z drugiej strony interfejs API Fluent to wygodny sposób zmiany większości konwencji i mapowań w warstwie infrastruktury trwałości danych, więc model jednostki będzie czysty i oddzielony od infrastruktury trwałości.

Fluent API i metoda OnModelCreating

Jak wspomniano, aby zmienić konwencje i mapowania, można użyć metody OnModelCreating w klasie DbContext.

Mikrousługa porządkowania w eShopOnContainers implementuje jawne mapowanie i konfigurację, w razie potrzeby, jak pokazano w poniższym kodzie.

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

Można ustawić wszystkie mapowania interfejsu API Fluent w ramach tej samej OnModelCreating metody, ale zaleca się podzielenie tego kodu i posiadanie wielu klas konfiguracji, po jednym na jednostkę, jak pokazano w przykładzie. Szczególnie w przypadku dużych modeli zaleca się posiadanie oddzielnych klas konfiguracji do konfigurowania różnych typów jednostek.

Kod w przykładzie przedstawia kilka jawnych deklaracji i mapowania. Jednak konwencje platformy EF Core automatycznie wykonują wiele z tych mapowań, więc rzeczywisty kod, który będzie potrzebny w Twoim przypadku, może być mniejszy.

Algorytm Hi/Lo w programie EF Core

Interesującym aspektem kodu w poprzednim przykładzie jest użycie algorytmu Hi/Lo jako strategii generowania kluczy.

Algorytm Hi/Lo jest przydatny, gdy potrzebne są unikatowe klucze przed zatwierdzeniem zmian. Podsumowując, algorytm Hi-Lo przypisuje unikatowe identyfikatory do wierszy tabeli, nie w zależności od natychmiastowego przechowywania wiersza w bazie danych. Dzięki temu można od razu rozpocząć korzystanie z identyfikatorów, tak jak w przypadku zwykłych sekwencyjnych identyfikatorów baz danych.

Algorytm Hi/Lo opisuje mechanizm pobierania partii unikatowych identyfikatorów z powiązanej sekwencji bazy danych. Te identyfikatory są bezpieczne do użycia, ponieważ baza danych gwarantuje unikatowość, więc nie będzie żadnych kolizji między użytkownikami. Ten algorytm jest interesujący z następujących powodów:

  • Nie przerywa wzorca Unit of Work.

  • Pobiera identyfikatory sekwencji w partiach w celu zminimalizowania rund do bazy danych.

  • Generuje on identyfikator czytelny dla człowieka, w przeciwieństwie do technik korzystających z identyfikatorów GUID.

Program EF Core obsługuje hiLo z UseHiLo metodą , jak pokazano w poprzednim przykładzie.

Mapowanie pól zamiast właściwości

Dzięki tej funkcji, dostępnej od wersji EF Core 1.1, można bezpośrednio mapować kolumny na pola. Nie można używać właściwości w klasie jednostki i po prostu mapować kolumny z tabeli na pola. Typowym zastosowaniem tego elementu jest pole prywatne dla każdego stanu wewnętrznego, do którego nie trzeba uzyskiwać dostępu spoza jednostki.

Można to zrobić za pomocą pojedynczych pól lub kolekcji, takich jak List<> pole. Ten punkt został wymieniony wcześniej podczas omawiania modelowania klas modelu domeny, ale tutaj można zobaczyć, jak to mapowanie jest wykonywane z konfiguracją wyróżnioną PropertyAccessMode.Field w poprzednim kodzie.

Używanie właściwości w tle w programie EF Core, ukrytych na poziomie infrastruktury

Właściwości w tle w programie EF Core to właściwości, które nie istnieją w modelu klasy jednostki. Wartości i stany tych właściwości są utrzymywane wyłącznie w klasie ChangeTracker na poziomie infrastruktury.

Implementowanie wzorca specyfikacji zapytania

Jak wspomniano wcześniej w sekcji projektowania, wzorzec specyfikacji zapytania jest wzorcem projektowania opartego na domenie zaprojektowanym jako miejsce, w którym można umieścić definicję zapytania z opcjonalną logiką sortowania i stronicowania.

Wzorzec specyfikacji zapytania definiuje zapytanie w obiekcie. Na przykład, aby hermetyzować stronicowane zapytanie wyszukujące niektóre produkty, można utworzyć specyfikację PagedProduct, która przyjmuje niezbędne parametry wejściowe (pageNumber, pageSize, filter itp.). Następnie w dowolnej metodzie repozytorium (zwykle przeciążeniu List() zaakceptuje zdarzenie IQuerySpecification i uruchomi oczekiwane zapytanie na podstawie tej specyfikacji.

Przykładem ogólnego interfejsu specyfikacji jest następujący kod podobny do kodu używanego w aplikacji referencyjnej 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; }
}

Następnie implementacja klasy podstawowej specyfikacji ogólnej jest następująca.

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

Poniższa specyfikacja ładuje pojedynczą jednostkę koszyka z identyfikatorem koszyka lub identyfikatorem nabywcy, do którego należy koszyk. Z niecierpliwością załaduje kolekcję koszykaItems.

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

Na koniec możesz zobaczyć poniżej, jak ogólne repozytorium EF może używać takiej specyfikacji do filtrowania i ładowania danych związanych z danym typem jednostki 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();
}

Oprócz hermetyzacji logiki filtrowania specyfikacja może określać kształt zwracanych danych, w tym właściwości do wypełnienia.

Chociaż nie zalecamy powrotu IQueryable z repozytorium, doskonale dobrze jest użyć ich w repozytorium do utworzenia zestawu wyników. To podejście jest używane w powyższej metodzie List, która używa wyrażeń pośrednich IQueryable do utworzenia listy uwzględnień zapytania przed wykonaniem zapytania z kryteriami specyfikacji w ostatnim wierszu.

Dowiedz się , jak wzorzec specyfikacji jest stosowany w przykładzie eShopOnWeb.

Dodatkowe zasoby