Alterações significativas no EF Core 7.0 (EF7)

Essa página documenta as alterações de comportamento e a API que têm o potencial de interromper aplicativos existentes ao atualizar do EF Core 6 para o EF Core 7. Verifique as alterações interruptivas anteriores se estiver atualizando a partir de uma versão anterior do Entity Framework Core:

Estrutura de Destino

O EF Core 7.0 tem como destino o .NET 6. Isso significa que os aplicativos existentes destinados ao .NET 6 podem continuar a fazê-lo. Os aplicativos destinados a versões mais antigas do .NET, do .NET Core e do .NET Framework precisarão ser direcionados ao .NET 6 ou ao .NET 7 para usar o EF Core 7.0.

Resumo

Alterações da falha Impacto
Encrypt o padrão para true conexões do SQL Server Alto
Alguns avisos lançarão novamente exceções por padrão Alto
Tabelas do SQL Server com gatilhos ou determinadas colunas computadas agora exigem uma configuração especial do EF Core Alto
As tabelas SQLite com gatilhos AFTER e tabelas virtuais agora exigem uma configuração especial do EF Core Alto
Dependentes órfãos de relações opcionais não são excluídos automaticamente Médio
A exclusão em cascata é configurada entre tabelas ao usar o mapeamento TPT com o SQL Server Médio
Maior chance de erros ocupados/bloqueados no SQLite quando não estiver usando o log write-ahead Médio
As propriedades de chave podem precisar ser configuradas com um comparador de valor de provedor Baixo
As restrições de verificação e outras facetas de tabela agora estão configuradas na tabela Baixo
As navegações de novas entidades para entidades excluídas não são corrigidas Baixo
Usar FromSqlRaw e métodos relacionados do provedor incorreto é gerado Baixo
Com andaimes OnConfiguring não é mais chamado IsConfigured Baixo

Alterações de alto impacto

Encrypt o padrão para true conexões do SQL Server

Problema de acompanhamento: SqlClient #1210

Importante

Essa é uma alteração significativa severa no pacote Microsoft.Data.SqlClient. Não há nada que possa ser feito no EF Core para reverter ou atenuar essa alteração. Envie comentários diretos para o Repositório GitHub Microsoft.Data.SqlClient ou entre em contato com um Profissional de Suporte da Microsoft para obter mais perguntas ou ajuda.

Comportamento antigo

As cadeias de conexão do SqlClient usam Encrypt=False por padrão. Isso permite conexões em computadores de desenvolvimento em que o servidor local não tem um certificado válido.

Novo comportamento

As cadeias de conexão do SqlClient usam Encrypt=True por padrão. Isso significa que:

  • O servidor deve ser configurado com um certificado válido
  • O cliente deve confiar nesse certificado

Se essas condições não forem atendidas, uma SqlException será lançada. Por exemplo:

Uma conexão com o servidor foi estabelecida com êxito, mas ocorreu um erro durante o processo de logon. (provedor: provedor SSL, erro: 0 – a cadeia de certificados foi emitida por uma autoridade que não é confiável.)

Por que

Essa alteração foi feita para garantir que, por padrão, a conexão seja segura ou o aplicativo não se conecte.

Atenuações

Há três maneiras de continuar:

  1. Instale um certificado válido no servidor. Observe que esse é um processo envolvido e requer a obtenção de um certificado e a garantia de que ele seja assinado por uma autoridade confiável pelo cliente.
  2. Se o servidor tiver um certificado, mas não for confiável pelo cliente, permita TrustServerCertificate=True ignorar os mecanismos normais de confiança.
  3. Adicione explicitamente Encrypt=False à cadeia de conexão.

Aviso

As opções 2 e 3 deixam o servidor em um estado potencialmente inseguro.

Alguns avisos geram exceções por padrão novamente

Problema de acompanhamento nº 29069

Comportamento antigo

No EF Core 6.0, um bug no provedor do SQL Server significava que alguns avisos configurados para gerar exceções por padrão estavam sendo registrados, mas não geravam exceções. Estes avisos são:

EventId Descrição
RelationalEventId.AmbientTransactionWarning Um aplicativo pode ter esperado que uma transação ambiente fosse usada quando ela fosse realmente ignorada.
RelationalEventId.IndexPropertiesBothMappedAndNotMappedToTable Um índice especifica as propriedades algumas das quais são mapeadas e algumas das quais não são mapeadas para uma coluna em uma tabela.
RelationalEventId.IndexPropertiesMappedToNonOverlappingTables Um índice especifica as propriedades que são mapeadas para colunas em tabelas não sobrepostas.
RelationalEventId.ForeignKeyPropertiesMappedToUnrelatedTables Uma chave estrangeira especifica propriedades que não são mapeadas para as tabelas relacionadas.

Novo comportamento

A partir do EF Core 7.0, esses avisos novamente, por padrão, resultam na geração de uma exceção.

Por que

Esses são problemas que provavelmente indicam um erro no código do aplicativo que deve ser corrigido.

Atenuações

Corrija o problema subjacente que é o motivo do aviso.

Como alternativa, o nível de aviso pode ser alterado para que seja registrado somente ou suprimido inteiramente. Por exemplo:

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

Tabelas do SQL Server com gatilhos ou determinadas colunas computadas agora exigem uma configuração especial do EF Core

Problema de acompanhamento nº 27372

Comportamento antigo

As versões anteriores do provedor do SQL Server salvaram as alterações por meio de uma técnica menos eficiente que sempre funcionou.

Novo comportamento

Por padrão, o EF Core agora salva as alterações por meio de uma técnica significativamente mais eficiente; infelizmente, essa técnica não terá suporte no SQL Server se a tabela de destino tiver gatilhos de banco de dados ou determinados tipos de colunas computadas. Consulte a documentação do SQL Server para obter mais detalhes.

Por que

As melhorias de desempenho vinculadas ao novo método são significativas o suficiente para que seja importante trazê-las aos usuários por padrão. Ao mesmo tempo, estimamos que o uso de gatilhos de banco de dados ou as colunas computadas afetadas em aplicativos EF Core seja baixo o suficiente para que as consequências negativas da alteração de interrupção sejam superadas pelo ganho de desempenho.

Mitigações

A partir do EF Core 8.0, o uso ou não da cláusula "OUTPUT" pode ser configurado explicitamente. Por exemplo:

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

No EF7 ou versões posteriores, se a tabela de destino tiver um gatilho, você poderá permitir que o EF Core saiba disso e o EF será revertido para a técnica anterior, menos eficiente. Isso pode ser feito configurando o tipo de entidade correspondente da seguinte maneira:

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

Observe que fazer isso não faz com que o EF Core crie ou gerencie o gatilho de forma alguma. Atualmente, ele apenas informa ao EF Core que os gatilhos estão presentes na tabela. Como resultado, qualquer nome de gatilho pode ser usado. A especificação de um gatilho pode ser usada para reverter o comportamento antigo mesmo que não haja realmente um gatilho na tabela.

Se a maioria ou todas as tabelas tiverem gatilhos, você poderá recusar o uso da técnica mais recente e eficiente para todas as tabelas do modelo usando a seguinte convenção de compilação de modelo:

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");
                }
            }
        }
    }
}

Use a convenção em seu DbContext substituindo o ConfigureConventions:

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

Isso efetivamente chama HasTrigger todas as tabelas do modelo, em vez de você precisar fazer isso manualmente para cada tabela.

As tabelas SQLite com gatilhos AFTER e tabelas virtuais agora exigem uma configuração especial do EF Core

Problema de acompanhamento nº 29916

Comportamento antigo

As versões anteriores do provedor SQLite salvaram as alterações por meio de uma técnica menos eficiente que sempre funcionou.

Novo comportamento

Por padrão, o EF Core agora salva as alterações por meio de uma técnica mais eficiente, usando a cláusula RETURNING. Infelizmente, essa técnica não terá suporte no SQLite se a tabela de destino tiver gatilhos AFTER do banco de dados, for virtual ou se versões mais antigas do SQLite estiverem sendo usadas. Consulte a documentação do SQLite para obter mais detalhes.

Por que

As simplificações e melhorias de desempenho vinculadas ao novo método são significativas o suficiente para que seja importante trazê-las aos usuários por padrão. Ao mesmo tempo, estimamos que o uso de gatilhos de banco de dados e tabelas virtuais em aplicativos EF Core seja baixo o suficiente para que as consequências negativas da alteração de interrupção sejam superadas pelo ganho de desempenho.

Mitigações

No EF Core 8.0, o método UseSqlReturningClause foi introduzido para reverter explicitamente para o SQL mais antigo e menos eficiente. Por exemplo:

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

Se você ainda estiver usando o EF Core 7.0, é possível reverter para o mecanismo antigo de todo o aplicativo inserindo o seguinte código em sua configuração de contexto:

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

Alterações de impacto médio

Dependentes órfãos de relações opcionais não são excluídos automaticamente

Problema de acompanhamento nº 27217

Comportamento antigo

Uma relação será opcional se sua chave estrangeira for anulável. A definição da chave estrangeira como nula permite que a entidade dependente exista sem nenhuma entidade principal relacionada. Relações opcionais podem ser configuradas para usar exclusões em cascata, embora esse não seja o padrão.

Um dependente opcional pode ser cortado de sua entidade de segurança definindo sua chave estrangeira como nula ou desmarcando a navegação para ou a partir dela. No EF Core 6.0, isso faria com que o dependente fosse excluído quando a relação fosse configurada para exclusão em cascata.

Novo comportamento

A partir do EF Core 7.0, o dependente não é mais excluído. Observe que, se a entidade de segurança for excluída, o dependente ainda será excluído, pois as exclusões em cascata serão configuradas para a relação.

Por que

O dependente pode existir sem qualquer relação com uma entidade de segurança, portanto, cortar a relação não deve fazer com que a entidade seja excluída.

Atenuações

O dependente pode ser excluído explicitamente:

context.Remove(blog);

Ou SaveChanges pode ser substituído ou interceptado para excluir dependentes sem referência principal. Por exemplo:

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;
            }
        }
    };

A exclusão em cascata é configurada entre tabelas ao usar o mapeamento TPT com o SQL Server

Problema de acompanhamento nº 28532

Comportamento antigo

Ao mapear uma hierarquia de herança usando a estratégia TPT, a tabela base deve conter uma linha para cada entidade salva, independentemente do tipo real dessa entidade. Excluir a linha na tabela base deve excluir linhas em todas as outras tabelas. O EF Core configura uma exclusão em cascata para isso.

No EF Core 6.0, um bug no provedor de banco de dados do SQL Server significava que essas exclusões em cascata não estavam sendo criadas.

Novo comportamento

A partir do EF Core 7.0, as exclusões em cascata agora estão sendo criadas para o SQL Server, assim como sempre foram para outros bancos de dados.

Por que

As exclusões em cascata da tabela base para as sub-tabelas no TPT permitem que uma entidade seja excluída excluindo sua linha na tabela base.

Atenuações

Na maioria dos casos, essa alteração não deve causar problemas. No entanto, o SQL Server é muito restritivo quando há vários comportamentos em cascata configurados entre tabelas. Isso significa que, se houver uma relação em cascata existente entre tabelas no mapeamento TPT, o SQL Server poderá gerar o seguinte erro:

Microsoft.Data.SqlClient.SqlException: A instrução DELETE entrou em conflito com a restrição REFERENCE “FK_Blogs_People_OwnerId”. O conflito ocorreu no banco de dados “Scratch”, na tabela “dbo.Blogs”, na coluna ‘OwnerId’. A instrução foi finalizada.

Por exemplo, esse modelo cria um ciclo de relações em cascata:

[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; }
}

Um deles precisará ser configurado para não usar exclusões em cascata no servidor. Por exemplo, para alterar a relação explícita:

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

Ou para alterar a relação implícita criada para o mapeamento TPT:

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

Maior chance de erros ocupados/bloqueados no SQLite quando não estiver usando o log write-ahead

Comportamento antigo

As versões anteriores do provedor SQLite salvavam as alterações por meio de uma técnica menos eficiente que era capaz de repetir automaticamente quando a tabela estava bloqueada/ocupada e o registro em log de gravação antecipada (WAL) não estava habilitado.

Novo comportamento

Por padrão, o EF Core agora salva as alterações por meio de uma técnica mais eficiente, usando a cláusula RETURNING. Infelizmente, essa técnica não é capaz de repetir automaticamente quando ocupada/bloqueada. Em um aplicativo multithread (como um aplicativo Web) que não usa o log de gravação antecipada, é comum encontrar esses erros.

Por que

As simplificações e melhorias de desempenho vinculadas ao novo método são significativas o suficiente para que seja importante trazê-las aos usuários por padrão. Os bancos de dados criados pelo EF Core também habilitam o log de gravação antecipada por padrão. A equipe do SQLite também recomenda habilitar o log de gravação antecipada por padrão.

Mitigações

Se possível, você deve habilitar o log de gravação antecipada em seu banco de dados. Se o banco de dados foi criado pelo EF, esse já deve ser o caso. Caso contrário, você pode habilitar o log write-ahead executando o comando a seguir.

PRAGMA journal_mode = 'wal';

Se, por algum motivo, você não puder habilitar o log write-ahead, é possível reverter para o mecanismo antigo para todo o aplicativo inserindo o seguinte código em sua configuração de contexto:

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

Alterações de baixo impacto

As propriedades de chave podem precisar ser configuradas com um comparador de valor de provedor

Problema de acompanhamento nº 27738

Comportamento antigo

No EF Core 6.0, os valores de chave obtidos diretamente das propriedades dos tipos de entidade foram usados para comparação de valores de chave ao salvar alterações. Isso faria uso de qualquer comparador de valor personalizado configurado nessas propriedades.

Novo comportamento

A partir do EF Core 7.0, os valores de banco de dados são usados para essas comparações. Este "só funciona" para a grande maioria dos casos. No entanto, se as propriedades estiverem usando um comparador personalizado e esse comparador não puder ser aplicado aos valores do banco de dados, um "comparador de valor do provedor" poderá ser necessário, conforme mostrado abaixo.

Por que

Várias divisões de entidade e divisão de tabelas podem resultar em várias propriedades mapeadas para a mesma coluna de banco de dados e vice-versa. Isso requer que os valores sejam comparados após a conversão em valor que será usado no banco de dados.

Atenuações

Configure um comparador de valor de provedor. Por exemplo, considere o caso em que um objeto de valor está sendo usado como uma chave e o comparador dessa chave usa comparações de cadeia de caracteres que não diferenciam maiúsculas de 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);

Os valores de banco de dados (cadeias de caracteres) não podem usar diretamente o comparador definido para tipos BlogKey. Portanto, um comparador de provedor para comparações de cadeia de caracteres que não diferencia maiúsculas de minúsculas deve ser configurado:

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);

As restrições de verificação e outras facetas de tabela agora estão configuradas na tabela

Problema de acompanhamento nº 28205

Comportamento antigo

No EF Core 6.0, HasCheckConstraint, HasComment e IsMemoryOptimized foram chamados diretamente no construtor de tipos de entidade. Por exemplo:

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();

Novo comportamento

A partir do EF Core 7.0, esses métodos são chamados no construtor de tabelas:

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());

Os métodos existentes foram marcados como Obsolete. Atualmente, eles têm o mesmo comportamento dos novos métodos, mas serão removidos em uma versão futura.

Por que

Essas facetas se aplicam somente a tabelas. Elas não serão aplicadas a exibições mapeadas, funções ou procedimentos armazenados.

Atenuações

Use os métodos do construtor de tabelas, conforme mostrado acima.

Problema de acompanhamento nº 28249

Comportamento antigo

No EF Core 6.0, quando uma nova entidade é rastreada de uma consulta de acompanhamento ou anexá-la ao DbContext, as navegações de e para entidades relacionadas no Deletedestado são corrigidas.

Novo comportamento

A partir do EF Core 7.0, as navegações de Deleted e para entidades não são corrigidas.

Por que

Depois que uma entidade é marcada, pois Deleted raramente faz sentido associá-la a entidades não excluídas.

Atenuações

Consultar ou anexar entidades antes de marcar entidades como Deleted, ou definir manualmente propriedades de navegação de e para a entidade excluída.

Problema de acompanhamento nº 26502

Comportamento antigo

No EF Core 6.0, usar o método de extensão do Azure Cosmos DB FromSqlRaw ao usar um provedor relacional ou o método de extensão relacional FromSqlRaw ao usar o provedor do Azure Cosmos DB pode falhar silenciosamente. Da mesma forma, o uso de métodos relacionais no provedor na memória não terá operações de forma silenciosa.

Novo comportamento

A partir do EF Core 7.0, o uso de um método de extensão projetado para um provedor em um provedor diferente irá gerar uma exceção.

Por que

O método de extensão correto deve ser usado para funcionar corretamente em todas as situações.

Atenuações

Use o método de extensão correto para o provedor que está sendo usado. Se vários provedores forem referenciados, chame o método de extensão como um método estático. Por exemplo:

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

Ou:

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

Scaffolded OnConfiguring não é mais chamado IsConfigured

Problema de acompanhamento nº 4274

Comportamento antigo

No EF Core 6.0, o tipo DbContext estruturado de um banco de dados existente continha uma chamada para IsConfigured. Por exemplo:

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");
    }
}

Novo comportamento

A partir do EF Core 7.0, a chamada para IsConfigured não está mais incluída.

Por que

Há cenários muito limitados em que o provedor de banco de dados está configurado dentro de seu DbContext em alguns casos, mas somente se o contexto ainda não estiver configurado. Em vez disso, deixar OnConfiguring aqui torna mais provável que uma cadeia de conexão contendo informações confidenciais seja deixada no código, apesar do aviso de tempo de compilação. Assim, considerou-se que o código mais seguro e mais limpo resultante da remoção valia a pena, especialmente considerando que o sinalizador --no-onconfiguring (.NET CLI) ou -NoOnConfiguring (Console do Visual Studio Package Manager) pode ser usado para evitar o scaffolding do método OnConfiguring e que existem modelos personalizáveis para adicionar de volta IsConfigured se for realmente necessário.

Atenuações

Qualquer um:

  • Use o argumento --no-onconfiguring (CLI do .NET) ou -NoOnConfiguring (Console do Gerenciador de Pacotes do Visual Studio) durante o scaffolding de um banco de dados existente.
  • Personalize os modelos T4 para adicionar novamente a chamada ao IsConfigured.