Cambios importantes en EF Core 7.0 (EF7)

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 6 a EF Core 7. 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 7.0 está orientado a .NET 6. Esto significa que las aplicaciones existentes orientadas a .NET 6 pueden seguir haciéndolo. Las aplicaciones orientadas a versiones anteriores de .NET, .NET Core y .NET Framework tendrán que orientarse a .NET 6 o .NET 7 para utilizar EF Core 7.0.

Resumen

Cambio importante Impacto
Encrypt por defecto para true conexiones SQL Server Alto
Algunas advertencias volverán a lanzar excepciones por defecto Alto
Las tablas de SQL Server con desencadenadores o determinadas columnas computadas requieren ahora una configuración especial de EF Core Alto
Las tablas SQLite con desencadenadores AFTER y las tablas virtuales requieren ahora una configuración especial de EF Core Alto
Los dependientes huérfanos de relaciones opcionales no se eliminan automáticamente Media
Se configura la eliminación en cascada entre tablas cuando se utiliza el mapeo TPT con SQL Server Media
Mayor probabilidad de errores de ocupado/bloqueado en SQLite cuando no se utiliza el registro de escritura anticipada Media
Puede ser necesario configurar las propiedades de las claves con un comparador de valores del proveedorr Bajo
Ahora las restricciones de verificación y otras facetas de la tabla están configuradas en la tabla Bajo
Las navegaciones desde entidades nuevas a entidades eliminadas no están arregladas Bajo
Utilizar FromSqlRaw y métodos relacionados del proveedor incorrecto lanza Bajo
scaffolding OnConfiguring ya no llama a IsConfigured Bajo

Cambios de impacto alto

Encrypt por defecto true para conexiones SQL Server

Problema de seguimiento: SqlClient #1210

Importante

Se trata de un cambio de ruptura grave en el paquete Microsoft.Data.SqlClient. No hay nada que se pueda hacer en EF Core para revertir o mitigar este cambio. Dirija sus comentarios a Microsoft.Data.SqlClient GitHub Repo o póngase en contacto con un Microsoft Support ProfessionalVisual Studio Professional para preguntas adicionales o ayuda.

Comportamiento anterior

Cadenas de conexión SqlClient utilizan Encrypt=False por defecto. Esto permite conexiones en máquinas de desarrollo donde el servidor local no tiene un certificado válido.

Comportamiento nuevo

Cadenas de conexión SqlClient utilizan Encrypt=True por defecto. Esto significa que:

  • El servidor debe estar configurado con un certificado válido
  • El cliente debe confiar en este certificado

Si no se cumplen estas condiciones, se producirá un SqlException. Por ejemplo:

Se estableció correctamente una conexión con el servidor, pero luego se produjo un error durante el proceso de inicio de sesión. (proveedor: Proveedor SSL, error: 0 - La cadena de certificados fue emitida por una autoridad que no es de confianza).

Por qué

Este cambio se realizó para garantizar que, por defecto, o la conexión es segura o la aplicación fallará al conectarse.

Mitigaciones

Hay tres formas de proceder:

  1. Instalar un certificado válido en el servidor. Tenga en cuenta que se trata de un proceso complejo que requiere obtener un certificado y asegurarse de que está firmado por una autoridad en la que confía el cliente.
  2. Si el servidor tiene un certificado, pero no es de confianza para el cliente, a continuación TrustServerCertificate=True para permitir eludir los mecanismos normales de confianza.
  3. Agregar explícitamente Encrypt=False a la cadena de conexión.

Advertencia

Las opciones 2 y 3 dejan al servidor en un estado potencialmente inseguro.

Algunos avisos vuelven a lanzar excepciones por defecto

Incidencia de seguimiento nº 29069

Comportamiento anterior

En EF Core 6.0, un error en el proveedor de SQL Server hacía que algunas advertencias configuradas para lanzar excepciones de forma predeterminada se registraran pero no lanzaran excepciones. Estas advertencias son:

EventId Descripción
RelationalEventId.AmbientTransactionWarning Una aplicación puede haber esperado que se utilizara una transacción ambiental cuando en realidad fue ignorada.
RelationalEventId.IndexPropertiesBothMappedAndNotMappedToTable Un índice especifica propiedades, algunas de las cuales están asignadas y otras no, a una columna de una tabla.
RelationalEventId.IndexPropertiesMappedToNonOverlappingTables Un índice especifica propiedades que se asignan a columnas de tablas que no están superpuestas.
RelationalEventId.ForeignKeyPropertiesMappedToUnrelatedTables Una clave externa especifica propiedades que no se asignan a las tablas relacionadas.

Comportamiento nuevo

A partir de EF Core 7.0, estas advertencias vuelven a provocar, por defecto, el lanzamiento de una excepción.

Por qué

Se trata de problemas que muy probablemente indican un error en el código de la aplicación que debería corregirse.

Mitigaciones

Corrija el problema subyacente que motiva la advertencia.

Alternativamente, se puede cambiar el nivel de advertencia para que solo se registre o se suprima por completoy. Por ejemplo:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .ConfigureWarnings(b => b.Ignore(RelationalEventId.AmbientTransactionWarning));

Las tablas de SQL Server con desencadenadores o determinadas columnas computadas requieren ahora una configuración especial de EF Core

Incidencia de seguimiento nº 27372

Comportamiento anterior

Las versiones anteriores del proveedor de SQL Server guardaban los cambios mediante una técnica menos eficaz que siempre funcionaba.

Comportamiento nuevo

Por defecto, EF Core ahora guarda los cambios a través de una técnica significativamente más eficiente; desafortunadamente, esta técnica no es compatible con SQL Server si la tabla de destino tiene triggers de base de datos, o ciertos tipos de columnas computadas. Consulte la documentación de SQL Server para obtener más detalles.

Por qué

Las mejoras de rendimiento ligadas al nuevo método son lo suficientemente significativas como para que sea importante ofrecerlas a los usuarios por defecto. Al mismo tiempo, estimamos que el uso de disparadores de bases de datos o de las columnas calculadas afectadas en las aplicaciones EF Core es lo suficientemente bajo como para que las consecuencias negativas de los cambios de rotura se vean compensadas por la ganancia de rendimiento.

Mitigaciones

A partir de EF Core 8.0, el uso o no de la cláusula "OUTPUT" se puede configurar explícitamente. Por ejemplo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .ToTable(tb => tb.UseSqlOutputClause(false));
}

En EF7 o posterior, si la tabla de destino tiene un desencadenador, puede informar a EF Core de conocerlo y EF revertirá a la técnica anterior, menos eficaz. Esto puede hacerse configurando el tipo de entidad correspondiente del siguiente modo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .ToTable(tb => tb.HasTrigger("SomeTrigger"));
}

Tenga en cuenta que esto no hace que EF Core cree o administre el desencadenador de ninguna manera: actualmente solo informa a EF Core de que hay desencadenadores presentes en la tabla. Como resultado, se puede usar cualquier nombre de desencadenador. Especificar un desencadenador se puede usar para revertir el comportamiento anterior incluso si realmente no hay un desencadenador en la tabla.

Si la mayoría o todas sus tablas tienen desencadenadores, puede optar por no utilizar la técnica más reciente y eficaz para todas las tablas de su modelo utilizando la siguiente convención de creación de modelos:

public class BlankTriggerAddingConvention : IModelFinalizingConvention
{
    public virtual void ProcessModelFinalizing(
        IConventionModelBuilder modelBuilder,
        IConventionContext<IConventionModelBuilder> context)
    {
        foreach (var entityType in modelBuilder.Metadata.GetEntityTypes())
        {
            var table = StoreObjectIdentifier.Create(entityType, StoreObjectType.Table);
            if (table != null
                && entityType.GetDeclaredTriggers().All(t => t.GetDatabaseName(table.Value) == null)
                && (entityType.BaseType == null
                    || entityType.GetMappingStrategy() != RelationalAnnotationNames.TphMappingStrategy))
            {
                entityType.Builder.HasTrigger(table.Value.Name + "_Trigger");
            }

            foreach (var fragment in entityType.GetMappingFragments(StoreObjectType.Table))
            {
                if (entityType.GetDeclaredTriggers().All(t => t.GetDatabaseName(fragment.StoreObject) == null))
                {
                    entityType.Builder.HasTrigger(fragment.StoreObject.Name + "_Trigger");
                }
            }
        }
    }
}

Utilice la convención en su DbContext anulando ConfigureConventions:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Add(_ => new BlankTriggerAddingConvention());
}

De este modo, se accede a todas las tablas del modelo HasTrigger, en lugar de tener que hacerlo manualmente para cada una de ellas.

Las tablas SQLite con desencadenadores AFTER y las tablas virtuales requieren ahora una configuración especial de EF Core

Incidencia de seguimiento nº 29916

Comportamiento anterior

Las versiones anteriores del proveedor de SQLite guardaban los cambios mediante una técnica menos eficiente que siempre funcionaba.

Comportamiento nuevo

Por defecto, EF Core ahora guarda los cambios a través de una técnica más eficiente, utilizando la cláusula RETURNING. Desafortunadamente, esta técnica no es compatible con SQLite si la tabla de destino tiene triggers AFTER de base de datos, es virtual o si se están utilizando versiones antiguas de SQLite. Consulte la documentación de SQLite para más detalles.

Por qué

Las simplificaciones y mejoras de rendimiento ligadas al nuevo método son lo suficientemente significativas como para que sea importante ponerlas a disposición de los usuarios por defecto. Al mismo tiempo, estimamos que el uso de desencadenadores de bases de datos y tablas virtuales en aplicaciones EF Core es lo suficientemente bajo como para que las consecuencias negativas de los cambios de rotura se vean compensadas por la ganancia de rendimiento.

Mitigaciones

En EF Core 8.0, el método UseSqlReturningClause se ha introducido para volver explícitamente a la versión anterior y menos eficaz de SQL. Por ejemplo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .ToTable(tb => tb.UseSqlReturningClause(false));
}

Si todavía usa EF Core 7.0, es posible revertir al mecanismo anterior para toda la aplicación insertando el código siguiente en la configuración del contexto:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseSqlite(...)
        .ReplaceService<IUpdateSqlGenerator, SqliteLegacyUpdateSqlGenerator>();

Cambios de impacto medio

Los dependientes huérfanos de relaciones opcionales no se eliminan automáticamente

Incidencia de seguimiento nº 27217

Comportamiento anterior

Una relación es opcional si su clave externa es anulable. Establecer la clave externa como nula permite que la entidad dependiente exista sin ninguna entidad principal relacionada. Las relaciones opcionales pueden ser configuradas para usar eliminación en cascada, aunque esto no es lo predeterminado.

Un dependiente opcional puede separarse de su principal estableciendo su clave externa en null, o borrando la navegación hacia o desde él. En EF Core 6.0, esto provocaba la eliminación del dependiente cuando la relación estaba configurada para la eliminación en cascada.

Comportamiento nuevo

A partir de EF Core 7.0, el dependiente ya no se elimina. Tenga en cuenta que si se elimina el principal, el dependiente también se eliminará, ya que la relación está configurada para la eliminación en cascada.

Por qué

El dependiente puede existir sin ninguna relación con un principal, por lo que la ruptura de la relación no debería provocar la eliminación de la entidad.

Mitigaciones

El dependiente puede borrarse explícitamente:

context.Remove(blog);

O SaveChanges puede ser anulado o interceptado para eliminar dependientes sin referencia principal. Por ejemplo:

context.SavingChanges += (c, _) =>
    {
        foreach (var entry in ((DbContext)c!).ChangeTracker
            .Entries<Blog>()
            .Where(e => e.State == EntityState.Modified))
        {
            if (entry.Reference(e => e.Author).CurrentValue == null)
            {
                entry.State = EntityState.Deleted;
            }
        }
    };

Se configura la eliminación en cascada entre tablas cuando se utiliza el mapeo TPT con SQL Server

Incidencia de seguimiento nº 28532

Comportamiento anterior

Cuando se asigna una jerarquía de herencia utilizando la estrategia TPT, la tabla base debe contener una fila para cada entidad guardada, independientemente del tipo real de dicha entidad. La eliminación de la fila de la tabla base debería eliminar las filas de todas las demás tablas. EF Core configura una eliminación en cascada para esto.

En EF Core 6.0, un error en el proveedor de bases de datos SQL Server impedía la creación de estas eliminaciones en cascada.

Comportamiento nuevo

A partir de EF Core 7.0, las eliminaciones en cascada se crean ahora para SQL Server como siempre se han hecho para otras bases de datos.

Por qué

La eliminación en cascada desde la tabla base a las subtablas en TPT permite eliminar una entidad borrando su fila en la tabla base.

Mitigaciones

En la mayoría de los casos, este cambio no debería causar ningún problema. Sin embargo, SQL Server es muy restrictivo cuando hay múltiples comportamientos en cascada configurados entre tablas. Esto significa que si existe una relación en cascada entre tablas en la asignación TPT, SQL Server puede generar el siguiente error:

Microsoft.Data.SqlClient.SqlException: La sentencia DELETE entró en conflicto con la restricción REFERENCE "FK_Blogs_People_OwnerId". El conflicto ha aparecido en la base de datos "Scratch", tabla "dbo.Blogs", columna "OwnerId". Se terminó la instrucción.

Por ejemplo, este modelo crea un ciclo de relaciones en cascada:

[Table("FeaturedPosts")]
public class FeaturedPost : Post
{
    public int ReferencePostId { get; set; }
    public Post ReferencePost { get; set; } = null!;
}

[Table("Posts")]
public class Post
{
    public int Id { get; set; }
    public string? Title { get; set; }
    public string? Content { get; set; }
}

Uno de ellos tendrá que ser configurado para no utilizar eliminaciones en cascada en el servidor. Por ejemplo, para cambiar la relación explícita:

modelBuilder
    .Entity<FeaturedPost>()
    .HasOne(e => e.ReferencePost)
    .WithMany()
    .OnDelete(DeleteBehavior.ClientCascade);

O para cambiar la relación implícita creada para la asignación TPT:

modelBuilder
    .Entity<FeaturedPost>()
    .HasOne<Post>()
    .WithOne()
    .HasForeignKey<FeaturedPost>(e => e.Id)
    .OnDelete(DeleteBehavior.ClientCascade);

Mayor probabilidad de errores de ocupado/bloqueado en SQLite cuando no se utiliza el registro de escritura anticipada

Comportamiento anterior

Las versiones anteriores del proveedor SQLite guardaron los cambios a través de una técnica menos eficiente que podía reintentar automáticamente cuando la tabla estaba bloqueada/ocupada y el registro de escritura anticipada (WAL) no estaba habilitado.

Comportamiento nuevo

Por defecto, EF Core ahora guarda los cambios a través de una técnica más eficiente, utilizando la cláusula RETURNING. Desafortunadamente, esta técnica no puede reintentar automáticamente cuando está ocupado/bloqueado. En una aplicación multiproceso (como una aplicación web) que no utiliza el registro de escritura anticipada, es común encontrar estos errores.

Por qué

Las simplificaciones y mejoras de rendimiento ligadas al nuevo método son lo suficientemente significativas como para que sea importante ponerlas a disposición de los usuarios por defecto. Las bases de datos creadas por EF Core también habilitan el registro de escritura anticipada de forma predeterminada. El equipo de SQLite también recomienda habilitar el registro de escritura anticipada de forma predeterminada.

Mitigaciones

Si es posible, debe habilitar el registro de escritura anticipada en su base de datos. Si su base de datos fue creada por EF, este ya debería ser el caso. De lo contrario, puede habilitar el registro de escritura anticipada ejecutando el siguiente comando.

PRAGMA journal_mode = 'wal';

Si, por algún motivo, no puede habilitar el registro de escritura anticipada, es posible volver al mecanismo anterior para toda la aplicación insertando el siguiente código en su configuración de contexto:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseSqlite(...)
        .ReplaceService<IUpdateSqlGenerator, SqliteLegacyUpdateSqlGenerator>();

Cambios de impacto bajo

Puede ser necesario configurar las propiedades de las claves con un comparador de valores del proveedor

Incidencia de seguimiento nº 27738

Comportamiento anterior

En EF Core 6.0, los valores clave tomados directamente de las propiedades de los tipos de entidad se utilizaban para comparar los valores clave al guardar los cambios. Esto haría uso de cualquier comparador de valores personalizado configurado en estas propiedades.

Comportamiento nuevo

A partir de EF Core 7.0, se utilizan los valores de la base de datos para estas comparaciones. Esto "simplemente funciona" en la inmensa mayoría de los casos. Sin embargo, si las propiedades estaban utilizando un comparador personalizado, y ese comparador no se puede aplicar a los valores de la base de datos, entonces puede ser necesario un "comparador de valores del proveedor", como se muestra a continuación.

Por qué

Varias divisiones de entidades y tablas pueden dar lugar a múltiples propiedades asignadas a la misma columna de la base de datos, y viceversa. Esto requiere que los valores se comparen después de la conversión al valor que se utilizará en la base de datos.

Mitigaciones

Configurar un comparador de valores de proveedores. Por ejemplo, considere el caso en el que un objeto de valor se está utilizando como clave, y el comparador para esa clave utiliza comparaciones de cadenas insensibles a mayúsculas/minúsculas:

var blogKeyComparer = new ValueComparer<BlogKey>(
    (l, r) => string.Equals(l.Id, r.Id, StringComparison.OrdinalIgnoreCase),
    v => v.Id.ToUpper().GetHashCode(),
    v => v);

var blogKeyConverter = new ValueConverter<BlogKey, string>(
    v => v.Id,
    v => new BlogKey(v));

modelBuilder.Entity<Blog>()
    .Property(e => e.Id).HasConversion(
        blogKeyConverter, blogKeyComparer);

Los valores de la base de datos (cadenas) no pueden utilizar directamente el comparador definido para los tipos BlogKey. Por lo tanto, debe configurarse un comparador de proveedores para comparaciones de cadenas sin distinción entre mayúsculas y minúsculas:

var caseInsensitiveComparer = new ValueComparer<string>(
    (l, r) => string.Equals(l, r, StringComparison.OrdinalIgnoreCase),
    v => v.ToUpper().GetHashCode(),
    v => v);

var blogKeyComparer = new ValueComparer<BlogKey>(
    (l, r) => string.Equals(l.Id, r.Id, StringComparison.OrdinalIgnoreCase),
    v => v.Id.ToUpper().GetHashCode(),
    v => v);

var blogKeyConverter = new ValueConverter<BlogKey, string>(
    v => v.Id,
    v => new BlogKey(v));

modelBuilder.Entity<Blog>()
    .Property(e => e.Id).HasConversion(
        blogKeyConverter, blogKeyComparer, caseInsensitiveComparer);

Ahora las restricciones de verificación y otras facetas de la tabla están configuradas en la tabla

Incidencia de seguimiento nº 28205

Comportamiento anterior

En EF Core 6.0, HasCheckConstraint, HasComment, y IsMemoryOptimized se llamaban directamente en el creador de tipos de entidad. Por ejemplo:

modelBuilder
    .Entity<Blog>()
    .HasCheckConstraint("CK_Blog_TooFewBits", "Id > 1023");

modelBuilder
    .Entity<Blog>()
    .HasComment("It's my table, and I'll delete it if I want to.");

modelBuilder
    .Entity<Blog>()
    .IsMemoryOptimized();

Comportamiento nuevo

A partir de EF Core 7.0, estos métodos se invocan en el generador de tablas:

modelBuilder
    .Entity<Blog>()
    .ToTable(b => b.HasCheckConstraint("CK_Blog_TooFewBits", "Id > 1023"));

modelBuilder
    .Entity<Blog>()
    .ToTable(b => b.HasComment("It's my table, and I'll delete it if I want to."));

modelBuilder
    .Entity<Blog>()
    .ToTable(b => b.IsMemoryOptimized());

Los métodos existentes se han marcado como Obsolete. Actualmente tienen el mismo comportamiento que los nuevos métodos, pero se eliminarán en una futura versión.

Por qué

Estas facetas solo se aplican a las tablas. No se aplicarán a ninguna vista, función o procedimiento almacenado asignados.

Mitigaciones

Utilice los métodos de creación de tablas, como se muestra arriba.

Incidencia de seguimiento nº 28249

Comportamiento anterior

En EF Core 6.0, cuando se rastrea una nueva entidad, ya sea a partir de una consulta de rastreo o adjuntándola al DbContext, las navegaciones hacia y desde entidades relacionadas en el Deleted estado se reparan.

Comportamiento nuevo

A partir de EF Core 7.0, las navegaciones hacia y desde las entidades Deleted no están arregladas.

Por qué

Una vez que una entidad se marca como Deleted rara vez tiene sentido asociarla con entidades no eliminadas.

Mitigaciones

Consulte o adjunte entidades antes de marcarlas como Deleted, o establezca manualmente las propiedades de navegación hacia y desde la entidad eliminada.

Incidencia de seguimiento nº 26502

Comportamiento anterior

En EF Core 6.0, el uso del método de extensión Azure Cosmos DB FromSqlRaw cuando se utiliza un proveedor relacional, o el método de extensión relacional FromSqlRaw cuando se utiliza el proveedor Azure Cosmos DB podría fallar silenciosamente. Del mismo modo, el uso de métodos relacionales en el proveedor en memoria es una operación sin operación silenciosa.

Comportamiento nuevo

A partir de EF Core 7.0, el uso de un método de extensión diseñado para un proveedor en otro proveedor producirá una excepción.

Por qué

Debe utilizarse el método de extensión correcto para que funcione correctamente en todas las situaciones.

Mitigaciones

Utilice el método de extensión correcto para el proveedor utilizado. Si se hace referencia a varios proveedores, llame al método de extensión como método estático. Por ejemplo:

var result = CosmosQueryableExtensions.FromSqlRaw(context.Blogs, "SELECT ...").ToList();

O:

var result = RelationalQueryableExtensions.FromSqlRaw(context.Blogs, "SELECT ...").ToList();

Scaffolded OnConfiguring ya no llama IsConfigured

Incidencia de seguimiento nº 4274

Comportamiento anterior

En EF Core 6.0, el tipo DbContext creado a partir de una base de datos existente contenía una llamada a IsConfigured. Por ejemplo:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    if (!optionsBuilder.IsConfigured)
    {
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263.
        optionsBuilder.UseNpgsql("MySecretConnectionString");
    }
}

Comportamiento nuevo

A partir de EF Core 7.0, ya no se incluye la llamada a IsConfigured.

Por qué

Hay escenarios muy limitados donde el proveedor de base de datos se configura dentro de su DbContext en algunos casos, pero solo si el contexto no está configurado ya. En cambio, si se deja OnConfiguring aquí es más probable que se deje en el código una cadena de conexión que contenga información confidencial, a pesar de la advertencia en tiempo de compilación. Por lo tanto, la seguridad adicional y el código más limpio de la eliminación de este se consideró que valía la pena, sobre todo teniendo en cuenta que el --no-onconfiguring (.NET CLI) o -NoOnConfiguring (Visual Studio Package Manager Console) bandera se puede utilizar para evitar el andamiaje del método OnConfiguring, y que existen plantillas personalizables para agregar de nuevo IsConfigured si es realmente necesario.

Mitigaciones

O bien:

  • Utilice el argumento --no-onconfiguring (.NET CLI) o -NoOnConfiguring (Visual Studio Package Manager Console) al crear un andamiaje a partir de una base de datos existente.
  • Personalice las plantillas T4 para volver a agregar la llamada a IsConfigured.