Compartir a través de


Implementación del nivel de persistencia de infraestructura con Entity Framework Core

Sugerencia

Este contenido es un extracto del libro electrónico, ".NET Microservices Architecture for Containerized .NET Applications" (Arquitectura de microservicios de .NET para aplicaciones de .NET contenedorizadas), disponible en Documentación de .NET o como un PDF descargable y gratuito que se puede leer sin conexión.

Miniatura de la portada del libro electrónico 'Arquitectura de microservicios de .NET para aplicaciones .NET contenedorizadas'.

Cuando se usan bases de datos relacionales como SQL Server, Oracle o PostgreSQL, se recomienda implementar la capa de persistencia basada en Entity Framework (EF). EF es compatible con LINQ y proporciona objetos fuertemente tipados para el modelo, así como una persistencia simplificada en la base de datos.

Entity Framework tiene un historial largo como parte de .NET Framework. Al usar .NET, también debe usar Entity Framework Core, que se ejecuta en Windows o Linux de la misma manera que .NET. EF Core es una reescritura completa de Entity Framework que se implementa con una superficie mucho más pequeña y mejoras importantes en el rendimiento.

Introducción a Entity Framework Core

Entity Framework (EF) Core es una versión ligera, extensible y multiplataforma de la popular tecnología de acceso a datos de Entity Framework. Se introdujo con .NET Core a mediados de 2016.

Como una introducción a EF Core ya está disponible en la documentación de Microsoft, aquí simplemente proporcionamos vínculos a esa información.

Recursos adicionales

Infraestructura en Entity Framework Core desde una perspectiva de DDD

Desde un punto de vista de DDD, una funcionalidad importante de EF es la capacidad de usar entidades de dominio POCO, también conocidas en la terminología de EF como entidades de código primero POCO. Si usa las entidades de dominio POCO, las clases de modelo de dominio ignoran la persistencia, siguiendo los principios de omisión de persistencia y omisión de infraestructura.

Según los patrones de DDD, debe encapsular el comportamiento de dominio y las reglas dentro de la propia clase de entidad, para que pueda controlar las invariantes, validaciones y reglas al acceder a cualquier colección. Por lo tanto, en DDD no se recomienda permitir el acceso público a colecciones de entidades secundarias u objetos de valor. En su lugar, quiere exponer métodos que controlan cómo y cuándo se pueden actualizar los campos y las colecciones de propiedades, y qué comportamiento y acciones deben producirse cuando esto sucede.

Desde la versión 1.1 de EF Core, para satisfacer estos requisitos de DDD, puede tener campos sin formato en las entidades en lugar de propiedades públicas. Si no desea que un campo de entidad sea accesible externamente, solo puede crear el atributo o campo en lugar de una propiedad. También puede utilizar establecedores de propiedades privadas.

De forma parecida, ahora puede tener acceso de solo lectura a las colecciones usando una propiedad pública del tipo IReadOnlyCollection<T>, que está respaldada por un miembro de campo privado para la colección (como List<T>) en la entidad que se basa en EF para la persistencia. Las versiones anteriores de Entity Framework requerían propiedades de colección para admitir ICollection<T>, lo que significaba que cualquier desarrollador que use la clase de entidad primaria podría agregar o quitar elementos a través de sus colecciones de propiedades. Esa posibilidad sería contra los patrones recomendados en DDD.

Puede usar una colección privada para exponer un objeto IReadOnlyCollection<T> de solo lectura, como se muestra en el ejemplo de código siguiente.

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

Solo se puede tener acceso de solo lectura a la propiedad OrderItems con IReadOnlyCollection<OrderItem>. Este tipo es de solo lectura, por lo que está protegido frente a actualizaciones externas normales.

EF Core proporciona una manera de asignar el modelo de dominio a la base de datos física sin "contaminar" el modelo de dominio. Es código POCO puro de .NET, ya que la acción de mapeo se implementa en la capa de persistencia. En esa acción de asignación, debe configurar la asignación de campos a base de datos. En el siguiente ejemplo del método OnModelCreating de OrderingContext y de la clase OrderEntityTypeConfiguration, la llamada a SetPropertyAccessMode indica a EF Core que acceda a la propiedad OrderItems a través de su campo.

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

Cuando se usan campos en lugar de propiedades, la OrderItem entidad se conserva como si tuviera una List<OrderItem> propiedad . Pero expone un descriptor de acceso único, el método AddOrderItem, para agregar nuevos elementos al pedido. Como resultado, el comportamiento y los datos están vinculados y serán coherentes en cualquier código de aplicación que use el modelo de dominio.

Implementación de repositorios personalizados con Entity Framework Core

En el nivel de implementación, un repositorio es simplemente una clase con código de persistencia de datos coordinado por una unidad de trabajo (DBContext en EF Core) al realizar actualizaciones, como se muestra en la siguiente clase:

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

La IBuyerRepository interfaz procede de la capa de modelo de dominio como un contrato. Sin embargo, la implementación del repositorio se realiza en el nivel de persistencia e infraestructura.

DbContext de EF pasa mediante el constructor a través de la inserción de dependencias. Se comparte entre varios repositorios dentro del mismo ámbito de solicitud HTTP, gracias a su duración predeterminada (ServiceLifetime.Scoped) en el contenedor de IoC (que también se puede establecer explícitamente con services.AddDbContext<>).

Métodos para implementar en un repositorio (actualizaciones o transacciones frente a consultas)

Dentro de cada clase de repositorio, debe colocar los métodos de persistencia que actualizan el estado de las entidades contenidas en su agregado relacionado. Recuerde que hay una relación uno a uno entre un agregado y su repositorio relacionado. Tenga en cuenta que un objeto de entidad raíz agregada podría tener entidades secundarias incrustadas dentro de su grafo de EF. Por ejemplo, un comprador puede tener varias formas de pago como entidades secundarias relacionadas.

Dado que el enfoque para el microservicio de ordenación en eShopOnContainers también se basa en CQS/CQRS, la mayoría de las consultas no se implementan en repositorios personalizados. Los desarrolladores tienen la libertad de crear las consultas y combinaciones que necesitan para la capa de presentación sin las restricciones impuestas por agregados, repositorios personalizados por agregado y DDD en general. La mayoría de los repositorios personalizados sugeridos por esta guía tienen varios métodos de actualización o transaccionales, pero solo los métodos de consulta necesarios para actualizar los datos. Por ejemplo, el repositorio BuyerRepository implementa un método FindAsync, ya que la aplicación debe saber si existe un comprador determinado antes de crear un nuevo comprador relacionado con el pedido.

Sin embargo, los métodos de consulta reales para obtener datos para enviar a la capa de presentación o las aplicaciones cliente se implementan, como se mencionó, en las consultas CQRS basadas en consultas flexibles mediante Dapper.

Uso de un repositorio personalizado frente al uso de EF DbContext directamente

La clase DbContext de Entity Framework se basa en los patrones unit of Work y Repository y se puede usar directamente desde el código, como desde un controlador MVC de ASP.NET Core. Los patrones de unidad de trabajo y repositorio dan como resultado el código más sencillo, como en el microservicio de catálogo CRUD en eShopOnContainers. En los casos en los que quiera que sea posible el código más sencillo, es posible que quiera usar directamente la clase DbContext, como hacen muchos desarrolladores.

Sin embargo, la implementación de repositorios personalizados proporciona varias ventajas al implementar microservicios o aplicaciones más complejos. Los patrones de unidad de trabajo y repositorio están diseñados para encapsular la capa de persistencia de infraestructura, por lo que se desacopla de las capas de aplicación y modelo de dominio. La implementación de estos patrones puede facilitar el uso de repositorios ficticios que simulan el acceso a la base de datos.

En la figura 7-18, puede ver las diferencias entre no usar repositorios (directamente mediante DBContext de EF) frente al uso de repositorios, lo que facilita la simulación de esos repositorios.

Diagrama que muestra los componentes y el flujo de datos en los dos repositorios.

Figura 7-18. Uso de repositorios personalizados frente a DbContext sin formato

En la figura 7-18 se muestra que el uso de un repositorio personalizado agrega una capa de abstracción que se puede usar para facilitar las pruebas simulando el repositorio. Hay varias alternativas al plantear una simulación. Podría simular solo repositorios o podría simular una unidad de trabajo completa. Normalmente es suficiente con simular repositorios y no suele ser necesario pasar por la complejidad de tener que abstraer y simular una unidad de trabajo.

Más adelante, cuando nos centramos en la capa de aplicación, verá cómo funciona la inserción de dependencias en ASP.NET Core y cómo se implementa al usar repositorios.

En resumen, los repositorios personalizados permiten probar el código más fácilmente con pruebas unitarias que no se ven afectadas por el estado del nivel de datos. Si ejecuta pruebas que también acceden a la base de datos real a través de Entity Framework, no son pruebas unitarias, sino pruebas de integración, que son mucho más lentas.

Si estuviera usando DbContext directamente, tendría que simularlo o ejecutar pruebas unitarias utilizando una versión en memoria de SQL Server con datos predecibles para pruebas unitarias. Pero simular la clase DbContext o controlar datos falsos requiere más trabajo que la simulación en el nivel de repositorio. Por supuesto, siempre podría probar los controladores MVC.

Duración de la instancia de EF DbContext e IUnitOfWork en el contenedor de IoC

El DbContext objeto (expuesto como un IUnitOfWork objeto) debe compartirse entre varios repositorios dentro del mismo ámbito de solicitud HTTP. Por ejemplo, esto es cierto cuando la operación que se ejecuta debe tratar con varios agregados, o simplemente porque usa varias instancias de repositorio. También es importante mencionar que la interfaz de IUnitOfWork forma parte del nivel de dominio, no es un tipo de EF Core.

Para ello, la instancia del objeto DbContext debe tener su duración de servicio establecida en ServiceLifetime.Scoped. Esta es la duración predeterminada al registrar un DbContext con builder.Services.AddDbContext en el contenedor de IoC desde el archivo Program.cs en el proyecto de API web de ASP.NET Core. El código siguiente ilustra esto.

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

El modo de creación de instancias de DbContext no debe configurarse como ServiceLifetime.Transient o ServiceLifetime.Singleton.

Duración de vida de la instancia del repositorio en el contenedor IoC

De forma similar, la duración del repositorio normalmente debe establecerse a nivel de ámbito (InstancePerLifetimeScope en Autofac). También puede ser transitorio (InstancePerDependency en Autofac), pero el servicio será más eficaz en lo que respecta a la memoria si se usa la duración de ámbito.

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

El uso de la duración de singleton para el repositorio puede causar problemas de simultaneidad graves al establecer DbContext en una duración con ámbito (InstancePerLifetimeScope) (las duraciones predeterminadas para DBContext). Siempre que las duraciones del servicio para los repositorios y DbContext tengan establecido un ámbito, evitará estos problemas.

Recursos adicionales

Asignación de tabla

El mapeo de tablas identifica los datos de la tabla que se van a consultar y guardar en la base de datos. Anteriormente vio cómo se pueden usar las entidades de dominio (por ejemplo, un dominio de producto o pedido) para generar un esquema de base de datos relacionado. EF está fuertemente diseñado en torno al concepto de convenciones. Las convenciones abordan preguntas como "¿Cuál será el nombre de una tabla?" o "¿Qué propiedad es la clave principal?" Normalmente, las convenciones se basan en nombres convencionales. Por ejemplo, es habitual que la clave principal sea una propiedad que termine con Id.

Por convención, cada entidad se configurará para asignarse a una tabla que tenga el mismo nombre que la propiedad DbSet<TEntity> que expone la entidad en el contexto derivado. Si no se proporciona ningún DbSet<TEntity> valor para la entidad especificada, se usa el nombre de clase.

Anotaciones de datos frente a Fluent API

Hay muchas convenciones adicionales de EF Core y la mayoría de ellas se pueden cambiar mediante anotaciones de datos o fluent API, implementadas en el método OnModelCreating.

Las anotaciones de datos se deben usar en las propias clases del modelo de entidad, que es una forma más intrusiva desde un punto de vista DDD. Esto se debe a que está contaminando el modelo con anotaciones de datos relacionadas con la base de datos de infraestructura. Por otro lado, Fluent API es una manera cómoda de cambiar la mayoría de las convenciones y asignaciones dentro del nivel de infraestructura de persistencia de datos, por lo que el modelo de entidad se limpiará y desacoplará de la infraestructura de persistencia.

Fluent API y el método OnModelCreating

Como se mencionó, para cambiar las convenciones y mapeos, puede usar el método OnModelCreating en la clase DbContext.

El microservicio de ordenación en eShopOnContainers implementa configuraciones y asignaciones explícitas, cuando es necesario, tal y como se muestra en el código siguiente.

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

Puede establecer todas las asignaciones de la API de Fluent en el mismo OnModelCreating método, pero es aconsejable particionar ese código y tener varias clases de configuración, una por entidad, como se muestra en el ejemplo. Especialmente para modelos grandes, es aconsejable tener clases de configuración independientes para configurar diferentes tipos de entidad.

El código del ejemplo muestra algunas declaraciones y mapeos explícitos. Pero las convenciones de EF Core realizan muchas de esas asignaciones automáticamente, por lo que, en su caso, podría necesitar un código más pequeño.

Algoritmo Hi/Lo en EF Core

Un aspecto interesante del código en el ejemplo anterior es que usa el algoritmo Hi/Lo como estrategia de generación de claves.

El algoritmo Hi/Lo es útil cuando se necesitan claves únicas antes de confirmar los cambios. Como resumen, el algoritmo Hi-Lo asigna identificadores únicos a filas de tabla, aunque no depende de almacenar la fila en la base de datos inmediatamente. Esto le permite empezar a usar los identificadores inmediatamente, como sucede con identificadores de base de datos secuenciales normales.

El algoritmo Hi/Lo describe un mecanismo para obtener un lote de identificadores únicos de una secuencia de base de datos relacionada. Estos identificadores son seguros de usar porque la base de datos garantiza la unicidad, por lo que no habrá colisiones entre los usuarios. Este algoritmo es interesante por estas razones:

  • No interrumpe el patrón de la unidad de trabajo.

  • Obtiene identificadores de secuencia en lotes para minimizar los recorridos de ida y vuelta a la base de datos.

  • Genera un identificador legible para humanos, a diferencia de las técnicas que usan Identificadores Únicos Globales (GUID).

EF Core admite HiLo con el UseHiLo método , como se muestra en el ejemplo anterior.

Mapear campos en lugar de propiedades

Con esta característica, disponible desde EF Core 1.1, puede asignar directamente columnas a campos. Es posible no usar propiedades en la clase de entidad y simplemente asignar columnas de una tabla a campos. Un uso habitual de ello serían los campos privados para cualquier estado interno, al que no sea necesario acceder desde fuera de la entidad.

Puede hacerlo con campos individuales o también con colecciones, como un List<> campo. Este punto se mencionó anteriormente al hablar del modelado de las clases del modelo de dominio, pero aquí puede ver cómo se realiza ese mapeo con la configuración PropertyAccessMode.Field destacada en el código anterior.

Uso de propiedades reemplazadas en EF Core y ocultas en el nivel de infraestructura

Las "shadow properties" en EF Core son propiedades que no existen en el modelo de clase de entidad. Los valores y estados de estas propiedades se mantienen exclusivamente en la clase ChangeTracker en el nivel de infraestructura.

Implementación del patrón de especificación de consulta

Como se introdujo anteriormente en la sección de diseño, el patrón especificación de consulta es un patrón de diseño Domain-Driven diseñado como el lugar donde puede colocar la definición de una consulta con lógica de ordenación y paginación opcionales.

El patrón Especificación de consulta define una consulta en un objeto . Por ejemplo, para encapsular una consulta paginada que busque algunos productos, puede crear una especificación PagedProduct que tome los parámetros de entrada necesarios (pageNumber, pageSize, filter, etc.). A continuación, dentro de cualquier método del Repositorio (habitualmente como una sobrecarga de List()) se aceptaría un IQuerySpecification y se ejecutaría la consulta esperada basada en esa especificación.

Un ejemplo de una interfaz de especificación genérica es el código siguiente, que es similar al código usado en la aplicación de referencia 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; }
}

A continuación, la implementación de una clase base de especificación genérica es la siguiente.

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

La siguiente especificación carga una entidad de cesta única a partir del identificador de cesta o del identificador del comprador al que pertenece la cesta y realiza una carga diligente de la colección Items de la cesta.

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

Por último, puede ver cómo un repositorio EF genérico puede usar esta especificación para filtrar y cargar datos relacionados con un tipo de entidad determinado 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();
}

Además de encapsular la lógica de filtrado, la especificación puede especificar la forma de los datos que se van a devolver, incluidas las propiedades que se van a rellenar.

Aunque no se recomienda devolver IQueryable desde un repositorio, es perfectamente adecuado usarlos en el repositorio para crear un conjunto de resultados. Puede ver este enfoque usado en el método List anterior, que usa expresiones intermedias IQueryable para crear la lista de inclusión de la consulta antes de ejecutar la consulta con los criterios de la especificación en la última línea.

Obtenga información sobre cómo se aplica el patrón de especificación en el ejemplo eShopOnWeb.

Recursos adicionales