Udostępnij przez


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

Wskazówka

Ta treść jest fragmentem eBooka "Architektura mikrousług .NET dla konteneryzowanych aplikacji .NET", dostępnego na .NET Docs lub jako bezpłatny plik PDF do pobrania i czytania w trybie offline.

Miniatura okładki eBooka „Architektura mikrousług platformy .NET dla konteneryzowanych aplikacji platformy .NET”.

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). Entity Framework obsługuje LINQ i udostępnia silnie typizowane obiekty dla Twojego modelu oraz uproszczone utrwalanie w Twojej 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 całkowite przepisanie Entity Framework, które jest realizowane przy znacznie mniejszym obciążeniu i oferuje ważne ulepszenia 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 jednostki POCO definiowane w podejściu code-first. Jeśli używasz jednostek domeny POCO, klasy modelu domeny są ignorujące trwałość, zgodnie z zasadą Niewiedzy trwałości i zasadą Ignorowania infrastruktury.

Zgodnie z wzorcami DDD, powinieneś umieścić zachowanie i reguły domeny w samej klasie encji, tak aby mogła kontrolować niezmienniki, 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.

Od EF Core 1.1, aby spełnić wymagania DDD, możesz mieć zwykłe pola w encjach 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ć ustawień właściwości prywatnych.

W podobny sposób można teraz uzyskać dostęp tylko do odczytu do kolekcji, używając właściwości publicznej typu IReadOnlyCollection<T>, która jest wspierana przez prywatne pole kolekcji (takie jak List<T>) 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żywać kolekcji prywatnej, udostępniając obiekt odczytu IReadOnlyCollection<T> tylko do 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 do właściwości OrderItems można uzyskać tylko w trybie tylko do odczytu za pomocą 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 .NET POCO, ponieważ akcja mapowania jest implementowana w warstwie utrzymania. W tej akcji mapowania należy skonfigurować mapowanie pól do bazy danych. W poniższym przykładzie metody OnModelCreating z klasy OrderingContext i OrderEntityTypeConfiguration, wywołanie SetPropertyAccessMode instruuje EF Core, aby uzyskać dostęp do właściwości OrderItems 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 pojedynczy akcesor, metodę 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 współużytkowany między wieloma repozytoriami w tym samym zakresie żądania HTTP, dzięki domyślnemu okresowi istnienia (ServiceLifetime.Scoped) w kontenerze IoC (który można również jawnie ustawić za pomocą services.AddDbContext<>).

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 agregatem 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.

Używanie niestandardowego repozytorium kontra bezpośrednie używanie 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 prowadzą do najprostszego kodu, tak jak w mikrousłudze katalogu CRUD w eShopOnContainers. W przypadkach, gdy potrzebujesz najprostszego kodu, możesz chcieć bezpośrednio użyć klasy DbContext, jak robi wielu programistó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 przedstawiający składniki i przepływ danych w dwóch repozytoriach.

Rysunek 7–18. Używanie repozytoriów niestandardowych kontra zwykły 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 DbContext bezpośrednio, musisz go mockować lub uruchamiać testy jednostkowe, korzystając z SQL Server w pamięci z przewidywalnymi danymi dla testów jednostkowych. Jednak symulowanie elementu DbContext lub obsługiwanie testowych danych wymaga więcej pracy niż symulowanie na poziomie warstwy 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ć określony cykl życia usługi jako ServiceLifetime.Scoped. Jest to domyślny czas życia podczas rejestrowania DbContext z builder.Services.AddDbContext w kontenerze IoC z pliku Program.cs w projekcie ASP.NET Core Web API. Poniższy kod ilustruje to.

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

Cykl życia wystąpienia repozytorium w kontenerze IoC

W podobny sposób czas życia repozytorium powinien być zwykle ustawiany jako zakres (InstancePerLifetimeScope w Autofac). Może to być również przejściowe (InstancePerDependency w Autofac), ale usługa będzie bardziej wydajna pod względem zużycia pamięci, gdy korzystasz z okresu istnienia o określonym zakresie.

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

Użycie cyklu życia singletonu dla repozytorium może spowodować poważne problemy ze współbieżnością, gdy DbContext jest ustawiony na cykl życia ograniczony zakresem (InstancePerLifetimeScope), co jest domyślnym ustawieniem dla DbContext. O ile czasy życia usług dla repozytoriów i DbContext są ustanowione jako zakresowe, unikniesz tych problemów.

Dodatkowe zasoby

Mapowanie tabeli

Mapowanie tabeli identyfikuje dane tabeli do pobierania i które mają być 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 encja zostanie skonfigurowana do mapowania na tabelę o nazwie takiej samej jak właściwość DbSet<TEntity>, która udostępnia encję 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 bezpośrednio w klasach modelu jednostek, co jest bardziej inwazyjnym podejściem z perspektywy 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 obsługi zamówień 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 działania „Unit of Work”.

  • Pobiera identyfikatory sekwencji w partiach, aby zminimalizować liczbę operacji z bazą 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 za pomocą metody UseHiLo, 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żyj właściwości cieniowych w EF Core, które są niewidoczne 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 Domain-Driven 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 ramach dowolnej metody repozytorium (zwykle w przeciążeniu List()), akceptowana byłaby IQuerySpecification i uruchamiane byłoby 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
    // for example, 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 entuzjazmem załaduje zbiory koszyka 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);
    }
}

Na koniec możesz zobaczyć poniżej, jak standardowe repozytorium EF może wykorzystać tę specyfikację do filtrowania i jednoczesnego ładowania danych odnoszących się do określonego typu 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ślić strukturę zwracanych danych, w tym właściwości, które mają być uzupełnione.

Chociaż nie zalecamy zwracania IQueryable z repozytorium, jest w porządku użyć IQueryable 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