Sdílet prostřednictvím


Implementace vrstvy trvalosti infrastruktury pomocí Entity Framework Core

Tip

Tento obsah je výňatek z eBooku, architektury mikroslužeb .NET pro kontejnerizované aplikace .NET, které jsou k dispozici na .NET Docs nebo jako zdarma ke stažení PDF, které lze číst offline.

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

Pokud používáte relační databáze, jako je SQL Server, Oracle nebo PostgreSQL, doporučujeme implementovat vrstvu trvalosti založenou na entity Framework (EF). EF podporuje LINQ a poskytuje objekty silného typu pro váš model a také zjednodušenou trvalost databáze.

Entity Framework má dlouhou historii jako součást rozhraní .NET Framework. Pokud používáte .NET, měli byste také použít Entity Framework Core, který běží ve Windows nebo Linuxu stejným způsobem jako .NET. EF Core je kompletní přepis entity Frameworku, který je implementovaný s mnohem menšími nároky a důležitými vylepšeními výkonu.

Úvod do Entity Framework Core

Entity Framework (EF) Core je jednoduchá, rozšiřitelná a multiplatformní verze oblíbené technologie přístupu k datům Entity Framework. V polovině roku 2016 byla představena s .NET Core.

Vzhledem k tomu, že úvod do EF Core je již k dispozici v dokumentaci Microsoftu, nabízíme odkazy na uvedené informace.

Další materiály

Infrastruktura v Entity Framework Core z pohledu DDD

Z pohledu DDD je důležitá schopnost EF používat entity domény POCO, známé také v terminologii EF jako entity poco typu kód-first. Pokud používáte entity domény POCO, vaše třídy doménového modelu jsou trvalost-ignorant, podle zásad trvalosti ainfrastruktury).

V jednotlivých vzorech DDD byste měli zapouzdřovat chování domény a pravidla v rámci samotné třídy entity, takže může řídit invarianty, ověřování a pravidla při přístupu k jakékoli kolekci. Proto není vhodné v DDD povolit veřejný přístup k kolekcí podřízených entit nebo objektů hodnot. Místo toho chcete zveřejnit metody, které řídí, jak a kdy se dají aktualizovat pole a kolekce vlastností a jaké chování a akce by se měly vyskytnout, když k tomu dojde.

Vzhledem k tomu, že EF Core 1.1 splňuje tyto požadavky DDD, můžete mít ve svých entitách místo veřejných vlastností prosté pole. Pokud nechcete, aby bylo pole entity externě přístupné, můžete místo vlastnosti vytvořit atribut nebo pole. Můžete také použít soukromé vlastnosti setters.

Podobným způsobem teď můžete mít přístup ke kolekcím jen pro čtení pomocí veřejné vlastnosti, IReadOnlyCollection<T>která je založená na členu soukromého pole pro kolekci (například a List<T>) ve vaší entitě, která spoléhá na trvalost EF. Předchozí verze Entity Framework vyžadovaly vlastnosti kolekce, které podporují ICollection<T>, což znamenalo, že každý vývojář používající nadřazenou třídu entity mohl přidávat nebo odebírat položky prostřednictvím svých kolekcí vlastností. Tato možnost by byla proti doporučeným vzorům v DDD.

Privátní kolekci můžete použít při zveřejnění objektu jen IReadOnlyCollection<T> pro čtení, jak je znázorněno v následujícím příkladu kódu:

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

K OrderItems vlastnosti lze přistupovat pouze pro čtení pomocí IReadOnlyCollection<OrderItem>. Tento typ je jen pro čtení, takže je chráněný před běžnými externími aktualizacemi.

EF Core poskytuje způsob, jak mapovat model domény na fyzickou databázi bez "kontaminace" doménového modelu. Jedná se o čistý kód .NET POCO, protože akce mapování je implementována ve vrstvě trvalosti. V této akci mapování je potřeba nakonfigurovat mapování polí na databázi. V následujícím příkladu OnModelCreating metody z OrderingContext a OrderEntityTypeConfiguration třídy volání sděluje SetPropertyAccessMode EF Core přístup k OrderItems vlastnosti prostřednictvím jeho pole.

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

Pokud místo vlastností použijete pole, entita OrderItem se zachová, jako by měla List<OrderItem> vlastnost. Zpřístupňuje však jeden přístup, metodu AddOrderItem pro přidání nových položek do objednávky. V důsledku toho jsou chování a data svázané a budou konzistentní v celém kódu aplikace, který používá doménový model.

Implementace vlastních úložišť pomocí Entity Framework Core

Na úrovni implementace je úložiště jednoduše třída s kódem trvalosti dat koordinovaným jednotkou práce (DBContext v EF Core) při provádění aktualizací, jak je znázorněno v následující třídě:

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

Rozhraní IBuyerRepository pochází z vrstvy doménového modelu jako kontrakt. Implementace úložiště se ale provádí ve vrstvě trvalosti a infrastruktury.

EF DbContext prochází konstruktorem prostřednictvím injektáže závislostí. Sdílí se mezi několika úložišti ve stejném rozsahu požadavků HTTP, a to díky výchozí době životnosti (ServiceLifetime.Scoped) v kontejneru IoC (který lze také explicitně nastavit pomocí services.AddDbContext<>).

Metody implementace v úložišti (aktualizace nebo transakce versus dotazy)

V rámci každé třídy úložiště byste měli umístit metody trvalosti, které aktualizují stav entit obsažených v související agregaci. Mějte na paměti, že mezi agregací a souvisejícím úložištěm existuje vztah 1:1. Vezměte v úvahu, že objekt agregované kořenové entity může mít vložené podřízené entity v rámci grafu EF. Kupující může mít například více způsobů platby jako související podřízené entity.

Vzhledem k tomu, že přístup k objednávání mikroslužeb v eShopOnContainers je také založený na CQS/CQRS, většina dotazů se neimplementuje ve vlastních úložištích. Vývojáři mají svobodu vytvářet dotazy a spojení, které potřebují pro prezentační vrstvu bez omezení, která platí pro agregace, vlastní úložiště na agregaci a DDD obecně. Většina vlastních úložišť, která tato příručka navrhuje, má několik metod aktualizace nebo transakcí, ale pouze metody dotazu potřebné k získání dat, které se mají aktualizovat. Například úložiště KupujícíRepository implementuje metodu FindAsync, protože aplikace potřebuje vědět, zda určitý kupující existuje před vytvořením nového kupujícího souvisejícího s objednávkou.

Skutečné metody dotazů pro získání dat pro odesílání do prezentační vrstvy nebo klientských aplikací se však implementují, jak je uvedeno, v dotazech CQRS na základě flexibilních dotazů pomocí Dapperu.

Přímé použití vlastního úložiště versus použití EF DbContext

Třída Entity Framework DbContext je založená na vzorech práce a úložiště a lze ji použít přímo z kódu, například z kontroleru ASP.NET Core MVC. Vzory práce a úložiště vedou k nejjednoduššímu kódu, například v mikroslužbě katalogu CRUD v eShopOnContainers. V případech, kdy chcete nejjednodušší možný kód, můžete chtít přímo použít třídu DbContext, kolik vývojářů to dělá.

Implementace vlastních úložišť ale přináší několik výhod při implementaci složitějších mikroslužeb nebo aplikací. Vzory práce a úložiště jsou určeny k zapouzdření vrstvy trvalosti infrastruktury, aby byla oddělená od vrstev aplikace a modelu domény. Implementace těchto vzorů může usnadnit použití napodobených úložišť simulujících přístup k databázi.

Na obrázku 7–18 můžete vidět rozdíly mezi tím, že nepoužíváte úložiště (přímo pomocí EF DbContext) a nepoužíváte úložiště, což usnadňuje napodobení těchto úložišť.

Diagram showing the components and dataflow in the two repositories.

Obrázek 7–18 Použití vlastních úložišť a prostého dbContextu

Obrázek 7–18 ukazuje, že použití vlastního úložiště přidává abstrakční vrstvu, která se dá použít k usnadnění testování napodobováním úložiště. Při napodobování existuje několik alternativ. Mohli byste napodobenit pouze úložiště nebo byste mohli napodobenou celou jednotku práce. Obvykle stačí napodobení jenom úložišť a složitost abstrakce a napodobení celé pracovní jednotky obvykle není potřeba.

Později, když se zaměříme na aplikační vrstvu, uvidíte, jak funguje injektáž závislostí v ASP.NET Core a jak se implementuje při použití úložišť.

Stručně řečeno, vlastní úložiště umožňují snadněji testovat kód pomocí testů jednotek, které nejsou ovlivněny stavem datové vrstvy. Pokud spustíte testy, které přistupují také ke skutečné databázi prostřednictvím Entity Frameworku, nejsou testy jednotek, ale integrační testy, které jsou mnohem pomalejší.

Pokud byste používali DbContext přímo, museli byste ho napodobit nebo spustit testy jednotek pomocí SQL Serveru v paměti s předvídatelnými daty pro testy jednotek. Ale napodobování DbContext nebo řízení falešných dat vyžaduje více práce než napodobování na úrovni úložiště. Samozřejmě byste mohli vždy testovat kontrolery MVC.

Životnost instance EF DbContext a IUnitOfWork v kontejneru IoC

Objekt DbContext (vystavený jako IUnitOfWork objekt) by se měl sdílet mezi více úložišti ve stejném oboru požadavku HTTP. To platí například v případě, že operace, která se spouští, se musí zabývat více agregacemi, nebo jednoduše proto, že používáte více instancí úložiště. Je také důležité zmínit, že IUnitOfWork rozhraní je součástí vaší doménové vrstvy, nikoli typu EF Core.

Aby to bylo možné, musí instance objektu DbContext mít jeho životnost služby nastavena na ServiceLifetime.Scoped. Toto je výchozí doba života při registraci do kontejneru DbContext IoC ze souboru Program.cs ve vašem projektu webového rozhraní API ASP.NET builder.Services.AddDbContext Core. Následující kód to ilustruje.

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

Režim vytváření instancí DbContext by neměl být nakonfigurovaný jako ServiceLifetime.Transient nebo ServiceLifetime.Singleton.

Životnost instance úložiště v kontejneru IoC

Podobně by životnost úložiště měla být obvykle nastavena jako vymezená (InstancePerLifetimeScope v Autofacu). Může to být také přechodné (InstancePerDependency ve službě Autofac), ale vaše služba bude efektivnější s ohledem na paměť při použití omezené životnosti.

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

Použití životnosti jednohotonu pro úložiště může způsobit vážné problémy souběžnosti, když je vlastnost DbContext nastavená na dobu životnosti (InstancePerLifetimeScope) (výchozí životnost pro DBContext). Pokud jsou vaše životnost služeb pro vaše úložiště i dbContext vymezená, vyhnete se těmto problémům.

Další materiály

Mapování tabulek

Mapování tabulek identifikuje data tabulky, která se mají dotazovat a uložit do databáze. Dříve jste viděli, jak lze entity domény (například produkt nebo doména objednávky) použít k vygenerování souvisejícího schématu databáze. EF je silně navržena podle konceptu konvencí. Konvence řeší otázky typu "Jaký bude název tabulky?" nebo "Jaká vlastnost je primární klíč?". Konvence jsou obvykle založené na konvenčních názvech. Například je typické, že primární klíč je vlastnost, která končí Id.

Podle konvence se každá entita nastaví tak, aby se mapovat na tabulku se stejným názvem jako DbSet<TEntity> vlastnost, která entitu zveřejňuje v odvozeného kontextu. Pokud pro danou entitu není zadaná žádná DbSet<TEntity> hodnota, použije se název třídy.

Datové poznámky a rozhraní Fluent API

Existuje mnoho dalších konvencí EF Core a většina z nich se dá změnit pomocí datových poznámek nebo rozhraní Fluent API implementovaných v rámci metody OnModelCreating.

Datové poznámky musí být použity u samotných tříd modelu entit, což je rušivý způsob z hlediska DDD. Důvodem je to, že model kontaminujete datovými poznámkami souvisejícími s databází infrastruktury. Na druhou stranu je rozhraní Fluent API pohodlným způsobem, jak změnit většinu konvencí a mapování ve vrstvě infrastruktury trvalosti dat, takže model entity bude čistý a oddělený od infrastruktury trvalosti.

Rozhraní Fluent API a metoda OnModelCreating

Jak už bylo zmíněno, abyste změnili konvence a mapování, můžete použít OnModelCreating metoda v DbContext třídy.

Objednávková mikroslužba v eShopOnContainers v případě potřeby implementuje explicitní mapování a konfiguraci, jak je znázorněno v následujícím kódu.

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

Můžete nastavit všechna mapování rozhraní Fluent API ve stejné OnModelCreating metodě, ale doporučujeme rozdělit tento kód a mít více tříd konfigurace, jednu na entitu, jak je znázorněno v příkladu. Zejména u velkých modelů je vhodné mít samostatné třídy konfigurace pro konfiguraci různých typů entit.

Kód v příkladu ukazuje několik explicitních deklarací a mapování. Konvence EF Core ale provádí mnoho z těchto mapování automaticky, takže skutečný kód, který byste potřebovali ve vašem případě, může být menší.

Algoritmus Hi/Lo v EF Core

Zajímavým aspektem kódu v předchozím příkladu je, že jako strategii generování klíčů používá algoritmus Hi/Lo.

Algoritmus Hi/Lo je užitečný, když před potvrzením změn potřebujete jedinečné klíče. Stručně řečeno, algoritmus Hi-Lo přiřazuje k řádkům tabulky jedinečné identifikátory, aniž by se v závislosti na okamžitém uložení řádku v databázi. Díky tomu můžete hned začít používat identifikátory, stejně jako u běžných sekvenčních ID databází.

Algoritmus Hi/Lo popisuje mechanismus získání dávky jedinečných ID ze související sekvence databáze. Tato ID jsou bezpečná pro použití, protože databáze zaručuje jedinečnost, takže nedojde ke kolizím mezi uživateli. Tento algoritmus je zajímavý z těchto důvodů:

  • Neporušuje vzor jednotek práce.

  • Získá id sekvence v dávkách, aby se minimalizovala doba odezvy do databáze.

  • Generuje čitelný identifikátor člověka na rozdíl od technik, které používají identifikátory GUID.

EF Core podporuje HiLo s metodou UseHiLo , jak je znázorněno v předchozím příkladu.

Mapování polí místo vlastností

Díky této funkci, která je dostupná od EF Core 1.1, můžete přímo mapovat sloupce na pole. Ve třídě entity není možné používat vlastnosti a pouze namapovat sloupce z tabulky na pole. Běžné použití tohoto typu by byla soukromá pole pro jakýkoli interní stav, ke kterému není potřeba přistupovat mimo entitu.

Můžete to udělat s jedním polem nebo také s kolekcemi, jako je List<> pole. Tento bod byl zmíněn dříve, když jsme probrali modelování tříd doménového modelu, ale tady vidíte, jak se toto mapování provádí s PropertyAccessMode.Field konfigurací zvýrazněnou v předchozím kódu.

Použití stínových vlastností v EF Core skryté na úrovni infrastruktury

Stínové vlastnosti v EF Core jsou vlastnosti, které ve vašem modelu tříd entit neexistují. Hodnoty a stavy těchto vlastností jsou udržovány čistě ve třídě ChangeTracker na úrovni infrastruktury.

Implementace vzoru specifikace dotazu

Jak jsme představili dříve v části návrhu, vzor specifikace dotazu je vzor návrhu řízený doménou navržený jako místo, kde můžete vložit definici dotazu s volitelnou logikou řazení a stránkování.

Vzor specifikace dotazu definuje dotaz v objektu. Například pro zapouzdření stránkovaného dotazu, který hledá některé produkty, můžete vytvořit specifikaci PagedProduct, která přebírá nezbytné vstupní parametry (pageNumber, pageSize, filtr atd.). Pak v rámci jakékoli metody úložiště (obvykle přetížení List() by přijal IQuerySpecification a spustil očekávaný dotaz na základě této specifikace.

Příkladem obecného rozhraní specifikace je následující kód, který se podobá kódu použitému v referenční aplikaci 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; }
}

Implementace základní třídy obecné specifikace je následující.

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

Následující specifikace načte jednu entitu košíku s ID košíku nebo ID kupujícího, kterému košík patří. Bude dychtivě načítat kolekci košíkuItems.

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

A nakonec si můžete prohlédnout níže, jak může obecné úložiště EF použít takovou specifikaci k filtrování a dychtivosti načítání dat souvisejících s daným typem entity 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();
}

Kromě zapouzdření logiky filtrování může specifikace určit tvar vrácených dat, včetně vlastností, které se mají naplnit.

I když nedoporučujeme vracet IQueryable se z úložiště, je naprosto v pořádku je použít v rámci úložiště k vytvoření sady výsledků. Tento přístup se používá v metodě List výše, který používá přechodné IQueryable výrazy k sestavení seznamu zahrnutí dotazu před spuštěním dotazu s kritérii specifikace na posledním řádku.

Zjistěte , jak se vzor specifikace používá v ukázce eShopOnWeb.

Další materiály