Compartir a través de


Cambios importantes en EF Core 8 (EF8)

En esta página se documentan los cambios de comportamiento de la API que pueden interrumpir la actualización de aplicaciones existentes de EF Core 7 a EF Core 8. 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 8 tiene como destino .NET 8. Las aplicaciones destinadas a versiones anteriores de .NET, .NET Core y .NET Framework tendrán que actualizarse para tener como destino .NET 8.

Resumen

Cambio importante Impacto
Contains en las consultas LINQ puede dejar de funcionar en versiones anteriores de SQL Server Alto
Las enumeraciones en JSON se almacenan como enteros en lugar de cadenas de forma predeterminada Alto
SQL Server date y time ahora aplican scaffolding en .NET DateOnly y TimeOnly Media
Las columnas booleanas con un valor generado por la base de datos ya no tienen scaffolding como que admiten valores NULL Media
Los métodos SQLite Math ahora traducen a SQL Bajo
ITypeBase reemplaza a IEntityType en algunas API Bajo
Las expresiones ValueGenerator deben usar API públicas Bajo
ExcludeFromMigrations ya no excluye otras tablas en una jerarquía de TPC Bajo
Las claves de entero sin propiedades reemplazadas persisten en documentos de Cosmos Bajo
El modelo relacional se genera en el modelo compilado Bajo
La scaffolding puede generar nombres de navegación diferentes Bajo
Los discriminadores ahora tienen una longitud máxima Bajo
Los valores de clave de SQL Server se comparan sin distinción entre mayúsculas y minúsculas Bajo
Múltiples llamadas AddDbContext se aplican en diferente orden Bajo
EntityTypeAttributeConventionBase sustituido por TypeAttributeConventionBase Bajo

Cambios de impacto alto

Contains en las consultas LINQ puede dejar de funcionar en versiones anteriores de SQL Server.

Incidencia de seguimiento n.º 13617

Comportamiento anterior

Anteriormente, cuando se usaba el operador Contains en consultas LINQ con una lista de valores parametrizados, EF generaba SQL que era ineficaz pero funcionaba en todas las versiones de SQL Server.

Comportamiento nuevo

A partir de EF Core 8.0, EF ahora genera SQL que es más eficaz, pero no es compatible con SQL Server 2014 y versiones inferiores.

Tenga en cuenta que las versiones más recientes de SQL Server se pueden configurar con un nivel de compatibilidad anterior, lo que también hace que sean incompatibles con el nuevo SQL. Esto también puede ocurrir con una base de datos Azure SQL que se migró desde una instancia anterior de SQL Server local, arrastrando el antiguo nivel de compatibilidad.

Por qué

El SQL anterior generado por EF Core para Contains insertaba los valores parametrizados como constantes en SQL. Por ejemplo, la siguiente consulta LINQ:

var names = new[] { "Blog1", "Blog2" };

var blogs = await context.Blogs
    .Where(b => names.Contains(b.Name))
    .ToArrayAsync();

... se traduciría al siguiente SQL:

SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] IN (N'Blog1', N'Blog2')

Esta inserción de valores constantes en el SQL crea muchos problemas de rendimiento, ya que anula el almacenamiento en caché del plan de consulta y provoca expulsiones innecesarias de otras consultas. La nueva traducción de EF Core 8.0 usa la función OPENJSON de SQL Server para transferir en su lugar los valores como una matriz JSON. Esto resuelve los problemas de rendimiento inherentes a la técnica anterior; sin embargo, la función OPENJSON no está disponible en SQL Server 2014 y versiones inferiores.

Para más información sobre este cambio, consulte esta entrada de blog.

Mitigaciones

Si la base de datos es SQL Server 2016 (13.x) o posterior, o si usa Azure SQL, compruebe el nivel de compatibilidad configurado de la base de datos mediante el siguiente comando:

SELECT name, compatibility_level FROM sys.databases;

Si el nivel de compatibilidad es inferior a 130 (SQL Server 2016), considere la posibilidad de cambiarlo a un valor más reciente (documentación).

De lo contrario, si la versión de la base de datos es realmente anterior a SQL Server 2016, o se establece en un nivel de compatibilidad anterior que no se puede cambiar por algún motivo, configure EF Core para revertir a la versión de SQL anterior y menos eficaz de la siguiente manera:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseSqlServer(@"<CONNECTION STRING>", o => o.UseCompatibilityLevel(120));

Las enumeraciones en JSON se almacenan como enteros en lugar de cadenas de forma predeterminada

Incidencia de seguimiento n.º 13617

Comportamiento anterior

En EF7, las enumeraciones asignadas a JSON se almacenan de forma predeterminada como valores de cadena en el documento JSON.

Comportamiento nuevo

A partir de EF Core 8.0 (ahora EF), de forma predeterminada, se asignan enumeraciones a valores enteros en el documento JSON.

Por qué

De forma predeterminada, EF siempre ha asignado enumeraciones a una columna numérica en bases de datos relacionales. Dado que EF admite consultas en las que los valores de JSON interactúan con valores de columnas y parámetros, es importante que los valores de JSON coincidan con los valores de la columna que no es JSON.

Mitigaciones

Para seguir usando cadenas, configure la propiedad enumeración con una conversión. Por ejemplo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<User>().Property(e => e.Status).HasConversion<string>();
}

O bien, para todas las propiedades del tipo de enumeración:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<StatusEnum>().HaveConversion<string>();
}

Cambios de impacto medio

SQL Server date y time ahora aplican scaffolding en .NET DateOnly y TimeOnly

Incidencia de seguimiento n.º 24507

Comportamiento anterior

Anteriormente, al aplicar scaffolding a una base de datos de SQL Server con date o time columnas, EF generaría propiedades de entidad con tipos DateTime y TimeSpan.

Comportamiento nuevo

A partir de EF Core 8.0 date y time, se aplica scaffolding como DateOnly y TimeOnly.

Por qué

DateOnly y TimeOnly se han introducido en .NET 6.0 y son una coincidencia perfecta para asignar los tipos de fecha y hora de la base de datos. DateTime en particular, contiene un componente de tiempo que no se usa y puede causar confusión al asignarlo a date, y TimeSpan representa un intervalo de tiempo, posiblemente incluidos los días, en lugar de una hora del día en la que se produce un evento. El uso de los nuevos tipos evita errores y confusiones, y proporciona claridad de intención.

Mitigaciones

Este cambio solo afecta a los usuarios que periódicamente vuelven a aplicar scaffolding a su base de datos en un modelo de código EF (flujo "database-first").

Se recomienda reaccionar a este cambio modificando el código para usar los tipos DateOnly y TimeOnly a los que se ha aplicado scaffolding recientemente. Sin embargo, si no es posible, puede editar las plantillas de scaffolding para revertir a la asignación anterior. Para ello, configure las plantillas como se describe en esta página. A continuación, edite el archivo EntityType.t4, busque dónde se generan las propiedades de entidad (busque property.ClrType) y cambie el código a lo siguiente:

        var clrType = property.GetColumnType() switch
        {
            "date" when property.ClrType == typeof(DateOnly) => typeof(DateTime),
            "date" when property.ClrType == typeof(DateOnly?) => typeof(DateTime?),
            "time" when property.ClrType == typeof(TimeOnly) => typeof(TimeSpan),
            "time" when property.ClrType == typeof(TimeOnly?) => typeof(TimeSpan?),
            _ => property.ClrType
        };

        usings.AddRange(code.GetRequiredUsings(clrType));

        var needsNullable = Options.UseNullableReferenceTypes && property.IsNullable && !clrType.IsValueType;
        var needsInitializer = Options.UseNullableReferenceTypes && !property.IsNullable && !clrType.IsValueType;
#>
    public <#= code.Reference(clrType) #><#= needsNullable ? "?" : "" #> <#= property.Name #> { get; set; }<#= needsInitializer ? " = null!;" : "" #>
<#

Las columnas booleanas con un valor generado por la base de datos ya no tienen scaffolding como que admiten valores NULL

Incidencia de seguimiento n.º 15070

Comportamiento anterior

Anteriormente, a las columnas bool que no aceptaban valores NULL, con una restricción predeterminada de base de datos, se les aplicaba scaffolding como propiedades bool? que admitían valores NULL.

Comportamiento nuevo

A partir de EF Core 8.0, a las columnas que no aceptan valores NULL bool siempre se les aplica scaffolding como propiedades que no aceptan valores NULL.

Por qué

Una propiedad bool no tendrá su valor enviado a la base de datos si ese valor es false, que es el valor predeterminado de CLR. Si la base de datos tiene un valor predeterminado de true para la columna, aunque el valor de la propiedad sea false, el valor de la base de datos termina como true. Sin embargo, en EF8, el sentinel se usa para determinar si una propiedad tiene un valor que se puede cambiar. Esto se hace automáticamente para las propiedades bool con un valor generado por la base de datos de true, lo que significa que ya no es necesario aplicar scaffolding a las propiedades como que admiten un valor NULL.

Mitigaciones

Este cambio solo afecta a los usuarios que periódicamente vuelven a aplicar scaffolding a su base de datos en un modelo de código EF (flujo "database-first").

Se recomienda reaccionar a este cambio modificando el código para usar la propiedad bool que no acepta valores NULL. Sin embargo, si no es posible, puede editar las plantillas de scaffolding para revertir a la asignación anterior. Para ello, configure las plantillas como se describe en esta página. A continuación, edite el archivo EntityType.t4, busque dónde se generan las propiedades de entidad (busque property.ClrType) y cambie el código a lo siguiente:

#>
        var propertyClrType = property.ClrType != typeof(bool)
                              || (property.GetDefaultValueSql() == null && property.GetDefaultValue() != null)
            ? property.ClrType
            : typeof(bool?);
#>
    public <#= code.Reference(propertyClrType) #><#= needsNullable ? "?" : "" #> <#= property.Name #> { get; set; }<#= needsInitializer ? " = null!;" : "" #>
<#
<#

Cambios de impacto bajo

Los métodos Math de SQLite ahora traducen a SQL.

Incidencia de seguimiento n.º 18843

Comportamiento antiguo

Anteriormente solo los métodos Abs, Max, Min y Round de Math se traducían a SQL. Todos los demás miembros se evaluaban en el cliente si aparecían en la expresión Select final de una consulta.

Comportamiento nuevo

En EF Core 8.0, todos los métodos Math con las funciones matemáticas de SQLite correspondientes se traducen a SQL.

Estas funciones matemáticas se han habilitado en la biblioteca nativa de SQLite que proporcionamos de forma predeterminada (mediante nuestra dependencia del paquete NuGet SQLitePCLRaw.bundle_e_sqlite3). También se han habilitado en la biblioteca proporcionada por SQLitePCLRaw.bundle_e_sqlcipher. Si usa una de estas bibliotecas, su aplicación no debería verse afectada por este cambio.

Sin embargo, existe la posibilidad de que las aplicaciones que incluyan la biblioteca nativa de SQLite por otros medios no puedan habilitar las funciones matemáticas. En estos casos, los métodos Math se traducirán a SQL y no encontrarán estos errores de funciones cuando se ejecuten.

Por qué

SQLite agregó funciones matemáticas integradas en la versión 3.35.0. Aunque están deshabilitadas de forma predeterminada, se han vuelto lo suficientemente generalizados como para que decidamos proporcionar traducciones predeterminadas para ellas en nuestro proveedor de SQLite de EF Core.

También colaboramos con Eric Sink en el proyecto SQLitePCLRaw para habilitar funciones matemáticas en todas las bibliotecas nativas de SQLite proporcionadas como parte de ese proyecto.

Mitigaciones

La manera más sencilla de corregir interrupciones es, siempre que sea posible, habilitar la función matemática en la biblioteca nativa de SQLite especificando la opción en tiempo de compilación SQLITE_ENABLE_MATH_FUNCTIONS.

Si no controla la compilación de la biblioteca nativa, también puede corregir interrupciones mediante la creación de las funciones por su cuenta en tiempo de ejecución usando las API Microsoft.Data.Sqlite.

sqliteConnection
    .CreateFunction<double, double, double>(
        "pow",
        Math.Pow,
        isDeterministic: true);

Como alternativa, puede forzar la evaluación de cliente dividiendo la expresión Select en dos partes separadas por AsEnumerable.

// Before
var query = dbContext.Cylinders
    .Select(
        c => new
        {
            Id = c.Id
            // May throw "no such function: pow"
            Volume = Math.PI * Math.Pow(c.Radius, 2) * c.Height
        });

// After
var query = dbContext.Cylinders
    // Select the properties you'll need from the database
    .Select(
        c => new
        {
            c.Id,
            c.Radius,
            c.Height
        })
    // Switch to client-eval
    .AsEnumerable()
    // Select the final results
    .Select(
        c => new
        {
            Id = c.Id,
            Volume = Math.PI * Math.Pow(c.Radius, 2) * c.Height
        });

ITypeBase reemplaza a IEntityType en algunas API

Incidencia de seguimiento n.º 13947

Comportamiento anterior

Anteriormente, todos los tipos estructurales asignados eran tipos de entidad.

Comportamiento nuevo

Con la introducción de tipos complejos en EF8, algunas API que anteriormente usaban IEntityType ahora usan ITypeBase para que las API se puedan usar con tipos complejos o de entidad. Esto incluye:

  • IProperty.DeclaringEntityType ahora está obsoleta y IProperty.DeclaringType se debe usar en su lugar.
  • IEntityTypeIgnoredConvention ahora está obsoleta y ITypeIgnoredConvention se debe usar en su lugar.
  • IValueGeneratorSelector.Selectahora acepta ITypeBase que puede ser, pero no tiene que ser IEntityType.

Por qué

Con la introducción de tipos complejos en EF8, estas API se pueden usar con IEntityType o IComplexType.

Mitigaciones

Las API antiguas están obsoletas, pero no se quitarán hasta EF10. El código debe actualizarse para usar las nuevas API lo antes posible.

Las expresiones ValueConverter y ValueComparer deben usar API públicas para el modelo compilado

Incidencia de seguimiento n.º 24896

Comportamiento anterior

Anteriormente, las definiciones ValueConverter y ValueComparer no se incluían en el modelo compilado, por lo que podrían contener código arbitrario.

Comportamiento nuevo

EF ahora extrae las expresiones de los objetos ValueConverter y ValueComparer e incluye estos C# en el modelo compilado. Esto significa que estas expresiones solo deben usar la API pública.

Por qué

El equipo de EF está moviendo gradualmente más construcciones al modelo compilado para admitir el uso de EF Core con AOT en el futuro.

Mitigaciones

Haga públicas las API que usa el comparador. Por ejemplo, considere este convertidor simple:

public class MyValueConverter : ValueConverter<string, byte[]>
{
    public MyValueConverter()
        : base(v => ConvertToBytes(v), v => ConvertToString(v))
    {
    }

    private static string ConvertToString(byte[] bytes)
        => ""; // ... TODO: Conversion code

    private static byte[] ConvertToBytes(string chars)
        => Array.Empty<byte>(); // ... TODO: Conversion code
}

Para usar este convertidor en un modelo compilado con EF8, los métodos ConvertToString y ConvertToBytes se deben hacer públicos. Por ejemplo:

public class MyValueConverter : ValueConverter<string, byte[]>
{
    public MyValueConverter()
        : base(v => ConvertToBytes(v), v => ConvertToString(v))
    {
    }

    public static string ConvertToString(byte[] bytes)
        => ""; // ... TODO: Conversion code

    public static byte[] ConvertToBytes(string chars)
        => Array.Empty<byte>(); // ... TODO: Conversion code
}

ExcludeFromMigrations ya no excluye otras tablas en una jerarquía de TPC

Incidencia de seguimiento n.º 30079

Comportamiento anterior

Anteriormente, el uso de ExcludeFromMigrations en una tabla de una jerarquía de TPC también excluiría otras tablas de la jerarquía.

Comportamiento nuevo

A partir de EF Core 8.0, ExcludeFromMigrations no afecta a otras tablas.

Por qué

El comportamiento anterior era un error y impedía que las migraciones se usaran para administrar jerarquías entre proyectos.

Mitigaciones

Use ExcludeFromMigrations explícitamente en cualquier otra tabla que se deba excluir.

Las claves de entero sin propiedades reemplazadas persisten en documentos de Cosmos

Incidencia de seguimiento n.º 31664

Comportamiento anterior

Anteriormente, las propiedades de entero sin propiedades reemplazadas, que coincidían con los criterios para ser una propiedad de clave sintetizada, no se persistían en el documento JSON, sino que se volvían a sintetizar al salir.

Comportamiento nuevo

A partir de EF Core 8.0, estas propiedades ahora persisten.

Por qué

El comportamiento anterior era un error y impedía que las propiedades que coincidían con los criterios de clave sintetizada persistan en Cosmos.

Mitigaciones

Excluya la propiedad del modelo si no se debe conservar su valor. Además, puede deshabilitar este comportamiento completamente estableciendo el modificador Microsoft.EntityFrameworkCore.Issue31664 de AppContext en true. Consulte AppContext para consumidores de bibliotecas para más información.

AppContext.SetSwitch("Microsoft.EntityFrameworkCore.Issue31664", isEnabled: true);

El modelo relacional se genera en el modelo compilado

Incidencia de seguimiento n.º 24896

Comportamiento anterior

Anteriormente, el modelo relacional se calculaba en tiempo de ejecución incluso cuando se usaba un modelo compilado.

Comportamiento nuevo

A partir de EF Core 8.0, el modelo relacional forma parte del modelo compilado generado. Sin embargo, para los modelos especialmente grandes, el archivo generado puede no compilarse.

Por qué

Esto se ha hecho para mejorar aún más el tiempo de inicio.

Mitigaciones

Edite el archivo *ModelBuilder.cs generado y quite la línea AddRuntimeAnnotation("Relational:RelationalModel", CreateRelationalModel());, así como el método CreateRelationalModel().

La scaffolding puede generar nombres de navegación diferentes

Incidencia de seguimiento n.º 27832

Comportamiento anterior

Anteriormente, al aplicar scaffolding a los tipos de entidad DbContext de una base de datos existente, los nombres de navegación de las relaciones a veces se derivaban de un prefijo común de varios nombres de columna de clave externa.

Comportamiento nuevo

A partir de EF Core 8.0, los prefijos comunes de nombres de columna de una clave externa compuesta ya no se usan para generar nombres de navegación.

Por qué

Se trata de una regla de nomenclatura oscura que a veces genera nombres muy pobres como, S, Student_ o incluso solo _. Sin esta regla, los nombres extraños ya no se generan y las convenciones de nomenclatura para las navegaciones también se simplifican, lo que facilita la comprensión y predicción de los nombres que se generarán.

Mitigaciones

EF Core Power Tools tiene una opción para seguir generando navegaciones de la manera antigua. Como alternativa, el código generado se puede personalizar completamente mediante plantillas T4. Esto se puede usar para ejemplo las propiedades de clave externa de las relaciones de scaffolding y usar cualquier regla adecuada para el código para generar los nombres de navegación que necesita.

Los discriminadores ahora tienen una longitud máxima

Incidencia de seguimiento n.º 10691

Comportamiento anterior

Anteriormente, las columnas discriminatorias creadas para la asignación de herencia de TPH se configuraron como nvarchar(max) en SQL Server o Azure SQL, o el tipo de cadena sin enlazar equivalente en otras bases de datos.

Comportamiento nuevo

A partir de EF Core 8.0, las columnas discriminadora se crean con una longitud máxima que cubre todos los valores discriminadores conocidos. EF generará una migración para realizar este cambio. Sin embargo, si la columna discriminante está restringida de alguna manera(por ejemplo, como parte de un índice), es posible que se produzca un error en el AlterColumn creado por Migraciones.

Por qué

Las columnas nvarchar(max) son ineficaces e innecesarias cuando se conocen las longitudes de todos los valores posibles.

Mitigaciones

El tamaño de columna se puede convertir explícitamente sin enlazar:

modelBuilder.Entity<Foo>()
    .Property<string>("Discriminator")
    .HasMaxLength(-1);

Los valores de clave de SQL Server se comparan sin distinción entre mayúsculas y minúsculas

Incidencia de seguimiento n.º 27526

Comportamiento anterior

Anteriormente, al realizar un seguimiento de entidades con claves de cadena con los proveedores de bases de datos SQL Server o Azure SQL, los valores de las claves se comparaban mediante el comparador ordinal predeterminado de .NET que distingue entre mayúsculas y minúsculas.

Comportamiento nuevo

A partir de EF Core 8.0, los valores de clave de cadena de SQL Server o Azure SQL se comparan con el comparador ordinal sin distinción entre mayúsculas y minúsculas predeterminado de .NET.

Por qué

De forma predeterminada, SQL Server usa comparaciones que no distinguen mayúsculas de minúsculas al comparar valores de clave externa para coincidencias con valores de clave principal. Esto significa que cuando EF usa comparaciones que distinguen mayúsculas de minúsculas, es posible que no conecte una clave externa a una clave principal cuando debería.

Mitigaciones

Las comparaciones que distinguen mayúsculas de minúsculas se pueden usar estableciendo un ValueComparerpersonalizado. Por ejemplo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var comparer = new ValueComparer<string>(
        (l, r) => string.Equals(l, r, StringComparison.Ordinal),
        v => v.GetHashCode(),
        v => v);

    modelBuilder.Entity<Blog>()
        .Property(e => e.Id)
        .Metadata.SetValueComparer(comparer);

    modelBuilder.Entity<Post>(
        b =>
        {
            b.Property(e => e.Id).Metadata.SetValueComparer(comparer);
            b.Property(e => e.BlogId).Metadata.SetValueComparer(comparer);
        });
}

Múltiples llamadas AddDbContext se aplican en diferente orden

Incidencia de seguimiento n.º 32518

Comportamiento anterior

Anteriormente, cuando se realizaban varias llamadas a AddDbContext, AddDbContextPool, AddDbContextFactory o AddPooledDbContextFactor con el mismo tipo de contexto pero con una configuración conflictiva, ganaba la primera.

Comportamiento nuevo

A partir de EF Core 8.0, la configuración de la última llamada tendrá prioridad.

Por qué

Esto se ha cambiado para ser coherente con el nuevo método ConfigureDbContext que se puede utilizar para añadir configuración antes o después de los métodos Add*.

Mitigaciones

Invertir el orden de las llamadas Add*.

EntityTypeAttributeConventionBase sustituido por TypeAttributeConventionBase

Comportamiento nuevo

En EF Core 8.0 EntityTypeAttributeConventionBase se renomobró a TypeAttributeConventionBase.

Por qué

TypeAttributeConventionBase representa mejor la funcionalidad ya que ahora se puede utilizar para tipos complejos y tipos de entidad.

Mitigaciones

Reemplazar usos EntityTypeAttributeConventionBase por TypeAttributeConventionBase.