Compartir a través de


Cambios importantes en EF Core 10 (EF10)

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

Resumen

Cambio importante Impacto
Ahora, las herramientas de EF requieren que se especifique el marco para proyectos de varios destinos Mediana
El nombre de la aplicación ahora se inserta en la cadena de conexión. Bajo
Tipo de datos JSON de SQL Server que se usa de forma predeterminada en Azure SQL y el nivel de compatibilidad 170 Bajo
Las colecciones con parámetros ahora usan varios parámetros de forma predeterminada Bajo
ExecuteUpdateAsync ahora acepta un lambda normal, sin expresiones Bajo
Los nombres de columna de tipo complejo ahora están uniquificados Bajo
Las propiedades de tipos complejos anidados utilizan la ruta completa en los nombres de las columnas Bajo
La firma de IDiscriminatorPropertySetConvention ha cambiado Bajo
Los métodos de IRelationalCommandDiagnosticsLogger añaden el parámetro logCommandText Bajo

Cambios de impacto medio

Ahora, las herramientas de EF requieren que se especifique el marco para proyectos de varios destinos

Problema de seguimiento n.º 37230

Comportamiento anterior

Anteriormente, las herramientas de EF (dotnet-ef) se podían usar en proyectos destinados a varios marcos sin especificar qué marco usar.

Nuevo comportamiento

A partir de EF Core 10.0, al ejecutar herramientas de EF en un proyecto destinado a varios marcos (mediante <TargetFrameworks> en lugar de <TargetFramework>), debe especificar explícitamente qué marco de destino se usará con la opción --framework. Sin esta opción, se producirá el siguiente error:

El proyecto tiene como destino varios marcos de trabajo. Use la opción --framework para especificar qué marco de destino se va a usar.

Por qué

En EF Core 10, las herramientas empezaron a confiar en la ResolvePackageAssets tarea de MSBuild para obtener información más precisa sobre las dependencias del proyecto. Sin embargo, esta tarea no está disponible si el proyecto tiene como destino varias plataformas de destino (TFM). La solución requiere que los usuarios seleccionen qué marco se debe usar.

Mitigaciones

Al ejecutar cualquier comando de herramientas de EF en un proyecto destinado a varios marcos, especifique el marco de destino mediante la --framework opción . Por ejemplo:

dotnet ef migrations add MyMigration --framework net9.0
dotnet ef database update --framework net9.0
dotnet ef migrations script --framework net9.0

Si el archivo del proyecto tiene este aspecto:

<PropertyGroup>
  <TargetFrameworks>net8.0;net9.0</TargetFrameworks>
</PropertyGroup>

Deberá elegir uno de los marcos (por ejemplo, net9.0) al ejecutar las herramientas de EF.

Cambios de bajo impacto

El nombre de la aplicación ahora se inserta en la cadena de conexión.

Problema de seguimiento n.º 35730

Nuevo comportamiento

Cuando se pasa a EF una cadena de conexión sin un Application Name, EF ahora inserta un Application Name que contiene información anónima sobre las versiones de EF y SqlClient que se usan. En la gran mayoría de los casos, esto no afecta a la aplicación de ninguna manera, pero puede afectar al comportamiento en algunos casos perimetrales. Por ejemplo, si se conecta a la misma base de datos con EF y otra tecnología de acceso a datos que no son de EF (por ejemplo, Dapper, ADO.NET), SqlClient usará un grupo de conexiones interno diferente, ya que EF usará ahora una cadena de conexión diferente y actualizada (una donde Application Name se ha insertado). Si este tipo de acceso mixto se realiza dentro de TransactionScope, esto puede provocar la necesidad de escalar a una transacción distribuida, cuando previamente no era necesario, debido a dos cadenas de conexión que SqlClient identifica como dos bases de datos distintas.

Mitigaciones

Una mitigación consiste simplemente en definir una Application Name en la cadena de conexión. Una vez definido uno, EF no lo sobrescribe y la cadena de conexión original se conserva exactamente as-is.

Tipo de datos JSON de SQL Server que se usa de forma predeterminada en Azure SQL y el nivel de compatibilidad 170

Problema de seguimiento n.º 36372

Comportamiento anterior

Anteriormente, al asignar colecciones primitivas o tipos de propiedad a JSON en la base de datos, el proveedor de SQL Server almacenó los datos JSON en una nvarchar(max) columna:

public class Blog
{
    // ...

    // Primitive collection, mapped to nvarchar(max) JSON column
    public string[] Tags { get; set; }
    // Owned entity type mapped to nvarchar(max) JSON column
    public List<Post> Posts { get; set; }
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>().OwnsMany(b => b.Posts, b => b.ToJson());
}

Para lo anterior, EF generó anteriormente la tabla siguiente:

CREATE TABLE [Blogs] (
    ...
    [Tags] nvarchar(max),
    [Posts] nvarchar(max)
);

Nuevo comportamiento

Con EF 10, si configura EF con UseAzureSql (consulte la documentación) o configure EF con un nivel de compatibilidad de 170 o superior (consulte la documentación), EF se asignará al nuevo tipo de datos JSON en su lugar:

CREATE TABLE [Blogs] (
    ...
    [Tags] json
    [Posts] json
);

Aunque el nuevo tipo de datos JSON es la manera recomendada de almacenar datos JSON en SQL Server en adelante, puede haber algunas diferencias de comportamiento al realizar la transición desde nvarchar(max)y es posible que no se admitan algunos formularios de consulta específicos. Por ejemplo, SQL Server no admite el operador DISTINCT a través de matrices JSON y se producirá un error en las consultas que intentan hacerlo.

Tenga en cuenta que si tiene una tabla existente y usa UseAzureSql, la actualización a EF 10 hará que se genere una migración que modifique todas las columnas JSON existentes nvarchar(max) en json. Esta operación de modificación se admite y debe aplicarse sin problemas y sin problemas, pero es un cambio no trivial en la base de datos.

Por qué

El nuevo tipo de datos JSON introducido por SQL Server es una forma superior y de primera clase para almacenar e interactuar con datos JSON en la base de datos; en particular aporta importantes mejoras de rendimiento (consulte la documentación). Se recomienda que todas las aplicaciones que usan Azure SQL Database o SQL Server 2025 se migren al nuevo tipo de datos JSON.

Mitigaciones

Si tiene como destino Azure SQL Database y no desea realizar la transición al nuevo tipo de datos JSON inmediatamente, puede configurar EF con un nivel de compatibilidad inferior a 170:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseAzureSql("<connection string>", o => o.UseCompatibilityLevel(160));
}

Si tiene como destino SQL Server local, el nivel de compatibilidad predeterminado con UseSqlServer es actualmente 150 (SQL Server 2019), por lo que no se usa el tipo de datos JSON.

Como alternativa, puede establecer explícitamente el tipo de columna en propiedades específicas para que sea nvarchar(max):

public class Blog
{
    public string[] Tags { get; set; }
    public List<Post> Posts { get; set; }
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>().PrimitiveCollection(b => b.Tags).HasColumnType("nvarchar(max)");
    modelBuilder.Entity<Blog>().OwnsMany(b => b.Posts, b => b.ToJson().HasColumnType("nvarchar(max)"));
    modelBuilder.Entity<Blog>().ComplexProperty(e => e.Posts, b => b.ToJson());
}

Las colecciones con parámetros ahora usan varios parámetros de forma predeterminada

Problema de seguimiento n.º 34346

Comportamiento anterior

En EF Core 9 y versiones anteriores, las colecciones con parámetros en consultas LINQ (como las usadas con .Contains()) se traducen a SQL mediante un parámetro de matriz JSON de forma predeterminada. Considere la consulta siguiente:

int[] ids = [1, 2, 3];
var blogs = await context.Blogs.Where(b => ids.Contains(b.Id)).ToListAsync();

En SQL Server, esto generó el siguiente comando SQL:

@__ids_0='[1,2,3]'

SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Id] IN (
    SELECT [i].[value]
    FROM OPENJSON(@__ids_0) WITH ([value] int '$') AS [i]
)

Nuevo comportamiento

A partir de EF Core 10.0, las colecciones con parámetros ahora se traducen mediante varios parámetros escalares de forma predeterminada:

SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Id] IN (@ids1, @ids2, @ids3)

Por qué

La nueva traducción predeterminada proporciona al planificador de consultas información de cardinalidad sobre la colección, lo que puede dar lugar a mejores planes de consulta en muchos escenarios. El enfoque de varios parámetros logra un equilibrio entre la eficacia de la caché de planes (mediante la parametrización) y la optimización de consultas (al proporcionar cardinalidad).

Sin embargo, las distintas cargas de trabajo pueden beneficiarse de diferentes estrategias de traducción en función de los tamaños de colección, los patrones de consulta y las características de la base de datos.

Nota:

Aunque la nueva traducción predeterminada no provocará ningún cambio de comportamiento o regresión de rendimiento en la mayoría de los casos, el cambio en la forma en que las consultas se traducen a SQL pueden tener consecuencias adversas en algunos escenarios.

Las aplicaciones que se compilaron con EF Core 8 o 9 y dependen de las características de rendimiento de la traducción de parámetros de matriz JSON (mediante OPENJSON o funciones específicas de base de datos similares) pueden experimentar diferencias de rendimiento al actualizar a EF Core 10. Esto es especialmente relevante para las consultas con colecciones grandes o patrones de consulta específicos que se beneficiaron de la estrategia de traducción anterior.

Si experimenta regresiones de rendimiento después de la actualización, considere la posibilidad de usar las estrategias de mitigación siguientes para revertir al comportamiento anterior globalmente o para consultas específicas.

Mitigaciones

Si tiene problemas con el nuevo comportamiento predeterminado (como regresiones de rendimiento), puede configurar el modo de traducción globalmente:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseSqlServer("<CONNECTION STRING>", 
            o => o.UseParameterizedCollectionMode(ParameterTranslationMode.Constant));

Los modos disponibles son:

  • ParameterTranslationMode.MultipleParameters : el nuevo valor predeterminado (varios parámetros escalares)
  • ParameterTranslationMode.Constant - Valores insertados como constantes (comportamiento predeterminado anterior a EF8)
  • ParameterTranslationMode.Parameter : usa el parámetro de matriz JSON (valor predeterminado de EF8-9)

También puede controlar la traducción por consulta:

// Use constants instead of parameters for this specific query
var blogs = await context.Blogs
    .Where(b => EF.Constant(ids).Contains(b.Id))
    .ToListAsync();

// Use a single parameter (e.g. JSON parameter with OPENJSON) instead of parameters for this specific query
var blogs = await context.Blogs
    .Where(b => EF.Parameter(ids).Contains(b.Id))
    .ToListAsync();

// Use multiple scalar parameters for this specific query. This is the default in EF 10, but is useful if the default was changed globally:
var blogs = await context.Blogs
    .Where(b => EF.MultipleParameters(ids).Contains(b.Id))
    .ToListAsync();

Para obtener más información sobre la traducción de colecciones parametrizadas, consulte la documentación.

ExecuteUpdateAsync ahora acepta un lambda normal, sin expresiones

Incidencia de seguimiento n.º 32018

Comportamiento anterior

Anteriormente, ExecuteUpdate aceptaba un argumento de árbol de expresión (Expression<Func<...>>) para los configuradores de columna.

Nuevo comportamiento

A partir de EF Core 10.0, ahora ExecuteUpdate acepta un argumento que no es de expresión (Func<...>) para los establecedores de columnas. Si estaba creando árboles de expresión para crear dinámicamente el argumento de establecedores de columnas, el código ya no se compilará, pero se puede reemplazar por una alternativa mucho más sencilla (consulte a continuación).

Por qué

El hecho de que el parámetro de establecedores de columnas era un árbol de expresión dificultaba bastante la construcción dinámica de los establecedores de columnas, donde algunos establecedores solo están presentes en función de alguna condición (vea Mitigaciones a continuación para ver un ejemplo).

Mitigaciones

El código que estaba creando árboles de expresión para crear dinámicamente el argumento de establecedores de columnas tendrá que reescribirse, pero el resultado será mucho más sencillo. Por ejemplo, supongamos que queremos actualizar las vistas de un blog, pero condicionalmente también su nombre. Dado que el argumento setters era un árbol de expresión, era necesario que código como el siguiente necesitara ser escrito:

// Base setters - update the Views only
Expression<Func<SetPropertyCalls<Blog>, SetPropertyCalls<Blog>>> setters =
    s => s.SetProperty(b => b.Views, 8);

// Conditionally add SetProperty(b => b.Name, "foo") to setters, based on the value of nameChanged
if (nameChanged)
{
    var blogParameter = Expression.Parameter(typeof(Blog), "b");

    setters = Expression.Lambda<Func<SetPropertyCalls<Blog>, SetPropertyCalls<Blog>>>(
        Expression.Call(
            instance: setters.Body,
            methodName: nameof(SetPropertyCalls<Blog>.SetProperty),
            typeArguments: [typeof(string)],
            arguments:
            [
                Expression.Lambda<Func<Blog, string>>(Expression.Property(blogParameter, nameof(Blog.Name)), blogParameter),
                Expression.Constant("foo")
            ]),
        setters.Parameters);
}

await context.Blogs.ExecuteUpdateAsync(setters);

La creación manual de árboles de expresión es complicada y propensa a errores y hace que este escenario común sea mucho más difícil de lo que debería haber sido. A partir de EF 10, ahora puede escribir lo siguiente en su lugar:

await context.Blogs.ExecuteUpdateAsync(s =>
{
    s.SetProperty(b => b.Views, 8);
    if (nameChanged)
    {
        s.SetProperty(b => b.Name, "foo");
    }
});

Los nombres de columna de tipo complejo ahora están uniquificados

Problema de seguimiento n.º 4970

Comportamiento anterior

Anteriormente, al asignar tipos complejos a columnas de tabla, si varias propiedades de tipos complejos diferentes tuvieran el mismo nombre de columna, compartirían silenciosamente la misma columna.

Nuevo comportamiento

A partir de EF Core 10.0, los nombres de columna de tipo complejo se uniquifican anexando un número al final si existe otra columna con el mismo nombre en la tabla.

Por qué

Esto evita daños en los datos que pueden producirse cuando varias propiedades se asignan involuntariamente a la misma columna.

Mitigaciones

Si necesita varias propiedades para compartir la misma columna, configúrelas explícitamente mediante Property y HasColumnName:

modelBuilder.Entity<Customer>(b =>
{
    b.ComplexProperty(c => c.ShippingAddress, p => p.Property(a => a.Street).HasColumnName("Street"));
    b.ComplexProperty(c => c.BillingAddress, p => p.Property(a => a.Street).HasColumnName("Street"));
});

Las propiedades de tipo complejo anidado usan la ruta de acceso completa en los nombres de columna

Comportamiento anterior

Anteriormente, las propiedades de los tipos complejos anidados se asignaban a columnas con solo el nombre de tipo declarante. Por ejemplo, EntityType.Complex.NestedComplex.Property se asignó a la columna NestedComplex_Property.

Nuevo comportamiento

A partir de EF Core 10.0, las propiedades de los tipos complejos anidados usan la ruta de acceso completa a la propiedad como parte del nombre de columna. Por ejemplo, EntityType.Complex.NestedComplex.Property ahora está asignado a la columna Complex_NestedComplex_Property.

Por qué

Esto proporciona una mejor unicidad del nombre de columna y hace que sea más clara qué propiedad se asigna a qué columna.

Mitigaciones

Si necesita mantener los nombres de columna antiguos, configúrelos explícitamente mediante Property y HasColumnName:

modelBuilder.Entity<EntityType>()
    .ComplexProperty(e => e.Complex)
    .ComplexProperty(o => o.NestedComplex)
    .Property(c => c.Property)
    .HasColumnName("NestedComplex_Property");

La firma de IDiscriminatorPropertySetConvention ha cambiado

Comportamiento anterior

Anteriormente, IDiscriminatorPropertySetConvention.ProcessDiscriminatorPropertySet tomaba IConventionEntityTypeBuilder como parámetro.

Nuevo comportamiento

A partir de EF Core 10.0, la firma del método cambió para tomar IConventionTypeBaseBuilder en lugar de IConventionEntityTypeBuilder.

Por qué

Este cambio permite que la convención funcione con tipos de entidad y tipos complejos.

Mitigaciones

Actualice las implementaciones de convención personalizadas para usar la nueva firma:

public virtual void ProcessDiscriminatorPropertySet(
    IConventionTypeBaseBuilder typeBaseBuilder, // Changed from IConventionEntityTypeBuilder
    string name,
    Type type,
    MemberInfo memberInfo,
    IConventionContext<IConventionProperty> context)

Los métodos de IRelationalCommandDiagnosticsLogger añaden el parámetro logCommandText

Problema de seguimiento n.º 35757

Comportamiento anterior

Anteriormente, los métodos en IRelationalCommandDiagnosticsLogger como CommandReaderExecuting, CommandReaderExecuted, CommandScalarExecutingy otros aceptaron un command parámetro que representa el comando de base de datos que se ejecuta.

Nuevo comportamiento

A partir de EF Core 10.0, estos métodos ahora requieren un parámetro adicional logCommandText . Este parámetro contiene el texto del comando SQL que se registrará, que puede tener datos confidenciales censurados cuando EnableSensitiveDataLogging no está habilitado.

Por qué

Este cambio admite la nueva característica para redactar constantes en línea del registro de forma predeterminada. Cuando EF inserta valores de parámetro en SQL (por ejemplo, al usar EF.Constant()), esos valores ahora se redactan de los registros a menos que el registro de datos confidenciales esté habilitado explícitamente. El logCommandText parámetro proporciona sql censurado para fines de registro, mientras que el command parámetro contiene el código SQL real que se ejecuta.

Mitigaciones

Si tiene una implementación personalizada de IRelationalCommandDiagnosticsLogger, deberá actualizar las firmas de método para incluir el nuevo logCommandText parámetro. Por ejemplo:

public InterceptionResult<DbDataReader> CommandReaderExecuting(
    IRelationalConnection connection,
    DbCommand command,
    DbContext context,
    Guid commandId,
    Guid connectionId,
    DateTimeOffset startTime,
    string logCommandText) // New parameter
{
    // Use logCommandText for logging purposes
    // Use command for execution-related logic
}

El logCommandText parámetro contiene el código SQL que se va a registrar (con constantes insertadas potencialmente redactadas), mientras command.CommandText contiene el código SQL real que se ejecutará en la base de datos.

Cambios importantes en Microsoft.Data.Sqlite

Resumen

Cambio importante Impacto
El uso de GetDateTimeOffset sin especificar un desplazamiento asume ahora UTC Alto
Escribir DateTimeOffset en la columna REAL ahora escribe en UTC Alto
Usar GetDateTime con un desplazamiento ahora devuelve el valor en UTC Alto

Cambios de impacto alto

El uso de GetDateTimeOffset sin un desplazamiento horario ahora asume UTC.

Problema de seguimiento n.º 36195

Comportamiento anterior

Anteriormente, al usar GetDateTimeOffset en una marca de tiempo textual que no tenía un desplazamiento (por ejemplo, 2014-04-15 10:47:16), Microsoft.Data.Sqlite presupondría que el valor estaba en la zona horaria local. Es decir, el valor se ha analizado como 2014-04-15 10:47:16+02:00 (suponiendo que la zona horaria local era UTC+2).

Nuevo comportamiento

A partir de Microsoft.Data.Sqlite 10.0, cuando se usa GetDateTimeOffset en una marca de tiempo textual que no tiene un desplazamiento, Microsoft.Data.Sqlite asume que el valor está en UTC.

Por qué

Esto es para alinearse con el comportamiento de SQLite, en el que las marcas de tiempo sin un desplazamiento se tratan como UTC.

Mitigaciones

El código se debe ajustar en consecuencia.

Como último recurso temporal, puede revertir al comportamiento anterior estableciendo el modificador Microsoft.Data.Sqlite.Pre10TimeZoneHandling de AppContext en true, consulte AppContext para consumidores de bibliotecas para obtener más detalles.

AppContext.SetSwitch("Microsoft.Data.Sqlite.Pre10TimeZoneHandling", isEnabled: true);

La escritura de DateTimeOffset en la columna REAL ahora se realiza en UTC

Problema de seguimiento n.º 36195

Comportamiento anterior

Anteriormente, al escribir un DateTimeOffset valor en una columna REAL, Microsoft.Data.Sqlite escribiría el valor sin tener en cuenta el desplazamiento.

Nuevo comportamiento

A partir de Microsoft.Data.Sqlite 10.0, al escribir un DateTimeOffset valor en una columna REAL, Microsoft.Data.Sqlite convertirá el valor a UTC antes de realizar las conversiones y escribirlo.

Por qué

El valor escrito era incorrecto, no se alineaba con el comportamiento de SQLite en el que las marcas de tiempo REALES se enumeraban como UTC.

Mitigaciones

El código se debe ajustar en consecuencia.

Como último recurso temporal, puede revertir al comportamiento anterior estableciendo el modificador Microsoft.Data.Sqlite.Pre10TimeZoneHandling de AppContext en true, consulte AppContext para consumidores de bibliotecas para obtener más detalles.

AppContext.SetSwitch("Microsoft.Data.Sqlite.Pre10TimeZoneHandling", isEnabled: true);

El uso de GetDateTime con una compensación ahora devuelve el valor en UTC.

Problema de seguimiento n.º 36195

Comportamiento anterior

Anteriormente, al usar GetDateTime en una marca de tiempo de texto que tenía un desplazamiento (por ejemplo, 2014-04-15 10:47:16+02:00), Microsoft.Data.Sqlite devolvería el valor con DateTimeKind.Local (incluso si el desplazamiento no fuera local). La hora se ha analizado correctamente teniendo en cuenta el desplazamiento.

Nuevo comportamiento

A partir de Microsoft.Data.Sqlite 10.0, cuando se usa GetDateTime en una marca de tiempo textual que tiene un desplazamiento, Microsoft.Data.Sqlite convertirá el valor a UTC y lo devolverá con DateTimeKind.Utc.

Por qué

Aunque la hora se anificó correctamente, dependía de la zona horaria local configurada por la máquina, lo que podría dar lugar a resultados inesperados.

Mitigaciones

El código se debe ajustar en consecuencia.

Como último recurso temporal, puede revertir al comportamiento anterior estableciendo el modificador Microsoft.Data.Sqlite.Pre10TimeZoneHandling de AppContext en true, consulte AppContext para consumidores de bibliotecas para obtener más detalles.

AppContext.SetSwitch("Microsoft.Data.Sqlite.Pre10TimeZoneHandling", isEnabled: true);