Compartir a través de


Novedades de EF Core 9

EF Core 9 (EF9) es la versión posterior a EF Core 8 y está programado que se lance en noviembre de 2024.

EF9 está disponible en forma de compilaciones diarias que contienen todas las características de EF9 y los ajustes de la API más recientes. Los ejemplos que se detallan aquí hacen uso de estas compilaciones diarias.

Sugerencia

Puede ejecutar y depurar los ejemplos descargando el código de ejemplo de GitHub. Cada sección siguiente vincula al código fuente específico de esa sección.

EF9 tiene como destino .NET 8 y, por tanto, se puede usar con .NET 8 (LTS) o .NET 9.

Sugerencia

Los documentos Novedades se actualizan para cada versión preliminar. Todas las muestras están configuradas para usar las compilaciones diarias de EF9, que normalmente tienen varias semanas adicionales de trabajo completado en comparación con la versión preliminar más reciente. Recomendamos, encarecidamente, el uso de las compilaciones diarias al probar nuevas características para que no realice las pruebas con bits obsoletos.

Azure Cosmos DB para NoSQL

EF 9.0 aporta mejoras sustanciales al proveedor EF Core para Azure Cosmos DB; se han reescrito partes significativas del proveedor para proporcionar nuevas funcionalidades, permitir nuevas formas de consulta y alinear mejor el proveedor con las mejores prácticas de Azure Cosmos DB. Las principales mejoras de alto nivel se enumeran a continuación; para obtener una lista completa, consulte esta edición épica.

Advertencia

Como parte de las mejoras introducidas en el proveedor, se han tenido que realizar una serie de cambios importantes de alto impacto. Si está actualizando una aplicación existente, lea atentamente la sección de cambios importantes.

Mejoras en la consulta con claves de partición e identificadores de documento

Cada documento almacenado en la base de datos de Azure Cosmos DB tiene un identificador de recurso único. Además, cada documento puede contener una "clave de partición" que determina la creación de particiones lógica de los datos de forma que la base de datos se pueda escalar de forma eficaz. Puede encontrar más información sobre cómo elegir claves de partición en Creación de particiones y escalado horizontal en Azure Cosmos DB.

En EF 9.0, el proveedor de Azure Cosmos DB identifica mejor las comparaciones de claves de partición en las consultas LINQ y las extrae para que las consultas solo se envíen a la partición pertinente; esto puede mejorar considerablemente el rendimiento de las consultas y reducir los costes RU. Por ejemplo:

var sessions = await context.Sessions
    .Where(b => b.PartitionKey == "someValue" && b.Username.StartsWith("x"))
    .ToListAsync();

En esta consulta, el proveedor reconoce automáticamente la comparación en PartitionKey. Si examinamos los registros, veremos lo siguiente:

Executed ReadNext (189.8434 ms, 2.8 RU) ActivityId='8cd669ed-2ca5-4f2b-8923-338899071361', Container='test', Partition='["someValue"]', Parameters=[]
SELECT VALUE c
FROM root c
WHERE STARTSWITH(c["Username"], "x")

Tenga en cuenta que la cláusula WHERE no contiene PartitionKey: esa comparación se ha "quitado" y se usa para ejecutar la consulta solo contra la partición relevante. En versiones anteriores, la comparación se dejaba en la cláusula WHERE en muchas situaciones, provocando que la consulta se ejecutara contra todas las particiones y dando lugar a un aumento de los costes y una reducción del rendimiento.

Además, si su consulta también proporciona un valor para la propiedad ID del documento, y no incluye ninguna otra operación de consulta, el proveedor puede aplicar una optimización adicional:

var somePartitionKey = "someValue";
var someId = 8;
var sessions = await context.Sessions
    .Where(b => b.PartitionKey == somePartitionKey && b.Id == someId)
    .SingleAsync();

Los registros muestran lo siguiente para esta consulta:

Executed ReadItem (73 ms, 1 RU) ActivityId='13f0f8b8-d481-47f0-bf41-67f7deb008b2', Container='test', Id='8', Partition='["someValue"]'

En este caso, no se envía ninguna consulta SQL. En su lugar, el proveedor realiza una lectura puntual (API ReadItem) extremadamente eficiente, que obtiene directamente el documento dada la clave de partición y el ID. Este es el tipo de lectura más eficiente y rentable que puedes realizar en Azure Cosmos DB; consulte la documentación de Azure Cosmos DB para obtener más información sobre las lecturas puntuales.

Para obtener más información sobre las consultas con claves de partición y lecturas puntuales, consulte la página de documentación sobre consultas.

Claves de partición jerárquicas

Sugerencia

El código que se muestra aquí procede de HierarchicalPartitionKeysSample.cs.

Azure Cosmos DB era compatible originalmente con una única clave de partición, pero desde entonces ha ampliado las capacidades de partición para ser también compatible con la subpartición mediante la especificación de hasta tres niveles de jerarquía en la clave de partición. EF Core 9 ofrece compatibilidad total con claves de partición jerárquicas, lo que permite aprovechar el mejor rendimiento y el ahorro de costes asociados a esta función.

Las claves de partición se especifican mediante la API de creación de modelos, normalmente en DbContext.OnModelCreating. Debe haber una propiedad asignada en el tipo de entidad para cada nivel de la clave de partición. Por ejemplo, considere un tipo de entidad UserSession:

public class UserSession
{
    // Item ID
    public Guid Id { get; set; }

    // Partition Key
    public string TenantId { get; set; } = null!;
    public Guid UserId { get; set; }
    public int SessionId { get; set; }

    // Other members
    public string Username { get; set; } = null!;
}

El código siguiente especifica una clave de partición de tres niveles mediante las propiedades TenantId, UserId y SessionId:

modelBuilder
    .Entity<UserSession>()
    .HasPartitionKey(e => new { e.TenantId, e.UserId, e.SessionId });

Sugerencia

Esta definición de clave de partición sigue el ejemplo proporcionado en Elección de las claves de partición jerárquicas de la documentación de Azure Cosmos DB.

Observe cómo, a partir de EF Core 9, las propiedades de cualquier tipo asignado se pueden usar en la clave de partición. Para los tipos bool y numéricos, como la propiedad int SessionId, el valor se usa directamente en la clave de partición. Otros tipos, como la propiedad Guid UserId, se convierten automáticamente en cadenas.

Al realizar consultas, EF extrae automáticamente los valores de la clave de partición de las consultas y los aplica a la API de consultas de Azure Cosmos DB para garantizar que las consultas se limitan adecuadamente al menor número de particiones posible. Por ejemplo, considere la siguiente consulta LINQ que proporciona los tres valores de clave de partición de la jerarquía:

var tenantId = "Microsoft";
var sessionId = 7;
var userId = new Guid("99A410D7-E467-4CC5-92DE-148F3FC53F4C");

var sessions = await context.Sessions
    .Where(
        e => e.TenantId == tenantId
             && e.UserId == userId
             && e.SessionId == sessionId
             && e.Username.Contains("a"))
    .ToListAsync();

Al ejecutar esta consulta, EF Core extraerá los valores de los parámetros tenantId, userId y sessionId y los pasará a la API de consulta de Azure Cosmos DB como valor de la clave de partición Por ejemplo, consulte los registros de la ejecución de la consulta anterior:

info: 6/10/2024 19:06:00.017 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing SQL query for container 'UserSessionContext' in partition '["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]' [Parameters=[]]
      SELECT c
      FROM root c
      WHERE ((c["Discriminator"] = "UserSession") AND CONTAINS(c["Username"], "a"))

Observe que las comparaciones de claves de partición se han eliminado de la cláusula WHERE, y en su lugar se usan como clave de partición para una ejecución eficiente: ["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0].

Para más información, consulte la documentación sobre consultas con claves de partición.

Mejora significativa de las capacidades de consulta LINQ

En EF 9.0, las capacidades de traducción LINQ del proveedor Azure Cosmos DB se han ampliado considerablemente y el proveedor puede ejecutar ahora muchos más tipos de consulta. La lista completa de mejoras en las consultas es demasiado larga para enumerarla, pero aquí están los aspectos más destacados:

  • Compatibilidad total con las colecciones primitivas de EF, lo que permite realizar consultas LINQ en colecciones de, por ejemplo, ints o cadenas. Para obtener más información, consulte Novedades de EF8: colecciones primitivas.
  • Soporte para consultas arbitrarias sobre colecciones no primitivas.
  • Ahora es compatible con muchos operadores LINQ adicionales: indexación en colecciones, Length/Count, ElementAt, Contains y muchos otros.
  • Soporte para operadores agregados como Count y Sum.
  • Se han añadido muchas traducciones adicionales de funciones (consulte la documentación de mapeos de funciones para ver la lista completa de la compatibilidad de traducciones):
    • Traducciones para miembros de componentes DateTime y DateTimeOffset (DateTime.Year, DateTimeOffset.Month...).
    • EF.Functions.IsDefined y EF.Functions.CoalesceUndefined ahora permiten trabajar con valores undefined.
    • string.Contains, StartsWith y EndsWith ahora son compatibles con StringComparison.OrdinalIgnoreCase.

Para ver la lista completa de mejoras en las consultas, consulte esta edición:

Modelado mejorado alineado con los estándares Azure Cosmos DB y JSON

EF 9.0 mapea los documentos de Azure Cosmos DB de forma más natural para una base de datos de documentos basada en JSON, y ayuda a interoperar con otros sistemas que acceden a sus documentos. Aunque esto implica cambios importantes, existen API que permiten volver al comportamiento anterior a la versión 9.0 en todos los casos.

Propiedades id simplificadas sin discriminadores

En primer lugar, las versiones anteriores de EF insertaban el valor del discriminador en la propiedad JSON id, produciendo documentos como los siguientes:

{
    "id": "Blog|1099",
    ...
}

Esto se hacía para permitir la existencia de documentos de diferentes tipos (por ejemplo, Blog y Post) y el mismo valor clave (1099) dentro de la misma partición contenedora. A partir de EF 9.0, la propiedad id contiene solo el valor clave:

{
    "id": 1099,
    ...
}

Esta es una forma más natural de mapear a JSON, y hace que sea más fácil para las herramientas y sistemas externos interactuar con los documentos JSON generados por EF; tales sistemas externos generalmente no son conscientes de los valores discriminantes de EF, que son por defecto derivados de tipos .NET.

Tenga en cuenta que se trata de un cambio importante, ya que EF ya no podrá consultar documentos existentes con el formato antiguo id. Se ha introducido una API para volver al comportamiento anterior; consulte la nota de cambio importante y la documentación para obtener más detalles.

Se ha cambiado el nombre de la propiedad Discriminador a $type

La propiedad discriminador predeterminada se denominaba anteriormente Discriminator. EF 9.0 cambia el valor predeterminado a $type:

{
    "id": 1099,
    "$type": "Blog",
    ...
}

Esto sigue el estándar emergente para el polimorfismo JSON, permitiendo una mejor interoperabilidad con otras herramientas. Por ejemplo, System.Text.Json de .NET también soporta polimorfismo, usando $type como discriminador por defecto el nombre de la propiedad (docs).

Tenga en cuenta que se trata de un cambio importante, ya que EF ya no podrá consultar documentos existentes con el antiguo nombre de propiedad discriminador. Consulte la nota de cambio importante para obtener más información sobre cómo volver a la denominación anterior.

Búsqueda de similitud vectorial (vista previa)

Azure Cosmos DB ofrece ahora soporte de vista previa para la búsqueda de similitud vectorial. El vector de búsqueda es una parte fundamental de algunos tipos de aplicaciones, incluye IA, la búsqueda semántica y otras. Azure Cosmos DB le permite almacenar vectores directamente en sus documentos junto con el resto de sus datos, lo que significa que puede realizar todas sus consultas en una única base de datos. Esto puede simplificar considerablemente su arquitectura y eliminar la necesidad de una solución de base de datos de vectores adicional y dedicada en su pila. Para obtener más información sobre el vector de búsqueda en Azure Cosmos DB, consulte la documentación.

Una vez que su contenedor Azure Cosmos DB esté configurado correctamente, usar el vector de búsqueda a través de EF es una simple cuestión de agregar una propiedad vectorial y configurarla:

public class Blog
{
    ...

    public float[] Vector { get; set; }
}

public class BloggingContext
{
    ...

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .Property(b => b.Embeddings)
            .IsVector(DistanceFunction.Cosine, dimensions: 1536);
    }
}

Una vez hecho esto, usa la función EF.Functions.VectorDistance() en las consultas LINQ para realizar la búsqueda de similitud vectorial:

var blogs = await context.Blogs
    .OrderBy(s => EF.Functions.VectorDistance(s.Vector, vector))
    .Take(5)
    .ToListAsync();

Para más información, consulta la Documentación sobre vector de búsqueda.

Compatibilidad con la paginación

El proveedor Azure Cosmos DB permite ahora paginar los resultados de las consultas mediante tokens de continuación, lo que resulta mucho más eficiente y rentable que el uso tradicional de Skip y Take:

var firstPage = await context.Posts
    .OrderBy(p => p.Id)
    .ToPageAsync(pageSize: 10, continuationToken: null);

var continuationToken = firstPage.ContinuationToken;
foreach (var post in page.Values)
{
    // Display/send the posts to the user
}

El operador nuevo ToPageAsync devuelve un CosmosPage, que expone un token de continuación que puede usarse para reanudar eficientemente la consulta en un punto posterior, obteniendo los 10 elementos siguientes:

var nextPage = await context.Sessions.OrderBy(s => s.Id).ToPageAsync(10, continuationToken);

Para más información, consulte la sección de documentación sobre paginación.

FromSql para consultas SQL más seguras

El proveedor Azure Cosmos DB ha permitido la consulta SQL a través de FromSqlRaw. Sin embargo, esa API puede ser susceptible a ataques de inyección SQL cuando los datos proporcionados por el usuario se interpolan o concatenan en el SQL. En EF 9.0, ahora se puede usar el nuevo método FromSql, que siempre integra datos parametrizados como un parámetro fuera del SQL:

var maxAngle = 8;
_ = await context.Blogs
    .FromSql($"SELECT VALUE c FROM root c WHERE c.Angle1 <= {maxAngle}")
    .ToListAsync();

Para más información, consulte la sección de documentación sobre paginación.

Acceso basado en roles

Azure Cosmos DB for NoSQL incluye un sistema de control de acceso basado en rol (RBAC) integrado. Esto es ahora soportado por EF9 para todas las operaciones del plano de datos. Sin emabargo, Azure Cosmos DB SDK no es compatible con RBAC para las operaciones del plano de administración en Azure Cosmos DB. Use Azure Management API en lugar de EnsureCreatedAsync con RBAC.

La E/S sincrónica ahora está bloqueada de forma predeterminada

Azure Cosmos DB para NoSQL no admite API sincrónicas (de bloqueo) desde el código de la aplicación. Anteriormente, EF enmascaraba esta situación bloqueando las llamadas asíncronas. Sin embargo, esto fomenta el uso de E/S sincrónicas, lo cual es una mala práctica, y puede provocar bloqueos. Por lo tanto, a partir de EF 9, se lanza una excepción cuando se intenta el acceso sincrónico. Por ejemplo:

Por ahora, la E/S sincrónica puede seguir utilizándose configurando el nivel de advertencia adecuadamente. Por ejemplo, en OnConfiguring; en el tipo de DbContext:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.ConfigureWarnings(b => b.Ignore(CosmosEventId.SyncNotSupported));

Tenga en cuenta, sin embargo, que tenemos previsto eliminar por completo el soporte de sincronización en EF 11, así que empiece a actualizarse para usar métodos asincrónicos como ToListAsync y SaveChangesAsync tan pronto como sea posible.

Consultas AOT y precompiladas

Advertencia

NativeAOT y precompilación de consultas son características muy experimentales y aún no son adecuadas para su uso en producción. La compatibilidad descrita a continuación debe verse como infraestructura para la característica final, que probablemente se publicará con EF 10. Le animamos a experimentar con el soporte técnico actual e informar sobre sus experiencias, pero se recomienda implementar aplicaciones EF NativeAOT en producción.

EF 9.0 ofrece compatibilidad inicial y experimental con .NET NativeAOT, lo que permite la publicación de aplicaciones compiladas de antemano que usan EF para acceder a las bases de datos. Para admitir consultas LINQ en modo NativeAOT, EF se basa en la precompilación de consultas: este mecanismo identifica estáticamente las consultas LINQ de EF y genera interceptores de C#, que contienen código para ejecutar cada consulta específica. Esto puede reducir significativamente el tiempo de inicio de la aplicación, ya que el trabajo pesado del procesamiento y la compilación de las consultas LINQ en SQL ya no se produce cada vez que se inicia la aplicación. En su lugar, el interceptor de cada consulta contiene el SQL finalizado para esa consulta, así como el código optimizado para materializar los resultados de la base de datos como objetos .NET.

Por ejemplo, dado un programa con la consulta EF siguiente:

var blogs = await context.Blogs.Where(b => b.Name == "foo").ToListAsync();

EF generará un interceptor de C# en el proyecto, que tomará el control de la ejecución de la consulta. En lugar de procesar la consulta y traducirla a SQL cada vez que se inicia el programa, el interceptor tiene insertado sql directamente en ella (para SQL Server en este caso), lo que permite que el programa se inicie mucho más rápido:

var relationalCommandTemplate = ((IRelationalCommandTemplate)(new RelationalCommand(materializerLiftableConstantContext.CommandBuilderDependencies, "SELECT [b].[Id], [b].[Name]\nFROM [Blogs] AS [b]\nWHERE [b].[Name] = N'foo'", new IRelationalParameter[] { })));

Además, el mismo interceptor contiene código para materializar el objeto .NET a partir de los resultados de la base de datos:

var instance = new Blog();
UnsafeAccessor_Blog_Id_Set(instance) = dataReader.GetInt32(0);
UnsafeAccessor_Blog_Name_Set(instance) = dataReader.GetString(1);

Esto usa otra nueva característica de .NET: descriptores de acceso no seguros, para insertar datos de la base de datos en los campos privados del objeto.

Si está interesado en NativeAOT y le gusta experimentar con características de vanguardia, pruebe esto. Tenga en cuenta que la característica debe considerarse inestable y actualmente tiene muchas limitaciones; esperamos estabilizarlo y hacer que sea más adecuado para el uso de producción en EF 10.

Consulte la página de documentación de NativeAOT para obtener más detalles.

Traducción de LINQ y SQL

Como en cada versión, EF9 incluye un gran número de mejoras en las capacidades de consulta LINQ. Se pueden traducir nuevas consultas, y se han mejorado muchas traducciones SQL para los escenarios soportados, tanto para mejorar el rendimiento como la legibilidad.

El número de mejoras es demasiado grande para enumerarlas todas aquí. A continuación, se destacan algunas de las mejoras más importantes; consulte esta edición para obtener una lista más completa del trabajo realizado en la versión 9.0.

Nos gustaría agradecer a Andrea Canciani (@ranma42) sus numerosas contribuciones de gran calidad para optimizar el SQL generado por EF Core.

Tipos complejos: Soporte para GroupBy y ExecuteUpdate

GroupBy

Sugerencia

El código que se muestra aquí procede de ComplexTypesSample.cs.

EF9 admite la agrupación por una instancia de tipo complejo. Por ejemplo:

var groupedAddresses = await context.Stores
    .GroupBy(b => b.StoreAddress)
    .Select(g => new { g.Key, Count = g.Count() })
    .ToListAsync();

EF traduce esto como agrupación por cada miembro del tipo complejo, que se alinea con la semántica de tipos complejos como objetos de valor. Por ejemplo, en Azure SQL:

SELECT [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode], COUNT(*) AS [Count]
FROM [Stores] AS [s]
GROUP BY [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode]

ExecuteUpdate

Sugerencia

El código que se muestra aquí procede de ExecuteUpdateSample.cs.

Del mismo modo, en EF9 ExecuteUpdate también se ha mejorado para aceptar propiedades de tipo complejo. Sin embargo, cada miembro del tipo complejo debe especificarse explícitamente. Por ejemplo:

var newAddress = new Address("Gressenhall Farm Shop", null, "Beetley", "Norfolk", "NR20 4DR");

await context.Stores
    .Where(e => e.Region == "Germany")
    .ExecuteUpdateAsync(s => s.SetProperty(b => b.StoreAddress, newAddress));

Esto genera SQL que actualiza cada columna asignada al tipo complejo:

UPDATE [s]
SET [s].[StoreAddress_City] = @__complex_type_newAddress_0_City,
    [s].[StoreAddress_Country] = @__complex_type_newAddress_0_Country,
    [s].[StoreAddress_Line1] = @__complex_type_newAddress_0_Line1,
    [s].[StoreAddress_Line2] = NULL,
    [s].[StoreAddress_PostCode] = @__complex_type_newAddress_0_PostCode
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Germany'

Antes, había que enumerar manualmente las distintas propiedades del tipo complejo en la llamada ExecuteUpdate.

Eliminación de elementos innecesarios de SQL

Anteriormente, EF producía a veces SQL que contenía elementos que no eran realmente necesarios. En la mayoría de los casos, estos eran posiblemente necesarios en una etapa anterior del procesamiento SQL, y se dejaban atrás. EF9 ahora elimina la mayoría de estos elementos, lo que da como resultado un SQL más compacto y, en algunos casos, más eficaz.

Eliminación de tablas

Como primer ejemplo, el SQL generado por EF a veces contenía JOIN en tablas que no eran realmente necesarias en la consulta. Consideremos el siguiente modelo, que usa la asignación de herencia tabla por tipo (TPT):

public class Order
{
    public int Id { get; set; }
    ...

    public Customer Customer { get; set; }
}

public class DiscountedOrder : Order
{
    public double Discount { get; set; }
}

public class Customer
{
    public int Id { get; set; }
    ...

    public List<Order> Orders { get; set; }
}

public class BlogContext : DbContext
{
    ...

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Order>().UseTptMappingStrategy();
    }
}

Si a continuación ejecutamos la siguiente consulta para obtener todos los Clientes con al menos un Pedido:

var customers = await context.Customers.Where(o => o.Orders.Any()).ToListAsync();

EF8 genera el siguiente SQL:

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
    SELECT 1
    FROM [Orders] AS [o]
    LEFT JOIN [DiscountedOrders] AS [d] ON [o].[Id] = [d].[Id]
    WHERE [c].[Id] = [o].[CustomerId])

Tenga en cuenta que la consulta contenía una unión a la tabla DiscountedOrders aunque no se hiciera referencia a ninguna columna en ella. EF9 genera un SQL eliminado sin la unión:

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
    SELECT 1
    FROM [Orders] AS [o]
    WHERE [c].[Id] = [o].[CustomerId])

Eliminación de proyección

De forma similar, examinemos la siguiente consulta:

var orders = await context.Orders
    .Where(o => o.Amount > 10)
    .Take(5)
    .CountAsync();

En EF8, esta consulta generó el siguiente SQL:

SELECT COUNT(*)
FROM (
    SELECT TOP(@__p_0) [o].[Id]
    FROM [Orders] AS [o]
    WHERE [o].[Amount] > 10
) AS [t]

Tenga en cuenta que la proyección [o].[Id] no es necesaria en la subconsulta, ya que la expresión SELECT externa simplemente cuenta las filas. EF9 genera lo siguiente en su lugar:

SELECT COUNT(*)
FROM (
    SELECT TOP(@__p_0) 1 AS empty
    FROM [Orders] AS [o]
    WHERE [o].[Amount] > 10
) AS [s]

... y la proyección está vacía. Esto puede no parecer gran cosa, pero puede simplificar significativamente el SQL en algunos casos. Le invitamos a desplazarse por algunos de los cambios de SQL en las pruebas para ver el efecto.

Traducciones que implican GREATEST/LEAST

Sugerencia

El código que se muestra aquí procede de LeastGreatestSample.cs.

Se han introducido varias traducciones nuevas que usan las funciones GREATEST y LEAST de SQL.

Importante

Las funciones de GREATEST y LEAST se introdujeron en las bases de datos SQL Server/Azure SQL en la versión de 2022. Visual Studio 2022 instala SQL Server 2019 de forma predeterminada. Se recomienda instalar SQL Server Developer Edition 2022 para probar estas nuevas traducciones en EF9.

Por ejemplo, las consultas que usan Math.Max o Math.Min ahora se traducen para Azure SQL mediante GREATEST y LEAST respectivamente. Por ejemplo:

var walksUsingMin = await context.Walks
    .Where(e => Math.Min(e.DaysVisited.Count, e.ClosestPub.Beers.Length) > 4)
    .ToListAsync();

Esta consulta se traduce al siguiente SQL cuando se usa EF9 ejecutándose con SQL Server 2022:

SELECT [w].[Id], [w].[ClosestPubId], [w].[DaysVisited], [w].[Name], [w].[Terrain]
FROM [Walks] AS [w]
INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id]
WHERE LEAST((
    SELECT COUNT(*)
    FROM OPENJSON([w].[DaysVisited]) AS [d]), (
    SELECT COUNT(*)
    FROM OPENJSON([p].[Beers]) AS [b])) >

Math.Min y Math.Max también se pueden usar en los valores de una colección primitiva. Por ejemplo:

var pubsInlineMax = await context.Pubs
    .SelectMany(e => e.Counts)
    .Where(e => Math.Max(e, threshold) > top)
    .ToListAsync();

Esta consulta se traduce al siguiente SQL cuando se usa EF9 ejecutándose con SQL Server 2022:

SELECT [c].[value]
FROM [Pubs] AS [p]
CROSS APPLY OPENJSON([p].[Counts]) WITH ([value] int '$') AS [c]
WHERE GREATEST([c].[value], @__threshold_0) > @__top_1

Por último, RelationalDbFunctionsExtensions.Least y RelationalDbFunctionsExtensions.Greatest se pueden usar para invocar directamente la función Least o Greatest en SQL. Por ejemplo:

var leastCount = await context.Pubs
    .Select(e => EF.Functions.Least(e.Counts.Length, e.DaysVisited.Count, e.Beers.Length))
    .ToListAsync();

Esta consulta se traduce al siguiente SQL cuando se usa EF9 ejecutándose con SQL Server 2022:

SELECT LEAST((
    SELECT COUNT(*)
    FROM OPENJSON([p].[Counts]) AS [c]), (
    SELECT COUNT(*)
    FROM OPENJSON([p].[DaysVisited]) AS [d]), (
    SELECT COUNT(*)
    FROM OPENJSON([p].[Beers]) AS [b]))
FROM [Pubs] AS [p]

Forzar o impedir la parametrización de consultas

Sugerencia

El código que se muestra aquí procede de QuerySample.cs.

Excepto en algunos casos especiales, EF Core parametriza las variables usadas en una consulta LINQ, pero incluye constantes en el SQL generado. Por ejemplo, considere el siguiente método de consulta:

async Task<List<Post>> GetPosts(int id)
    => await context.Posts
        .Where(e => e.Title == ".NET Blog" && e.Id == id)
        .ToListAsync();

Esto se traduce en los siguientes parámetros y SQL cuando se usa Azure SQL:

Executed DbCommand (1ms) [Parameters=[@__id_0='1'], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] = @__id_0

Observe que EF creó una constante en el blog de SQL para ".NET" porque este valor no cambiará de consulta a consulta. El uso de una constante permite que el motor de base de datos examine este valor al crear un plan de consulta, lo que podría dar lugar a una consulta más eficaz.

Por otro lado, el valor de id se parametriza, ya que la misma consulta se puede ejecutar con muchos valores diferentes para id. La creación de una constante en este caso daría lugar a la contaminación de la caché de consultas con un montón de consultas que solo difieren en los valores id. Esto es muy malo para el rendimiento general de la base de datos.

Por lo general, estos valores predeterminados no deben cambiarse. Sin embargo, EF Core 8.0.2 introduce un método EF.Constant que obliga a EF a usar una constante incluso si se usara un parámetro de manera predeterminada. Por ejemplo:

async Task<List<Post>> GetPostsForceConstant(int id)
    => await context.Posts
        .Where(e => e.Title == ".NET Blog" && e.Id == EF.Constant(id))
        .ToListAsync();

La traducción ahora contiene una constante para el valor de id:

Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] = 1

El método EF.Parameter

EF9 presenta el método EF.Parameter para hacer lo contrario. Es decir, forzar a EF a usar un parámetro aunque el valor sea una constante en el código. Por ejemplo:

async Task<List<Post>> GetPostsForceParameter(int id)
    => await context.Posts
        .Where(e => e.Title == EF.Parameter(".NET Blog") && e.Id == id)
        .ToListAsync();

La traducción ahora contiene un parámetro para la cadena ".NET Blog":

Executed DbCommand (1ms) [Parameters=[@__p_0='.NET Blog' (Size = 4000), @__id_1='1'], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = @__p_0 AND [p].[Id] = @__id_1

Colecciones primitivas parametrizadas

EF8 ha cambiado la forma en que se traducen algunas consultas que utilizan colecciones primitivas. Cuando una consulta LINQ contiene una colección primitiva parametrizada, EF convierte su contenido a JSON y lo pasa como valor de parámetro único la consulta:

async Task<List<Post>> GetPostsPrimitiveCollection(int[] ids)
    => await context.Posts
        .Where(e => e.Title == ".NET Blog" && ids.Contains(e.Id))
        .ToListAsync();

Esto dará como resultado la siguiente traducción en SQL Server:

Executed DbCommand (5ms) [Parameters=[@__ids_0='[1,2,3]' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] IN (
    SELECT [i].[value]
    FROM OPENJSON(@__ids_0) WITH ([value] int '$') AS [i]
)

Esto permite tener la misma consulta SQL para diferentes colecciones parametrizadas (solo cambia el valor del parámetro), pero en algunas situaciones puede dar lugar a problemas de rendimiento ya que la base de datos no es capaz de planificar de forma óptima la consulta. El método EF.Constant se puede utilizar para volver a la traducción anterior.

La siguiente consulta usa EF.Constant para ello:

async Task<List<Post>> GetPostsForceConstantCollection(int[] ids)
    => await context.Posts
        .Where(
            e => e.Title == ".NET Blog" && EF.Constant(ids).Contains(e.Id))
        .ToListAsync();

El SQL resultante es el siguiente:

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] IN (1, 2, 3)

Además, EF9 introduce TranslateParameterizedCollectionsToConstants, la opción de contexto que puede utilizarse para impedir la parametrización de colecciones primitivas para todas las consultas. También hemos añadido un complemento TranslateParameterizedCollectionsToParameters que fuerza la parametrización de colecciones primitivas explícitamente (este es el comportamiento por defecto).

Sugerencia

El método EF.Parameter anula la opción de contexto. Si quiere evitar la parametrización de colecciones primitivas para la mayoría de sus consultas (pero no para todas), puede establecer la opción de contexto TranslateParameterizedCollectionsToConstants y usar EF.Parameter para las consultas o variables individuales que quiera parametrizar.

Subconsultas no correlacionadas insertadas

Sugerencia

El código que se muestra aquí procede de QuerySample.cs.

En EF8, se puede ejecutar una IQueryable a la que se hace referencia en otra consulta como un recorrido de ida y vuelta de base de datos separado. Por ejemplo, considere la siguiente consulta LINQ:

var dotnetPosts = context
    .Posts
    .Where(p => p.Title.Contains(".NET"));

var results = dotnetPosts
    .Where(p => p.Id > 2)
    .Select(p => new { Post = p, TotalCount = dotnetPosts.Count() })
    .Skip(2).Take(10)
    .ToArray();

En EF8, la consulta de dotnetPosts se ejecuta como un recorrido de ida y vuelta y, a continuación, los resultados finales se ejecutan como segunda consulta. Por ejemplo, en SQL Server:

SELECT COUNT(*)
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%'

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2
ORDER BY (SELECT 1)
OFFSET @__p_1 ROWS FETCH NEXT @__p_2 ROWS ONLY

En EF9, el IQueryable se alinea con dotnetPosts, resultando en un único viaje de ida y vuelta a la base de datos:

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata], (
    SELECT COUNT(*)
    FROM [Posts] AS [p0]
    WHERE [p0].[Title] LIKE N'%.NET%')
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2
ORDER BY (SELECT 1)
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY

Funciones agregadas sobre subconsultas y agregados en SQL Server

EF9 mejora la traducción de algunas consultas complejas utilizando funciones agregadas compuestas sobre subconsultas u otras funciones agregadas. A continuación se muestra un ejemplo de este tipo de consultas:

var latestPostsAverageRatingByLanguage = await context.Blogs
    .Select(x => new
    {
        x.Language,
        LatestPostRating = x.Posts.OrderByDescending(xx => xx.PublishedOn).FirstOrDefault()!.Rating
    })
    .GroupBy(x => x.Language)
    .Select(x => x.Average(xx => xx.LatestPostRating))
    .ToListAsync();

En primer lugar, Select computa LatestPostRating por cada Post de las que requieren una subconsulta al traducirse a SQL. Más tarde en la consulta estos resultados se agregan utilizando la operación Average. El SQL resultante tiene el siguiente aspecto cuando se ejecuta en SQL Server:

SELECT AVG([s].[Rating])
FROM [Blogs] AS [b]
OUTER APPLY (
    SELECT TOP(1) [p].[Rating]
    FROM [Posts] AS [p]
    WHERE [b].[Id] = [p].[BlogId]
    ORDER BY [p].[PublishedOn] DESC
) AS [s]
GROUP BY [b].[Language]

En versiones anteriores EF Core generaba SQL inválido para consultas similares, intentando aplicar la operación de agregado directamente sobre la subconsulta. Esto no está permitido en SQL Server y da lugar a una excepción. El mismo principio se aplica a las consultas que utilizan agregados sobre otros agregados:

var topRatedPostsAverageRatingByLanguage = await context.Blogs.
Select(x => new
{
    x.Language,
    TopRating = x.Posts.Max(x => x.Rating)
})
.GroupBy(x => x.Language)
.Select(x => x.Average(xx => xx.TopRating))
.ToListAsync();

Nota:

Este cambio no afecta a Sqlite, que soporta agregados sobre subconsultas (u otros agregados) y no soporta LATERAL JOIN (APPLY). A continuación se muestra el SQL de la primera consulta ejecutada en Sqlite:

SELECT ef_avg((
    SELECT "p"."Rating"
    FROM "Posts" AS "p"
    WHERE "b"."Id" = "p"."BlogId"
    ORDER BY "p"."PublishedOn" DESC
    LIMIT 1))
FROM "Blogs" AS "b"
GROUP BY "b"."Language"

Las consultas que usan Count != 0 están optimizadas

Sugerencia

El código que se muestra aquí procede de QuerySample.cs.

En EF8, la siguiente consulta LINQ se traducía para usar la función COUNT de SQL:

var blogsWithPost = await context.Blogs
    .Where(b => b.Posts.Count > 0)
    .ToListAsync();

Ahora, EF9 genera una traducción más eficaz mediante EXISTS:

SELECT "b"."Id", "b"."Name", "b"."SiteUri"
FROM "Blogs" AS "b"
WHERE EXISTS (
    SELECT 1
    FROM "Posts" AS "p"
    WHERE "b"."Id" = "p"."BlogId")

Semántica de C# para operaciones de comparación de valores null

En EF8, las comparaciones entre elementos anulables no se realizaban correctamente en algunos casos. En C#, si uno o ambos operandos son null el resultado de una operación de comparación es falso; en caso contrario, se comparan los valores contenidos de los operandos. En EF8 solíamos traducir las comparaciones usando la semántica null de la base de datos. Esto producía resultados diferentes a una consulta similar usando LINQ to Objects. Además, se obtenían resultados diferentes cuando la comparación se realizaba en el filtro o en la proyección. Algunas consultas también producirían resultados diferentes entre Sql Server y Sqlite/Postgres.

Por ejemplo, observe la consulta siguiente:

var negatedNullableComparisonFilter = await context.Entities
    .Where(x => !(x.NullableIntOne > x.NullableIntTwo))
    .Select(x => new { x.NullableIntOne, x.NullableIntTwo }).ToListAsync();

generaría el siguiente SQL:

SELECT [e].[NullableIntOne], [e].[NullableIntTwo]
FROM [Entities] AS [e]
WHERE NOT ([e].[NullableIntOne] > [e].[NullableIntTwo])

que filtra las entidades cuyo NullableIntOne o NullableIntTwo es null.

En EF9 se produce:

SELECT [e].[NullableIntOne], [e].[NullableIntTwo]
FROM [Entities] AS [e]
WHERE CASE
    WHEN [e].[NullableIntOne] > [e].[NullableIntTwo] THEN CAST(0 AS bit)
    ELSE CAST(1 AS bit)
END = CAST(1 AS bit)

Una comparación similar realizada en una proyección:

var negatedNullableComparisonProjection = await context.Entities.Select(x => new
{
    x.NullableIntOne,
    x.NullableIntTwo,
    Operation = !(x.NullableIntOne > x.NullableIntTwo)
}).ToListAsync();

daría como resultado el siguiente SQL:

SELECT [e].[NullableIntOne], [e].[NullableIntTwo], CASE
    WHEN NOT ([e].[NullableIntOne] > [e].[NullableIntTwo]) THEN CAST(1 AS bit)
    ELSE CAST(0 AS bit)
END AS [Operation]
FROM [Entities] AS [e]

que devuelve false para las entidades cuyos NullableIntOne o NullableIntTwo son null (en lugar de true esperado en C#). Ejecutando el mismo escenario en Sqlite generado:

SELECT "e"."NullableIntOne", "e"."NullableIntTwo", NOT ("e"."NullableIntOne" > "e"."NullableIntTwo") AS "Operation"
FROM "Entities" AS "e"

que da lugar a una excepción Nullable object must have a value, ya que la traducción produce un valor null para los casos en los que NullableIntOne o NullableIntTwo son null.

EF9 ahora maneja correctamente estos escenarios, produciendo resultados consistentes con LINQ to Objects y a través de diferentes proveedores.

Esta mejora ha sido realizada por @ranma42. Muchas gracias.

Traducción de operadores LINQ Order y OrderDescending

EF9 permite la traducción de las operaciones de ordenación simplificadas LINQ (Order y OrderDescending). Funcionan de forma similar a OrderBy/OrderByDescending, pero no requieren un argumento. En su lugar, aplican un orden por defecto; para entidades esto significa un orden basado en valores de clave primaria y para otros tipos, un orden basado en los propios valores.

A continuación se muestra un ejemplo de consulta que aprovecha los operadores de ordenación simplificados:

var orderOperation = await context.Blogs
    .Order()
    .Select(x => new
    {
        x.Name,
        OrderedPosts = x.Posts.OrderDescending().ToList(),
        OrderedTitles = x.Posts.Select(xx => xx.Title).Order().ToList()
    })
    .ToListAsync();

Esta consulta equivale a lo siguiente:

var orderByEquivalent = await context.Blogs
    .OrderBy(x => x.Id)
    .Select(x => new
    {
        x.Name,
        OrderedPosts = x.Posts.OrderByDescending(xx => xx.Id).ToList(),
        OrderedTitles = x.Posts.Select(xx => xx.Title).OrderBy(xx => xx).ToList()
    })
    .ToListAsync();

y produce el siguiente SQL:

SELECT [b].[Name], [b].[Id], [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata], [p0].[Title], [p0].[Id]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
LEFT JOIN [Posts] AS [p0] ON [b].[Id] = [p0].[BlogId]
ORDER BY [b].[Id], [p].[Id] DESC, [p0].[Title]

Nota:

Los métodos Order y OrderDescending solo se admiten para colecciones de entidades, tipos complejos o escalares; no funcionarán en proyecciones más complejas, por ejemplo, colecciones de tipos anónimos que contengan múltiples propiedades.

Esta mejora ha sido aportada por el antiguo alumno del equipo EF @bricelam. Muchas gracias.

Traducción mejorada del operador lógico de negación (!)

EF9 aporta muchas optimizaciones en torno a SQL CASE/WHEN, COALESCE, negación, y otras construcciones; la mayoría de ellas han sido aportadas por Andrea Canciani (@ranma42). ¡Muchas gracias por todo! A continuación, detallaremos solo algunas de estas optimizaciones en torno a la negación lógica.

Examinemos la siguiente consulta:

var negatedContainsSimplification = await context.Posts
    .Where(p => !p.Content.Contains("Announcing"))
    .Select(p => new { p.Content }).ToListAsync();

En EF8 produciríamos el siguiente SQL:

SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE NOT (instr("p"."Content", 'Announcing') > 0)

En EF9, la operación NOT "push" se realiza en la comparación:

SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE instr("p"."Content", 'Announcing') <= 0

Otro ejemplo, aplicable a SQL Server, es una operación condicional negada.

var caseSimplification = await context.Blogs
    .Select(b => !(b.Id > 5 ? false : true))
    .ToListAsync();

En EF8 solía dar lugar a bloques anidados CASE :

SELECT CASE
    WHEN CASE
        WHEN [b].[Id] > 5 THEN CAST(0 AS bit)
        ELSE CAST(1 AS bit)
    END = CAST(0 AS bit) THEN CAST(1 AS bit)
    ELSE CAST(0 AS bit)
END
FROM [Blogs] AS [b]

En EF9 eliminamos el anidamiento:

SELECT CASE
    WHEN [b].[Id] > 5 THEN CAST(1 AS bit)
    ELSE CAST(0 AS bit)
END
FROM [Blogs] AS [b]

En SQL Server, al proyectar una propiedad bool negada:

var negatedBoolProjection = await context.Posts.Select(x => new { x.Title, Active = !x.Archived }).ToListAsync();

EF8 generaba un bloque CASE porque las comparaciones no pueden aparecer en la proyección directamente en consultas SQL Server:

SELECT [p].[Title], CASE
   WHEN [p].[Archived] = CAST(0 AS bit) THEN CAST(1 AS bit)
   ELSE CAST(0 AS bit)
END AS [Active]
FROM [Posts] AS [p]

En EF9, esta traducción se ha simplificado y ahora utiliza NOT (~) a nivel de bits:

SELECT [p].[Title], ~[p].[Archived] AS [Active]
FROM [Posts] AS [p]

Mejor soporte para Azure SQL y Azure Synapse

EF9 permite una mayor flexibilidad a la hora de especificar el tipo de SQL Server al que se destina. En lugar de configurar EF con UseSqlServer, ahora puede especificar UseAzureSql o UseAzureSynapse. Esto permite a EF producir mejor SQL cuando se utiliza Azure SQL o Azure Synapse. EF puede aprovechar las características específicas de la base de datos (por ejemplo, el tipo dedicado para JSON en Azure SQL), o trabajar en torno a sus limitaciones (por ejemplo, la cláusula ESCAPE no está disponible cuando se utiliza LIKE en Azure Synapse).

Otras mejoras en las consultas

  • La compatibilidad de consulta con colecciones primitivas introducida en EF8 se ha ampliado para ser compatible con todos los tipos ICollection<T>. Las colecciones primitivas que forman parte de entidades siguen estando limitadas a arrays, listas y, en EF9, también a arrays/listas de solo lectura.
  • Nuevas funciones ToHashSetAsync para devolver los resultados de una consulta como HashSet (30033, aportados por @wertzui).
  • TimeOnly.FromDateTime y FromTimeSpan ahora se traducen en SQL Server (33678).
  • ToString over enums ahora se traduce (#33706, contribuido por @Danevandy99).
  • string.Join ahora se traduce en CONCAT_WS en contexto no agregado en SQL Server (28899).
  • EF.Functions.PatIndex ahora se traduce en la función de SQL Server PATINDEX , que devuelve la posición inicial de la primera aparición de un patrón (#33702, @smnsht).
  • Sum y Average ahora funcionan para decimales en SQLite (#33721, aportado por @ranma42).
  • Correcciones y optimizaciones en string.StartsWith y EndsWith (31482).
  • Los métodos Convert.To* ahora pueden aceptar el argumento de tipo object (33891, aportado por @imangd).
  • La operación Exclusive-Or (XOR) ahora se traduce en SQL Server (#34071, contribuido por @ranma42).
  • Optimizaciones en torno a la nulabilidad de COLLATE las operaciones y AT TIME ZONE (#34263, aportadas por @ranma42).
  • Optimizaciones para DISTINCT más IN, EXISTS y establecer operaciones (#34381, aportadas por @ranma42).

Las anteriores son solo algunas de las mejoras más importantes en las consultas de EF9; consulta esta edición para obtener una lista más completa.

Migraciones

Protección contra migraciones simultáneas

EF9 introduce un mecanismo de bloqueo para proteger contra ejecuciones de migraciones múltiples que ocurren simultáneamente, ya que eso podría dejar la base de datos en un estado corrupto. Esto no ocurre cuando las migraciones se despliegan en el entorno de producción utilizando los métodos recomendados, pero puede ocurrir si las migraciones se aplican en tiempo de ejecución utilizando el método DbContext.Database.Migrate(). Se recomienda aplicar migraciones en la implementación, en lugar de como parte del inicio de la aplicación, pero esto puede dar lugar a arquitecturas de aplicaciones más complicadas (por ejemplo , al usar proyectos de .NET Aspire).

Nota:

Si utiliza una base de datos Sqlite, consulte los posibles problemas asociados a esta función.

Advertencia cuando no se pueden ejecutar varias operaciones de migración dentro de una transacción

La mayoría de las operaciones realizadas durante las migraciones están protegidas por una transacción. Esto asegura que si por alguna razón la migración falla, la base de datos no termine en un estado corrupto. Sin embargo, algunas operaciones no están protegidas por una transacción (por ejemplo, las operaciones en tablas optimizadas para memoria de SQL Server o las operaciones que alteran la base de datos, como la modificación de la intercalación de la base de datos). Para evitar corromper la base de datos en caso de fallo de la migración, se recomienda que estas operaciones se realicen de forma aislada mediante una migración independiente. EF9 ahora detecta un escenario cuando una migración contiene múltiples operaciones, una de las cuales no puede ser envuelta en una transacción, y emite una advertencia.

Propagación de datos mejorada

EF9 introdujo una forma conveniente de realizar la siembra de datos, es decir, poblar la base de datos con datos iniciales. DbContextOptionsBuilder contiene ahora métodos UseSeeding y UseAsyncSeeding que se ejecutan cuando se inicializa el DbContext (como parte de EnsureCreatedAsync).

Nota:

Si la aplicación se ha ejecutado previamente, la base de datos puede contener ya los datos de muestra (que se habrían añadido en la primera inicialización del contexto). Como tal, UseSeeding UseAsyncSeeding debe comprobar si los datos existen antes de intentar rellenar la base de datos. Esto puede lograrse emitiendo una simple consulta EF.

He aquí un ejemplo de cómo se pueden utilizar estos métodos:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFDataSeeding;Trusted_Connection=True;ConnectRetryCount=0")
        .UseSeeding((context, _) =>
        {
            var testBlog = context.Set<Blog>().FirstOrDefault(b => b.Url == "http://test.com");
            if (testBlog == null)
            {
                context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
                context.SaveChanges();
            }
        })
        .UseAsyncSeeding(async (context, _, cancellationToken) =>
        {
            var testBlog = await context.Set<Blog>().FirstOrDefaultAsync(b => b.Url == "http://test.com", cancellationToken);
            if (testBlog == null)
            {
                context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
                await context.SaveChangesAsync(cancellationToken);
            }
        });

Puede encontrar más información aquí.

Otras mejoras de migración

  • Al cambiar una tabla existente a una tabla temporal de SQL Server, el tamaño del código de migración se ha reducido significativamente.

Creación de modelos

Modelos compilados automáticamente

Sugerencia

El código que se muestra aquí procede del ejemplo NewInEFCore9.CompiledModels.

Los modelos compilados pueden mejorar el tiempo de inicio de las aplicaciones con modelos grandes, que son recuentos de tipos de entidad en los años 100 o 1000. En versiones anteriores de EF Core, se debía generar manualmente un modelo compilado mediante la línea de comandos. Por ejemplo:

dotnet ef dbcontext optimize

Después de ejecutar el comando, se debe agregar una línea como .UseModel(MyCompiledModels.BlogsContextModel.Instance) a OnConfiguring para indicar a EF Core que use el modelo compilado.

A partir de EF9, esta línea de .UseModel ya no es necesaria cuando el tipo DbContext de la aplicación está en el mismo proyecto o ensamblado que el modelo compilado. En su lugar, el modelo compilado se detectará y usará automáticamente. Esto se puede ver si tiene registro de EF cada vez que compila el modelo. Al ejecutar una aplicación sencilla, se muestra EF que compila el modelo cuando se inicia la aplicación:

Starting application...
>> EF is building the model...
Model loaded with 2 entity types.

La salida de la ejecución de dotnet ef dbcontext optimize en el proyecto de modelo es:

PS D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model> dotnet ef dbcontext optimize

Build succeeded in 0.3s

Build succeeded in 0.3s
Build started...
Build succeeded.
>> EF is building the model...
>> EF is building the model...
Successfully generated a compiled model, it will be discovered automatically, but you can also call 'options.UseModel(BlogsContextModel.Instance)'. Run this command again when the model is modified.
PS D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model> 

Observe que la salida del registro indica que el modelo de se creó al ejecutar el comando. Si ahora ejecutamos la aplicación de nuevo, después de volver a generar pero sin realizar cambios en el código, la salida es:

Starting application...
Model loaded with 2 entity types.

Observe que el modelo no se creó al iniciar la aplicación porque se detectó y usó automáticamente el modelo compilado.

Integración de MSBuild

Con el enfoque anterior, el modelo compilado todavía debe volver a generarse manualmente cuando se cambian los tipos de entidad o DbContext configuración. Sin embargo, EF9 se incluye con un paquete de tareas de MSBuild que puede actualizar automáticamente el modelo compilado cuando se compila el proyecto de modelo. Para empezar, instale el paquete NuGet Microsoft.EntityFrameworkCore.Tasks. Por ejemplo:

dotnet add package Microsoft.EntityFrameworkCore.Tasks --version 9.0.0

Sugerencia

Use la versión del paquete en el comando anterior que coincida con la versión de EF Core que usa.

A continuación, habilite la integración estableciendo las EFOptimizeContext propiedades y EFScaffoldModelStage en el .csproj archivo. Por ejemplo:

<PropertyGroup>
    <EFOptimizeContext>true</EFOptimizeContext>
    <EFScaffoldModelStage>build</EFScaffoldModelStage>
</PropertyGroup>

Ahora, si compilamos el proyecto, podemos ver el registro en tiempo de compilación que indica que se está compilando el modelo compilado:

Optimizing DbContext...
dotnet exec --depsfile D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.deps.json
  --additionalprobingpath G:\packages 
  --additionalprobingpath "C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages" 
  --runtimeconfig D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.runtimeconfig.json G:\packages\microsoft.entityframeworkcore.tasks\9.0.0-preview.4.24205.3\tasks\net8.0\..\..\tools\netcoreapp2.0\ef.dll dbcontext optimize --output-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\obj\Release\net8.0\ 
  --namespace NewInEfCore9 
  --suffix .g 
  --assembly D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\bin\Release\net8.0\Model.dll
  --project-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model 
  --root-namespace NewInEfCore9 
  --language C# 
  --nullable 
  --working-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App 
  --verbose 
  --no-color 
  --prefix-output 

La ejecución de la aplicación muestra que se ha detectado el modelo compilado y, por tanto, el modelo no se vuelve a compilar:

Starting application...
Model loaded with 2 entity types.

Ahora, siempre que cambie el modelo, el modelo compilado se volverá a generar automáticamente en cuanto se compile el proyecto.

Para obtener más información, consulte Integración de MSBuild.

Colecciones primitivas de solo lectura

Sugerencia

El código que se muestra aquí procede de PrimitiveCollectionsSample.cs.

EF8 introdujo compatibilidad con matrices de asignación de y listas mutables de tipos primitivos. Esto se ha ampliado en EF9 para incluir colecciones o listas de solo lectura. En concreto, EF9 admite colecciones tipadas como IReadOnlyList, IReadOnlyCollectiono ReadOnlyCollection. Por ejemplo, en el código siguiente, DaysVisited se asignarán por convención como una colección primitiva de fechas:

public class DogWalk
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ReadOnlyCollection<DateOnly> DaysVisited { get; set; }
}

La colección de solo lectura puede estar respaldada por una colección mutable normal si lo desea. Por ejemplo, en el código siguiente, DaysVisited se puede asignar como una colección primitiva de fechas, a la vez que se permite que el código de la clase manipule la lista subyacente.

    public class Pub
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public IReadOnlyCollection<string> Beers { get; set; }

        private List<DateOnly> _daysVisited = new();
        public IReadOnlyList<DateOnly> DaysVisited => _daysVisited;
    }

A continuación, estas colecciones se pueden usar en consultas de la manera normal. Por ejemplo, esta consulta LINQ:

var walksWithADrink = await context.Walks.Select(
    w => new
    {
        WalkName = w.Name,
        PubName = w.ClosestPub.Name,
        Count = w.DaysVisited.Count(v => w.ClosestPub.DaysVisited.Contains(v)),
        TotalCount = w.DaysVisited.Count
    }).ToListAsync();

Lo que se traduce en el siguiente código SQL en SQLite:

SELECT "w"."Name" AS "WalkName", "p"."Name" AS "PubName", (
    SELECT COUNT(*)
    FROM json_each("w"."DaysVisited") AS "d"
    WHERE "d"."value" IN (
        SELECT "d0"."value"
        FROM json_each("p"."DaysVisited") AS "d0"
    )) AS "Count", json_array_length("w"."DaysVisited") AS "TotalCount"
FROM "Walks" AS "w"
INNER JOIN "Pubs" AS "p" ON "w"."ClosestPubId" = "p"."Id"

Especificación del factor de relleno para claves e índices

Sugerencia

El código que se muestra aquí procede de ModelBuildingSample.cs.

EF9 admite la especificación del factor de relleno de SQL Server al usar migraciones de EF Core para crear claves e índices. En los documentos de SQL Server, "Cuando se crea o se vuelve a generar un índice, el valor de factor de relleno determina el porcentaje de espacio en cada página a nivel hoja que se va a rellenar con datos, reservando el resto en cada página como espacio libre para el crecimiento futuro".

El factor de relleno se puede establecer en un principal único o compuesto, además de, claves e índices alternativos. Por ejemplo:

modelBuilder.Entity<User>()
    .HasKey(e => e.Id)
    .HasFillFactor(80);

modelBuilder.Entity<User>()
    .HasAlternateKey(e => new { e.Region, e.Ssn })
    .HasFillFactor(80);

modelBuilder.Entity<User>()
    .HasIndex(e => new { e.Name })
    .HasFillFactor(80);

modelBuilder.Entity<User>()
    .HasIndex(e => new { e.Region, e.Tag })
    .HasFillFactor(80);

Cuando se aplica a las tablas existentes, esto modificará las tablas al factor de relleno en la restricción:

ALTER TABLE [User] DROP CONSTRAINT [AK_User_Region_Ssn];
ALTER TABLE [User] DROP CONSTRAINT [PK_User];
DROP INDEX [IX_User_Name] ON [User];
DROP INDEX [IX_User_Region_Tag] ON [User];

ALTER TABLE [User] ADD CONSTRAINT [AK_User_Region_Ssn] UNIQUE ([Region], [Ssn]) WITH (FILLFACTOR = 80);
ALTER TABLE [User] ADD CONSTRAINT [PK_User] PRIMARY KEY ([Id]) WITH (FILLFACTOR = 80);
CREATE INDEX [IX_User_Name] ON [User] ([Name]) WITH (FILLFACTOR = 80);
CREATE INDEX [IX_User_Region_Tag] ON [User] ([Region], [Tag]) WITH (FILLFACTOR = 80);

Esta mejora fue aportada por @deano-hunter. Muchas gracias.

Hacer que las convenciones de creación de modelos existentes sean más extensibles

Sugerencia

El código que se muestra aquí procede de CustomConventionsSample.cs.

Las convenciones de creación de modelos públicos para aplicaciones se introdujeron en EF7. En EF9, hemos facilitado la ampliación de algunas de las convenciones existentes. Por ejemplo, el código para asignar propiedades por atributo en EF7 es el siguiente:

public class AttributeBasedPropertyDiscoveryConvention : PropertyDiscoveryConvention
{
    public AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
        : base(dependencies)
    {
    }

    public override void ProcessEntityTypeAdded(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionContext<IConventionEntityTypeBuilder> context)
        => Process(entityTypeBuilder);

    public override void ProcessEntityTypeBaseTypeChanged(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionEntityType? newBaseType,
        IConventionEntityType? oldBaseType,
        IConventionContext<IConventionEntityType> context)
    {
        if ((newBaseType == null
             || oldBaseType != null)
            && entityTypeBuilder.Metadata.BaseType == newBaseType)
        {
            Process(entityTypeBuilder);
        }
    }

    private void Process(IConventionEntityTypeBuilder entityTypeBuilder)
    {
        foreach (var memberInfo in GetRuntimeMembers())
        {
            if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
            {
                entityTypeBuilder.Property(memberInfo);
            }
            else if (memberInfo is PropertyInfo propertyInfo
                     && Dependencies.TypeMappingSource.FindMapping(propertyInfo) != null)
            {
                entityTypeBuilder.Ignore(propertyInfo.Name);
            }
        }

        IEnumerable<MemberInfo> GetRuntimeMembers()
        {
            var clrType = entityTypeBuilder.Metadata.ClrType;

            foreach (var property in clrType.GetRuntimeProperties()
                         .Where(p => p.GetMethod != null && !p.GetMethod.IsStatic))
            {
                yield return property;
            }

            foreach (var property in clrType.GetRuntimeFields())
            {
                yield return property;
            }
        }
    }
}

En EF9, esto se puede simplificar hasta lo siguiente:

public class AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
    : PropertyDiscoveryConvention(dependencies)
{
    protected override bool IsCandidatePrimitiveProperty(
        MemberInfo memberInfo, IConventionTypeBase structuralType, out CoreTypeMapping? mapping)
    {
        if (base.IsCandidatePrimitiveProperty(memberInfo, structuralType, out mapping))
        {
            if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
            {
                return true;
            }

            structuralType.Builder.Ignore(memberInfo.Name);
        }

        mapping = null;
        return false;
    }
}

Actualice ApplyConfigurationsFromAssembly para llamar a constructores no públicos

En versiones anteriores de EF Core, el método ApplyConfigurationsFromAssembly solo instanciaba tipos de configuración con un constructor público sin parámetros. En EF9, hemos mejorado los mensajes de error generados cuando esto falla, y también habilitado la instanciación por constructor no público. Esto resulta útil cuando el código de la aplicación nunca debe crear instancias de la configuración en una clase anidada privada. Por ejemplo:

public class Country
{
    public int Code { get; set; }
    public required string Name { get; set; }

    private class FooConfiguration : IEntityTypeConfiguration<Country>
    {
        private FooConfiguration()
        {
        }

        public void Configure(EntityTypeBuilder<Country> builder)
        {
            builder.HasKey(e => e.Code);
        }
    }
}

Como inciso, algunas personas piensan que este patrón es una abominación porque acopla el tipo de entidad a la configuración. Otros piensan que es muy útil porque coordina la configuración con el tipo de entidad. No vamos a debatir esto aquí. :-)

HierarchyId de SQL Server

Sugerencia

El código que se muestra aquí procede de HierarchyIdSample.cs.

Usar sugar para la generación de rutas de acceso HierarchyId

La compatibilidad de primera clase con el tipo de SQL Server HierarchyId se agregó en EF8. En EF9, se ha agregado el método sugar para facilitar la creación de nuevos nodos secundarios en la estructura de árbol. Por ejemplo, el código siguiente consulta sobre una entidad existente con una propiedad HierarchyId:

var daisy = await context.Halflings.SingleAsync(e => e.Name == "Daisy");

A continuación, esta propiedad HierarchyId se puede usar para crear nodos secundarios sin ninguna manipulación explícita de la cadena. Por ejemplo:

var child1 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1), "Toast");
var child2 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 2), "Wills");

Si daisy tiene un valor HierarchyId de /4/1/3/1/, entonces child1 obtendrá el "/4/1/3/1/1/" HierarchyId, y child2 obtendrá el "/4/1/3/1/2/" HierarchyId.

Para crear un nodo entre estos dos elementos secundarios, se puede usar un subnivel adicional. Por ejemplo:

var child1b = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1, 5), "Toast");

Esto crea un nodo con un HierarchyId de /4/1/3/1/1.5/, colocándolo entre child1 y child2.

Esta mejora ha sido aportada por @Rezakazemi890. Muchas gracias.

Herramientas

Menos recompilaciones

La dotnet efherramienta de línea de comandos compila de forma predeterminada el proyecto antes de ejecutar la herramienta. Esto se debe a que no recompilar antes de ejecutar la herramienta es una fuente común de confusión cuando las cosas no funcionan. Los desarrolladores experimentados pueden usar la opción --no-build para evitar esta compilación, lo que puede ser lento. Sin embargo, incluso la opción --no-build podría hacer que el proyecto se vuelva a compilar la próxima vez que se cree fuera de las herramientas de EF.

Creemos que una contribución comunitaria de @Suchiman ha corregido esto. Sin embargo, también somos conscientes de que los ajustes en torno a los comportamientos de MSBuild tienen una tendencia a tener consecuencias no intencionadas, por lo que estamos pidiendo a las personas como usted que prueben esto e informen sobre cualquier experiencia negativa que tengan.