Share via


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. Para más información, consulte Plan para Entity Framework Core 9.

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 una versión preliminar de .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

Estamos trabajando en actualizaciones significativas de EF9 para el proveedor de bases de datos de EF Core para Azure Cosmos DB for NoSQL.

Acceso basado en roles

Azure Cosmos DB for NoSQL incluye un sistema de control de acceso basado en rol (RBAC) integrado. Esto ahora es compatible con EF9 para la administración y el uso de contenedores. No se requieren cambios en el código de la aplicación. Vea Problema n.º 32197 para obtener más información.

Acceso sincrónico bloqueado de forma predeterminada

Sugerencia

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

Azure Cosmos DB for NoSQL no admite el acceso sincrónico (bloqueo) desde el código de la aplicación. Anteriormente, EF enmascaraba esto de forma predeterminada bloqueando para usted en llamadas asincrónicas. Sin embargo, esto fomenta el uso de sincronización, que es un procedimiento incorrecto, y puede provocar interbloqueos. Por lo tanto, a partir de EF9, se produce una excepción cuando se intenta el acceso sincrónico. Por ejemplo:

System.InvalidOperationException: An error was generated for warning 'Microsoft.EntityFrameworkCore.Database.SyncNotSupported':
 Azure Cosmos DB does not support synchronous I/O. Make sure to use and correctly await only async methods when using
 Entity Framework Core to access Azure Cosmos DB. See https://aka.ms/ef-cosmos-nosync for more information.
 This exception can be suppressed or logged by passing event ID 'CosmosEventId.SyncNotSupported' to the 'ConfigureWarnings'
 method in 'DbContext.OnConfiguring' or 'AddDbContext'.
   at Microsoft.EntityFrameworkCore.Diagnostics.EventDefinition.Log[TLoggerCategory](IDiagnosticsLogger`1 logger, Exception exception)
   at Microsoft.EntityFrameworkCore.Cosmos.Diagnostics.Internal.CosmosLoggerExtensions.SyncNotSupported(IDiagnosticsLogger`1 diagnostics)
   at Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal.CosmosClientWrapper.DeleteDatabase()
   at Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal.CosmosDatabaseCreator.EnsureDeleted()
   at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.EnsureDeleted()

Como indica la excepción, el acceso de sincronización todavía se puede usar por ahora configurando el nivel de advertencia de forma adecuada. 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 quitar completamente la compatibilidad con la sincronización en EF11, así que empiece a actualizar para usar métodos asincrónicos como ToListAsync y SaveChangesAsync lo antes posible.

Colecciones primitivas mejoradas

Sugerencia

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

El proveedor de Cosmos DB admite colecciones primitivas de forma limitada desde EF Core 6. Esta compatibilidad se mejora en EF9, empezando por la consolidación de las superficies de API y metadatos para colecciones primitivas en bases de datos de documentos para alinearse con colecciones primitivas en bases de datos relacionales. Esto significa que las colecciones primitivas ahora se pueden asignar explícitamente mediante la API de creación de modelos, lo que permite configurar facetas del tipo de elemento. Por ejemplo, para asignar una lista de cadenas necesarias (es decir, no null):

modelBuilder.Entity<Book>()
    .PrimitiveCollection(e => e.Quotes)
    .ElementType(b => b.IsRequired());

Vea Novedades de EF8: colecciones primitivas para obtener más información sobre la API de creación de modelos.

Consultas AOT y precompiladas

Como se mencionó en la introducción, hay mucho trabajo en segundo plano para permitir que EF Core se ejecute sin compilación Just-In-Time (JIT). En su lugar, EF compila con antelación (AOT) todo lo necesario para ejecutar consultas en la aplicación. Esta compilación de AOT y el procesamiento relacionado se producirán como parte de la compilación y publicación de la aplicación. En este momento de la versión de EF9, no hay mucha disponibilidad que pueda usar usted, el desarrollador de la aplicación. Sin embargo, para aquellos interesados, los problemas completados en EF9 que admiten AOT y las consultas precompiladas son:

Vuelva aquí para ver ejemplos de cómo usar consultas precompiladas a medida que la experiencia se reúne.

Traducción de LINQ y SQL

El equipo está trabajando en algunos cambios significativos en la arquitectura de la canalización de consultas en EF Core 9 como parte de nuestras mejoras continuas en la asignación de JSON y las bases de datos de documentos. Esto significa que necesitamos que las personas como usted ejecuten el código en estos nuevos aspectos internos. (Si está leyendo un documento "Novedades" en este momento de la versión, entonces usted es una parte realmente comprometida de la comunidad; ¡gracias!) Tenemos más de 120 000 pruebas, ¡pero no es suficiente! Le necesitamos a usted, gente que ejecute código real en nuestros bits, para poder encontrar problemas y lanzar una versión sólida.

Tipos complejos 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]

Eliminación de columnas pasadas a la cláusula WITH de OPENJSON

Sugerencia

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

EF9 quita columnas innecesarias al llamar a OPENJSON WITH. Por ejemplo, considere una consulta que obtiene un recuento de una colección JSON usando un predicado:

var postsUpdatedOn = await context.Posts
    .Where(p => p.Metadata!.Updates.Count(e => e.UpdatedOn >= date) == 1)
    .ToListAsync();

En EF8, esta consulta genera el siguiente código SQL al usar el proveedor de bases de datos de Azure SQL:

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 (
    SELECT COUNT(*)
    FROM OPENJSON([p].[Metadata], '$.Updates') WITH (
        [PostedFrom] nvarchar(45) '$.PostedFrom',
        [UpdatedBy] nvarchar(max) '$.UpdatedBy',
        [UpdatedOn] date '$.UpdatedOn',
        [Commits] nvarchar(max) '$.Commits' AS JSON
    ) AS [u]
    WHERE [u].[UpdatedOn] >= @__date_0) = 1

Observe que UpdatedBy y Commits no son necesarios en esta consulta. A partir de EF9, estas columnas ahora se eliminan:

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 (
    SELECT COUNT(*)
    FROM OPENJSON([p].[Metadata], '$.Updates') WITH (
        [PostedFrom] nvarchar(45) '$.PostedFrom',
        [UpdatedOn] date '$.UpdatedOn'
    ) AS [u]
    WHERE [u].[UpdatedOn] >= @__date_0) = 1

En algunos escenarios, esto da como resultado la eliminación completa de la cláusula WITH. Por ejemplo:

var tagsWithCount = await context.Tags.Where(p => p.Text.Length == 1).ToListAsync();

En EF8, esta consulta se traduce en el siguiente código SQL:

SELECT [t].[Id], [t].[Text]
FROM [Tags] AS [t]
WHERE (
    SELECT COUNT(*)
    FROM OPENJSON([t].[Text]) WITH ([value] nvarchar(max) '$') AS [t0]) = 1

En EF9, esto se ha mejorado para:

SELECT [t].[Id], [t].[Text]
FROM [Tags] AS [t]
WHERE (
    SELECT COUNT(*)
    FROM OPENJSON([t].[Text]) AS [t0]) = 1

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:

info: 2/5/2024 15:43:13.789 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) 
      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 da lugar a la contaminación de la memoria caché de consultas con una gran cantidad de consultas que solo difieren en los valores de parámetro. 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:

info: 2/5/2024 15:43:13.812 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) 
      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

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":

info: 2/5/2024 15:43:13.803 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      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

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 de dotnetPosts está insertado, lo que da como resultado un único recorrido de ida y vuelta:

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

Nuevos métodos ToHashSetAsync<T>

Sugerencia

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

Los métodos Enumerable.ToHashSet han existido desde .NET Core 2.0. En EF9, se han agregado los métodos asincrónicos equivalentes. Por ejemplo:

var set1 = await context.Posts
    .Where(p => p.Tags.Count > 3)
    .ToHashSetAsync();

var set2 = await context.Posts
    .Where(p => p.Tags.Count > 3)
    .ToHashSetAsync(ReferenceEqualityComparer.Instance);

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

ExecuteUpdate y ExecuteDelete

Permitir pasar instancias de tipo complejo a ExecuteUpdate

Sugerencia

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

La API de ExecuteUpdate se introdujo en EF7 para realizar actualizaciones inmediatas y directas en la base de datos sin seguimiento ni SaveChanges. Por ejemplo:

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

Al ejecutar este código se ejecuta la siguiente consulta para actualizar Region a "Deutschland":

UPDATE [s]
SET [s].[Region] = N'Deutschland'
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Germany'

En EF8 ExecuteUpdate también se puede usar para actualizar valores de 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 == "Deutschland")
    .ExecuteUpdateAsync(
        s => s.SetProperty(b => b.StoreAddress.Line1, newAddress.Line1)
            .SetProperty(b => b.StoreAddress.Line2, newAddress.Line2)
            .SetProperty(b => b.StoreAddress.City, newAddress.City)
            .SetProperty(b => b.StoreAddress.Country, newAddress.Country)
            .SetProperty(b => b.StoreAddress.PostCode, newAddress.PostCode));

La ejecución de este código da como resultado la ejecución de la consulta siguiente:

UPDATE [s]
SET [s].[StoreAddress_PostCode] = @__newAddress_PostCode_4,
    [s].[StoreAddress_Country] = @__newAddress_Country_3,
    [s].[StoreAddress_City] = @__newAddress_City_2,
    [s].[StoreAddress_Line2] = NULL,
    [s].[StoreAddress_Line1] = @__newAddress_Line1_0
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Deutschland'

En EF9, la misma actualización se puede realizar pasando la propia instancia de tipo complejo. Es decir, no es necesario especificar explícitamente cada miembro. 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));

La ejecución de este código da como resultado la misma ejecución de consulta que el ejemplo anterior:

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'

Se pueden combinar varias actualizaciones de propiedades de tipo complejo y propiedades simples en una sola llamada a ExecuteUpdate. Por ejemplo:

await context.Customers
    .Where(e => e.Name == name)
    .ExecuteUpdateAsync(
        s => s.SetProperty(
                b => b.CustomerInfo.WorkAddress, new Address("Gressenhall Workhouse", null, "Beetley", "Norfolk", "NR20 4DR"))
            .SetProperty(b => b.CustomerInfo.HomeAddress, new Address("Gressenhall Farm", null, "Beetley", "Norfolk", "NR20 4DR"))
            .SetProperty(b => b.CustomerInfo.Tag, "Tog"));

La ejecución de este código da como resultado la misma ejecución de consulta que el ejemplo anterior:

UPDATE [c]
SET [c].[CustomerInfo_Tag] = N'Tog',
    [c].[CustomerInfo_HomeAddress_City] = N'Beetley',
    [c].[CustomerInfo_HomeAddress_Country] = N'Norfolk',
    [c].[CustomerInfo_HomeAddress_Line1] = N'Gressenhall Farm',
    [c].[CustomerInfo_HomeAddress_Line2] = NULL,
    [c].[CustomerInfo_HomeAddress_PostCode] = N'NR20 4DR',
    [c].[CustomerInfo_WorkAddress_City] = N'Beetley',
    [c].[CustomerInfo_WorkAddress_Country] = N'Norfolk',
    [c].[CustomerInfo_WorkAddress_Line1] = N'Gressenhall Workhouse',
    [c].[CustomerInfo_WorkAddress_Line2] = NULL,
    [c].[CustomerInfo_WorkAddress_PostCode] = N'NR20 4DR'
FROM [Customers] AS [c]
WHERE [c].[Name] = @__name_0

Migraciones

Migraciones de tablas temporales mejoradas

La migración creada al cambiar una tabla existente a una tabla temporal se ha reducido en tamaño para EF9. Por ejemplo, en EF8 convertir una única tabla existente en una tabla temporal da como resultado la siguiente migración:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AlterTable(
        name: "Blogs")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AlterColumn<string>(
        name: "SiteUri",
        table: "Blogs",
        type: "nvarchar(max)",
        nullable: false,
        oldClrType: typeof(string),
        oldType: "nvarchar(max)")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AlterColumn<string>(
        name: "Name",
        table: "Blogs",
        type: "nvarchar(max)",
        nullable: false,
        oldClrType: typeof(string),
        oldType: "nvarchar(max)")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AlterColumn<int>(
        name: "Id",
        table: "Blogs",
        type: "int",
        nullable: false,
        oldClrType: typeof(int),
        oldType: "int")
        .Annotation("SqlServer:Identity", "1, 1")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart")
        .OldAnnotation("SqlServer:Identity", "1, 1");

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodEnd",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodStart",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
}

En EF9, la misma operación ahora da como resultado una migración mucho menor:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AlterTable(
        name: "Blogs")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodEnd",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:TemporalIsPeriodEndColumn", true);

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodStart",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:TemporalIsPeriodStartColumn", true);
}

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 MSBuild y el paquete de destinos que pueden 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-preview.4.24205.3

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 la propiedadEFOptimizeContext en el archivo.csproj. Por ejemplo:

<PropertyGroup>
    <EFOptimizeContext>true</EFOptimizeContext>
</PropertyGroup>

Hay propiedades adicionales, opcionales, de MSBuild para controlar cómo se compila el modelo, equivalente a las opciones que se pasan en la línea de comandos a dotnet ef dbcontext optimize. Entre ellas se incluyen las siguientes:

Propiedad de MSBuild Descripción
EFOptimizeContext Establézcalo en true para habilitar modelos compilados automáticamente.
DbContextName Clase DbContext que se va a usar. Nombre de clase solo o completo con espacios de nombres. Si se omite esta opción, EF Core encontrará la clase de contexto. Si hay varias clases de contexto, esta opción es obligatoria.
EFStartupProject Ruta de acceso relativa al proyecto de inicio. El valor predeterminado es la carpeta actual.
EFTargetNamespace Espacio de nombres que se va a usar para todas las clases generadas. El valor predeterminado se genera a partir del espacio de nombres raíz y el directorio de salida más CompiledModels.

En nuestro ejemplo, es necesario especificar el proyecto de inicio:

<PropertyGroup>
  <EFOptimizeContext>true</EFOptimizeContext>
  <EFStartupProject>..\App\App.csproj</EFStartupProject>
</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 --startup-assembly D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.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.

[NOTA!] Estamos trabajando en algunos problemas de rendimiento con los cambios realizados en el modelo compilado en EF8 y EF9. Vea Problema 33483# para obtener más información.

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 almacenamiento en caché para secuencias

Sugerencia

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

EF9 permite establecer las opciones de almacenamiento en caché para secuencias de base de datos para cualquier proveedor de bases de datos relacionales que admita esto. Por ejemplo, UseCache se puede usar para activar explícitamente el almacenamiento en caché y establecer el tamaño de la caché:

modelBuilder.HasSequence<int>("MyCachedSequence")
    .HasMin(10).HasMax(255000)
    .IsCyclic()
    .StartsAt(11).IncrementsBy(2)
    .UseCache(3);

Esto da como resultado la siguiente definición de secuencia cuando se usa SQL Server:

CREATE SEQUENCE [MyCachedSequence] AS int START WITH 11 INCREMENT BY 2 MINVALUE 10 MAXVALUE 255000 CYCLE CACHE 3;

Del mismo modo, UseNoCache desactiva explícitamente el almacenamiento en caché:

modelBuilder.HasSequence<int>("MyUncachedSequence")
    .HasMin(10).HasMax(255000)
    .IsCyclic()
    .StartsAt(11).IncrementsBy(2)
    .UseNoCache();
CREATE SEQUENCE [MyUncachedSequence] AS int START WITH 11 INCREMENT BY 2 MINVALUE 10 MAXVALUE 255000 CYCLE NO CACHE;

Si no se llama a UseCache ni a UseNoCache, no se especifica el almacenamiento en caché y la base de datos usará el que sea su valor predeterminado. Puede ser un valor predeterminado diferente para las distintas bases de datos.

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

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.