Compartir a través de


Cambios importantes en EF Core 9 (EF9)

En esta página se documentan los cambios de comportamiento de la API y que pueden interrumpir la actualización de aplicaciones existentes de EF Core 8 a EF Core 9. Asegúrese de revisar los cambios importantes anteriores si va a actualizar desde una versión anterior de EF Core:

Marco de destino

EF Core 9 se ha orientado a .NET 8. Esto significa que las aplicaciones existentes que estén orientadas a .NET 8 pueden seguir estándolo. Las aplicaciones orientadas a versiones anteriores de .NET, .NET Core y .NET Framework tendrán que orientarse a .NET 8 o .NET 9 para utilizar EF Core 9.

Resumen

Nota:

Si usa Azure Cosmos DB, consulte la sección independiente sobre cambios importantes en Azure Cosmos DB.

Cambio importante Impacto
Se produce una excepción al aplicar migraciones si hay cambios pendientes en el modelo Alto
Se produce una excepción al aplicar migraciones en una transacción explícita Alto
EF.Functions.Unhex() ahora devuelve byte[]? Bajo
Arity de los argumentos de nulabilidad de SqlFunctionExpression validados Bajo
El método ToString() devuelve ahora una cadena vacía para las instancias null Bajo
Las dependencias del marco compartido se actualizaron a 9.0.x Bajo

Cambios de impacto alto

Se produce una excepción al aplicar migraciones si hay cambios pendientes en el modelo.

Problema de seguimiento n.º 33732

Comportamiento anterior

Si el modelo tiene cambios pendientes en comparación con la última migración, no se aplican con el resto de las migraciones cuando se llama a Migrate.

Comportamiento nuevo

A partir de EF Core 9.0, si el modelo tiene cambios pendientes en comparación con la última migración, se produce una excepción cuando se llama a dotnet ef database update, Migrate o MigrateAsync:

El modelo del contexto "DbContext" tiene cambios pendientes. Agregue una nueva migración antes de actualizar la base de datos. Esta excepción se puede suprimir o registrar pasando el identificador de evento "RelationalEventId.PendingModelChangesWarning" al método "ConfigureWarnings" en "DbContext.OnConfiguring" o "AddDbContext".

Por qué

Olvidar agregar una nueva migración después de realizar cambios en el modelo es un error común que puede ser difícil de diagnosticar en algunos casos. La nueva excepción garantiza que el modelo de la aplicación coincida con la base de datos después de aplicar las migraciones.

Mitigaciones

Hay varias situaciones comunes en las que se puede producir esta excepción:

  • No hay migraciones en absoluto. Esto es común cuando la base de datos se actualiza a través de otros medios.
    • Mitigación: si no tiene previsto usar migraciones para administrar el esquema de la base de datos, quite la llamada Migrate o MigrateAsync; de lo contrario, agregue una migración.
  • Hay al menos una migración, pero falta la instantánea del modelo. Esto es habitual para las migraciones creadas manualmente.
    • Mitigación: agregue una nueva migración mediante las herramientas de EF, lo que actualizará la instantánea del modelo.
  • El desarrollador no modificó el modelo, pero está integrado de forma no determinista, lo que hace que EF lo detecte como modificado. Esto es habitual cuando new DateTime(), DateTime.Now, DateTime.UtcNowo Guid.NewGuid() se usan en objetos proporcionados para HasData().
    • mitigación: agregue una nueva migración, examine su contenido para localizar la causa y reemplace los datos dinámicos por un valor estático codificado de forma estática en el modelo. La migración deberá recrearse después de que se corrija el modelo. Si se deben usar datos dinámicos para la propagación, considere la posibilidad de usar el nuevo patrón de propagación en lugar de HasData().
  • La última migración se creó para un proveedor diferente al usado para aplicar las migraciones.
  • Las migraciones se generan o seleccionan dinámicamente al reemplazar algunos servicios de EF.
    • Mitigación: la advertencia es un falso positivo en este caso y debe omitirse:

      options.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning))

Si el escenario no se encuentra en ninguno de los casos anteriores y cada vez que agrega una nueva migración se crea la misma migración o una migración vacía y la excepción todavía se produce, cree un pequeño proyecto de reproducción y compártalo con el equipo de EF en un nuevo problema.

Se produce una excepción al aplicar migraciones en una transacción explícita

Problema de Seguimiento #17578

Comportamiento anterior

Para aplicar migraciones resistentemente, se usó el siguiente patrón:

await dbContext.Database.CreateExecutionStrategy().ExecuteAsync(async () =>
{
    await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
    await dbContext.Database.MigrateAsync(cancellationToken);
    await transaction.CommitAsync(cancellationToken);
});

Comportamiento nuevo

A partir de EF Core 9.0, las llamadas Migrate y MigrateAsync iniciarán una transacción y ejecutarán los comandos mediante un ExecutionStrategy y, si la aplicación usa el patrón anterior, se produce una excepción:

Se generó un error relacionado con la advertencia "Microsoft.EntityFrameworkCore.Migrations.MigrationsUserTransactionWarning": se inició una transacción antes de aplicar migraciones. Esto evita que se adquiera un bloqueo de base de datos y, por tanto, la base de datos no se protegerá de aplicaciones de migración simultáneas. EF ya administra las transacciones y la estrategia de ejecución según sea necesario. Elimine la transacción externa. Esta excepción se puede suprimir o registrar pasando el identificador de evento "RelationalEventId.MigrationsUserTransactionWarning" al método "ConfigureWarnings" en "DbContext.OnConfiguring" o "AddDbContext".

Por qué

El uso de una transacción explícita impide que se adquiera un bloqueo de base de datos y, por tanto, la base de datos no se protegerá de aplicaciones de migración simultáneas, también limita EF sobre cómo puede administrar las transacciones internamente.

Mitigaciones

Si hay solo una llamada a la base de datos dentro de la transacción, elimine la transacción externa y ExecutionStrategy:

await dbContext.Database.MigrateAsync(cancellationToken);

De lo contrario, si el escenario requiere una transacción explícita y tiene otro mecanismo implementado para evitar la aplicación de migración simultánea, omita la advertencia:

options.ConfigureWarnings(w => w.Ignore(RelationalEventId.MigrationsUserTransactionWarning))

Cambios de impacto bajo

EF.Functions.Unhex() ahora devuelve byte[]?

Incidencia de seguimiento n.º 33864

Comportamiento anterior

La función EF.Functions.Unhex() antes anotada para devolver byte[].

Comportamiento nuevo

A partir de EF Core 9.0, Unhex() ahora se anota para devolver byte[]?.

Por qué

Unhex() se traduce a la función SQLite unhex, que devuelve NULL para entradas no válidas. Como resultado, Unhex() devuelve null para esos casos, en violación de la anotación.

Mitigaciones

Si está seguro de que el contenido de texto pasado Unhex() representa una cadena hexadecimal válida, puede simplemente añadir el operador null-forgiving (permite valores null) como afirmación de que la invocación nunca devolverá un valor null:

var binaryData = await context.Blogs.Select(b => EF.Functions.Unhex(b.HexString)!).ToListAsync();

De lo contrario, agregue comprobaciones en tiempo de ejecución para null en el valor devuelto de Unhex().

Arity de los argumentos de nulabilidad de SqlFunctionExpression validados

Incidencia de seguimiento n.º 33852

Comportamiento anterior

Anteriormente era posible crear un SqlFunctionExpression con un número diferente de argumentos y argumentos de propagación de nulabilidad.

Comportamiento nuevo

A partir de EF Core 9.0, EF ahora inicia si el número de argumentos y argumentos de propagación de nulabilidad no coinciden.

Por qué

No tener un número coincidente de argumentos y argumentos de propagación de nulabilidad puede provocar un comportamiento inesperado.

Mitigaciones

Asegúrese de que tiene el argumentsPropagateNullability mismo número de elementos que arguments. Cuando se duda se usa false para el argumento de nulabilidad.

el método ToString() devuelve ahora una cadena vacía para las instancias null

Incidencia de seguimiento n.º 33941

Comportamiento anterior

Anteriormente EF devolvía resultados inconsistentes para el método ToString() cuando el valor del argumento era null. Por ejemplo, ToString() en la propiedad bool? con valor devuelto nullnull, pero para expresiones no propiedad bool? cuyo valor era null devolvía True. El comportamiento también era inconsistente para otros tipos de datos, por ejemplo, ToString() en el valor enum null devolvía cadena vacía.

Comportamiento nuevo

A partir de EF Core 9.0, el método ToString() devuelve ahora sistemáticamente una cadena vacía en todos los casos en que el valor del argumento es null.

Por qué

El comportamiento anterior era incoherente en diferentes tipos de datos y situaciones, además de no coincidir con el comportamiento de C#.

Mitigaciones

Para volver al comportamiento anterior, reescriba la consulta en consecuencia:

var newBehavior = context.Entity.Select(x => x.NullableBool.ToString());
var oldBehavior = context.Entity.Select(x => x.NullableBool == null ? null : x.NullableBool.ToString());

Las dependencias del marco compartido se actualizaron a 9.0.x

Comportamiento anterior

Las aplicaciones que utilizan el SDK Microsoft.NET.Sdk.Web y están orientadas a net8.0 resolverían paquetes como System.Text.Json, Microsoft.Extensions.Caching.Memory, Microsoft.Extensions.Configuration.Abstractions, Microsoft.Extensions.Logging y Microsoft.Extensions.DependencyModel desde el marco compartido, por lo que estos ensamblados no se implementarían normalmente con la aplicación.

Comportamiento nuevo

Aunque EF Core 9.0 sigue siendo compatible con net8.0, ahora hace referencia a las versiones 9.0.x de System.Text.Json, Microsoft.Extensions.Caching.Memory, Microsoft.Extensions.Configuration.Abstractions, Microsoft.Extensions.Logging y Microsoft.Extensions.DependencyModel. Las aplicaciones orientadas a net8.0 no podrán aprovechar el marco compartido para evitar la implantación de estos ensamblados.

Por qué

Las versiones de dependencia coincidentes contienen las últimas correcciones de seguridad y su uso simplifica el modelo de servicio para EF Core.

Mitigaciones

Cambie su aplicación a la versión de destino net9.0 para obtener el comportamiento anterior.

Cambios de última hora en Azure Cosmos DB

Se ha realizado un gran esfuerzo para mejorar el proveedor Azure Cosmos DB en la versión 9.0. Los cambios incluyen una serie de cambios importantes de alto impacto. Si está actualizando una aplicación existente, lea atentamente lo siguiente.

Cambio importante Impacto
La propiedad discriminator ahora se llama $type en lugar de Discriminator Alto
La propiedad id ya no contiene el discriminador por defecto Alto
la propiedad JSON id se asigna a la clave Alto
Ya no se admite la sincronización de E/S a través del proveedor de Azure Cosmos DB Media
Las consultas SQL ahora deben proyectar valores JSON directamente Media
Los resultados no definidos ahora se filtran automáticamente de los resultados de las consultas Media
Las consultas traducidas incorrectamente ya no se traducen Media
HasIndex ahora se lanza en lugar de ser ignorado Bajo
IncludeRootDiscriminatorInJsonId fue renombrado a HasRootDiscriminatorInJsonId después de 9.0.0-rc.2 Bajo

Cambios de impacto alto

La propiedad discriminator ahora se llama $type en lugar de Discriminator

Problema de seguimiento n.º 34269

Comportamiento anterior

EF añade automáticamente una propiedad discriminadora a los documentos JSON para identificar el tipo de entidad que representa el documento. En versiones anteriores de EF, esta propiedad JSON solía llamarse Discriminator por defecto.

Comportamiento nuevo

A partir de EF Core 9.0, la propiedad discriminator se llama ahora $type por defecto. Si tiene documentos existentes en Azure Cosmos DB de versiones anteriores de EF, estos utilizan la antigua nomenclatura Discriminator y, después de actualizar a EF 9.0, las consultas contra esos documentos fallarán.

Por qué

Una práctica emergente de JSON usa una propiedad $type en situaciones en las que es necesario identificar el tipo de documento. Por ejemplo, System.Text.Json de .NET también soporta polimorfismo, usando $type como discriminador por defecto el nombre de la propiedad (docs). Para alinearse con el resto del ecosistema y facilitar la interoperabilidad con herramientas externas, se ha cambiado el valor por defecto.

Mitigaciones

La mitigación más fácil es simplemente configurar el nombre de la propiedad discriminador para que sea Discriminator, igual que antes:

modelBuilder.Entity<Session>().HasDiscriminator<string>("Discriminator");

Hacer esto para todos sus tipos de entidad de nivel superior hará que EF se comporte igual que antes.

En este punto, si lo desea, también puede actualizar todos sus documentos para utilizar la nueva nomenclatura $type.

La propiedad id ahora contiene solo la propiedad clave de EF por defecto

Problema de seguimiento n.º 34179

Comportamiento anterior

Anteriormente, EF insertaba el valor discriminante de su tipo de entidad en la propiedad id del documento. Por ejemplo, si guardaba un tipo de entidad Blog con una propiedad Id que contenía 8, la propiedad id JSON contendría Blog|8.

Comportamiento nuevo

A partir de EF Core 9.0, la propiedad JSON id ya no contiene el valor discriminador, y solo contiene el valor de su propiedad clave. En el ejemplo anterior, la propiedad JSON id sería simplemente 8. Si tiene documentos existentes en Azure Cosmos DB de versiones anteriores de EF, estos tienen el valor discriminador en la propiedad JSON id, y después de actualizar a EF 9.0, las consultas sobre esos documentos fallarán.

Por qué

Dado que la propiedad JSON id debe ser única, el discriminador se añadió anteriormente para permitir la existencia de diferentes entidades con el mismo valor clave. Por ejemplo, esto permitía tener tanto a Blog como a Post con una propiedad Id que contuviera el valor 8 dentro del mismo contenedor y partición. Esto se ajustaba mejor a los patrones de modelado de datos de las bases de datos relacionales, donde cada tipo de entidad se asigna a su propia tabla y, por lo tanto, tiene su propio espacio de claves.

En general, EF 9.0 cambió la asignación para que se ajustara más a las prácticas y expectativas habituales de Azure Cosmos DB NoSQL, en lugar de corresponderse con las expectativas de los usuarios procedentes de bases de datos relacionales. Además, tener el valor discriminador en la propiedad id dificultaba la interacción de herramientas y sistemas externos con los documentos JSON generados por EF. Estos sistemas externos no suelen conocer los valores discriminadores de EF, que derivan por defecto de tipos .NET.

Mitigaciones

La solución más sencilla es simplemente configurar EF para que incluya el discriminador en la propiedad JSON id, como antes. Para ello se ha introducido una nueva opción de configuración:

modelBuilder.Entity<Session>().HasDiscriminatorInJsonId();

Hacer esto para todos sus tipos de entidad de nivel superior hará que EF se comporte igual que antes.

En este punto, si lo desea, también puede actualizar todos sus documentos para reescribir su propiedad JSON id. Tenga en cuenta que esto solo es posible si las entidades de diferentes tipos no comparten el mismo valor de identificador dentro del mismo contenedor.

La propiedad JSON id se asigna a la clave.

Problema de seguimiento n.º 34179

Comportamiento anterior

Anteriormente, EF creó una propiedad sombra mapeada a la propiedad JSON id, a menos que una de las propiedades se mapease a id explícitamente.

Comportamiento nuevo

A partir de EF Core 9, la propiedad de clave se asignará a la propiedad JSON id por convención, si es posible. Esto significa que la propiedad clave ya no se conservará en el documento con un nombre diferente y con el mismo valor, por lo que el código que no sea EF que consuma los documentos y dependa de la presencia de esta propiedad ya no funcionará correctamente.

Por qué

En general, EF 9.0 cambió la asignación para alinearse más con las prácticas y expectativas comunes de NoSQL de Azure Cosmos DB. Y no es habitual almacenar el valor de clave dos veces en el documento.

Mitigaciones

Si desea conservar el comportamiento de EF Core 8, la mitigación más sencilla es usar una nueva opción de configuración que se ha introducido para este propósito:

modelBuilder.Entity<Session>().HasShadowId();

Hacer esto para todos sus tipos de entidad de nivel superior hará que EF se comporte igual que antes. O bien, puede aplicarlo a todos los tipos de entidad del modelo con una llamada:

modelBuilder.HasShadowIds();

Cambios de impacto medio

Ya no se admite la sincronización de E/S a través del proveedor de Azure Cosmos DB

Incidencia de seguimiento n.º 32563

Comportamiento anterior

Anteriormente, llamar a métodos sincrónicos como ToList o SaveChanges haría que EF Core se bloqueara sincrónicamente mediante .GetAwaiter().GetResult() al ejecutar llamadas asincrónicas en el SDK de Azure Cosmos DB. Esto puede provocar un interbloqueo.

Comportamiento nuevo

A partir de EF Core 9.0, EF ahora lanza una excepción de forma predeterminada al intentar usar la E/S sincrónica. El mensaje de excepción es "Azure Cosmos DB no admite E/S sincrónica. Asegúrese de usar y esperar correctamente solo métodos asincrónicos al usar Entity Framework Core para acceder a Azure Cosmos DB. Para más información, vea https://aka.ms/ef-cosmos-nosync".

Por qué

El bloqueo sincrónico en métodos asincrónicos puede dar lugar a interbloqueo y el SDK de Azure Cosmos DB solo admite métodos asincrónicos.

Mitigaciones

En EF Core 9.0, el error se puede suprimir con:

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

Dicho esto, las aplicaciones deberían dejar de usar las API de sincronización con Azure Cosmos DB, ya que no es compatible con el SDK de Azure Cosmos DB. La capacidad de suprimir la excepción se quitará en una versión futura de EF Core, después de la cual la única opción será usar API asincrónicas.

Las consultas SQL ahora deben proyectar valores JSON directamente

Problema de seguimiento n.º 25527

Comportamiento anterior

Anteriormente, EF generaba consultas como la siguiente:

SELECT c["City"] FROM root c

Estas consultas hacen que Azure Cosmos DB encapsule cada resultado en un objeto JSON, como se muestra a continuación:

[
    {
        "City": "Berlin"
    },
    {
        "City": "México D.F."
    }
]
Comportamiento nuevo

A partir de EF Core 9.0, EF añade ahora el modificador VALUE a las consultas de la siguiente forma:

SELECT VALUE c["City"] FROM root c

Estas consultas hacen que Azure Cosmos DB devuelva los valores directamente, sin encapsularlos:

[
    "Berlin",
    "México D.F."
]

Si su aplicación hace uso de consultas SQL, es probable que dichas consultas no funcionen tras la actualización a EF 9.0, ya que no incluyen el modificador VALUE.

Por qué

Encapsular cada resultado en un objeto JSON adicional puede causar una degradación del rendimiento en algunos escenarios, aumenta la carga útil del resultado JSON y no es la forma natural de trabajar con Azure Cosmos DB.

Mitigaciones

Para evitarlo, basta con añadir el modificador VALUE a las proyecciones de las consultas SQL, como se muestra más arriba.

Los resultados no definidos ahora se filtran automáticamente de los resultados de las consultas

Problema de seguimiento n.º 25527

Comportamiento anterior

Anteriormente, EF generaba consultas como la siguiente:

SELECT c["City"] FROM root c

Estas consultas hacen que Azure Cosmos DB encapsule cada resultado en un objeto JSON, como se muestra a continuación:

[
    {
        "City": "Berlin"
    },
    {
        "City": "México D.F."
    }
]

Si alguno de los resultados era indefinido (por ejemplo, la propiedad City no estaba presente en el documento), se devolvía un documento vacío y EF regresaba null para ese resultado.

Comportamiento nuevo

A partir de EF Core 9.0, EF añade ahora el modificador VALUE a las consultas de la siguiente forma:

SELECT VALUE c["City"] FROM root c

Estas consultas hacen que Azure Cosmos DB devuelva los valores directamente, sin encapsularlos:

[
    "Berlin",
    "México D.F."
]

El comportamiento de Azure Cosmos DB consiste en filtrar automáticamente los valores undefined de los resultados; esto significa que si una de las propiedades City no aparece en el documento, la consulta devolverá un único resultado, en lugar de dos resultados, uno de los cuales será null.

Por qué

Encapsular cada resultado en un objeto JSON adicional puede causar una degradación del rendimiento en algunos escenarios, aumenta la carga útil del resultado JSON y no es la forma natural de trabajar con Azure Cosmos DB.

Mitigaciones

Si obtener valores null para resultados no definidos es importante para su aplicación, agrupe los valores undefined en null con el nuevo operador EF.Functions.Coalesce:

var users = await context.Customer
    .Select(c => EF.Functions.CoalesceUndefined(c.City, null))
    .ToListAsync();

Las consultas traducidas incorrectamente ya no se traducen

Problema de seguimiento n.º 34123

Comportamiento anterior

Anteriormente, EF traducía consultas como la siguiente:

var sessions = await context.Sessions
    .Take(5)
    .Where(s => s.Name.StartsWith("f"))
    .ToListAsync();

Sin embargo, la traducción SQL de esta consulta era incorrecta:

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Session") AND STARTSWITH(c["Name"], "f"))
OFFSET 0 LIMIT @__p_0

En SQL, la cláusula WHERE se evalúa antes que las cláusulas OFFSET y LIMIT, pero en la consulta LINQ anterior, el operador Take aparece antes que el operador Where. Como resultado, estas consultas podrían devolver resultados incorrectos.

Comportamiento nuevo

A partir de EF Core 9.0, estas consultas ya no se traducen y se lanza una excepción.

Por qué

Las traducciones incorrectas pueden causar corrupción silenciosa de datos, lo que puede introducir errores difíciles de detectar en su aplicación. EF siempre prefiere fracasar y responder rápido a los errores lanzándose por adelantado antes que causar posiblemente corrupción de datos.

Mitigaciones

Si estuviera satisfecho con el comportamiento anterior y quiere ejecutar el mismo SQL, simplemente cambie el orden de los operadores LINQ:

var sessions = await context.Sessions
    .Where(s => s.Name.StartsWith("f"))
    .Take(5)
    .ToListAsync();

Lamentablemente, Azure Cosmos DB no soporta actualmente las cláusulas OFFSET y LIMIT en las subconsultas SQL, que es lo que requiere la traducción correcta de la consulta LINQ original.

Cambios de impacto bajo

HasIndex ahora se lanza en lugar de ser ignorado

Problema de seguimiento n.º 34023

Comportamiento anterior

Anteriormente, las llamadas a HasIndex eran ignoradas por el proveedor de EF Cosmos DB.

Comportamiento nuevo

Ahora el proveedor lanza si HasIndex se especifica.

Por qué

En Azure Cosmos DB, todas las propiedades están indexadas por defecto y no es necesario especificar ninguna indexación. Aunque es posible definir una política de indexación personalizada, esto no está soportado actualmente por EF, y se puede hacer a través del Azure Portal sin soporte de EF. Dado que las llamadas HasIndex no eran útiles, ya no están permitidas.

Mitigaciones

Elimine cualquier llamada a HasIndex.

IncludeRootDiscriminatorInJsonId fue renombrado a HasRootDiscriminatorInJsonId después de 9.0.0-rc.2

Problema de seguimiento n.º 34717

Comportamiento anterior

La API IncludeRootDiscriminatorInJsonId se introdujo en 9.0.0 rc.1.

Comportamiento nuevo

Para la versión final de EF Core 9.0, se cambió el nombre de la API a HasRootDiscriminatorInJsonId

Por qué

Se renombró otra API relacionada para que empezara por Has en lugar de Include, por lo que esta también fue renombrada por coherencia.

Mitigaciones

Si su código usa la API IncludeRootDiscriminatorInJsonId, simplemente cámbielo para que referencie a HasRootDiscriminatorInJsonId en su lugar.