Compartilhar via


Alterações significativas no EF Core 10 (EF10)

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

Resumo

Observação

Se você estiver usando Microsoft.Data.Sqlite, consulte a seção separada abaixo sobre alterações interruptivas do Microsoft.Data.Sqlite.

Alteração interruptiva Impacto
Agora, as ferramentas de EF exigem que a estrutura seja especificada para projetos com vários destinos Medium
O Nome do Aplicativo agora é injetado na cadeia de conexão Baixo
Tipo de dados json do SQL Server usado por padrão no SQL do Azure e no nível de compatibilidade 170 Baixo
Coleções parametrizadas agora usam vários parâmetros por padrão Baixo
ExecuteUpdateAsync agora aceita um lambda regular, não uma expressão Baixo
Os nomes de coluna de tipo complexo agora são tornados únicos Baixo
Propriedades aninhadas de tipo complexo usam o caminho completo em nomes de coluna Baixo
Alteração na assinatura de IDiscriminatorPropertySetConvention Baixo
Os métodos IRelationalCommandDiagnosticsLogger adicionam o parâmetro logCommandText Baixo

Alterações de impacto médio

Agora, as ferramentas de EF exigem que a estrutura seja especificada para projetos com vários destinos

Problema de acompanhamento nº 37230

Comportamento antigo

Anteriormente, as ferramentas EF (dotnet-ef) podiam ser usadas em projetos direcionados a várias estruturas sem especificar qual estrutura usar.

Novo comportamento

A partir do EF Core 10.0, ao executar ferramentas de EF em um projeto que tenha como alvo várias frameworks (usando <TargetFrameworks> em vez de <TargetFramework>), você deve especificar explicitamente qual framework de destino usar com a opção --framework. Sem essa opção, o seguinte erro será gerado:

O projeto tem como destino várias estruturas. Use a opção --framework para especificar qual estrutura de destino usar.

Por que

No EF Core 10, as ferramentas começaram a depender da ResolvePackageAssets tarefa MSBuild para obter informações mais precisas sobre dependências do projeto. No entanto, essa tarefa não estará disponível se o projeto estiver direcionando várias estruturas de destino (TFMs). A solução exige que os usuários selecionem qual estrutura deve ser usada.

Atenuações

Ao executar qualquer comando de ferramentas EF em um projeto direcionado a várias estruturas, especifique a estrutura de destino usando a opção --framework . Por exemplo:

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

Se o arquivo de projeto tiver esta aparência:

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

Você precisará escolher uma das estruturas (por exemplo, net9.0) ao executar as ferramentas EF.

Alterações de baixo impacto

O Nome do Aplicativo agora é injetado na cadeia de conexão

Problema de acompanhamento nº 35730

Novo comportamento

Quando uma cadeia de conexão sem um Application Name é passada para o EF, este agora insere um Application Name que contém informações anônimas sobre as versões do EF e do SqlClient que estão sendo usadas. Na grande maioria dos casos, isso não impacta o aplicativo de nenhuma maneira, mas pode afetar o comportamento em alguns casos limite. Por exemplo, se você se conectar ao mesmo banco de dados com eF e outra tecnologia de acesso a dados não EF (por exemplo, Dapper, ADO.NET), o SqlClient usará um pool de conexões interno diferente, pois o EF agora usará uma cadeia de conexão diferente e atualizada (uma em que Application Name foi injetada). Se esse tipo de acesso misto for feito dentro de um TransactionScope, isso poderá causar escalonamento para uma transação distribuída em que anteriormente nenhuma era necessária, devido ao uso de duas cadeias de conexão que o SqlClient identifica como dois bancos de dados distintos.

Atenuações

Uma mitigação é simplesmente definir um Application Name na cadeia de conexão. Depois que um for definido, o EF não o substituirá e a cadeia de conexão original será preservada exatamente como está.

Tipo de dados json do SQL Server usado por padrão no SQL do Azure e no nível de compatibilidade 170

Problema de acompanhamento nº 36372

Comportamento antigo

Anteriormente, ao mapear coleções primitivas ou tipos de propriedade para JSON no banco de dados, o provedor do SQL Server armazenava os dados JSON em uma nvarchar(max) coluna:

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 o acima, o EF gerou anteriormente a seguinte tabela:

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

Novo comportamento

Com o EF 10, se você configurar o EF com UseAzureSql (consulte a documentação) ou configurar o EF com um nível de compatibilidade igual a 170 ou superior (consulte a documentação), o EF será mapeado para o novo tipo de dados JSON em vez disso:

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

Embora o novo tipo de dados JSON seja a maneira recomendada de armazenar dados JSON no SQL Server daqui para frente, pode haver algumas diferenças comportamentais ao fazer a transição e alguns formulários de nvarchar(max)consulta específicos podem não ter suporte. Por exemplo, o SQL Server não dá suporte ao operador DISTINCT em matrizes JSON e as consultas que tentam fazer isso falharão.

Observe que, se você tiver uma tabela existente e estiver usando UseAzureSql, a atualização para o EF 10 fará com que uma migração seja gerada, o que altera todas as colunas JSON existentes nvarchar(max) para json. Essa operação de alteração tem suporte e deve ser aplicada perfeitamente e sem problemas, mas é uma alteração não trivial no banco de dados.

Por que

O novo tipo de dados JSON introduzido pelo SQL Server é uma maneira superior de armazenar e interagir com dados JSON no banco de dados; ele notavelmente traz melhorias significativas de desempenho (consulte a documentação). Todos os aplicativos que usam o Banco de Dados SQL do Azure ou o SQL Server 2025 são incentivados a migrar para o novo tipo de dados JSON.

Atenuações

Se você estiver direcionando o Banco de Dados SQL do Azure e não quiser fazer a transição para o novo tipo de dados JSON imediatamente, poderá configurar o EF com um nível de compatibilidade inferior a 170:

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

Se você estiver direcionando o SQL Server local, o nível de compatibilidade padrão é UseSqlServer atualmente 150 (SQL Server 2019), portanto, o tipo de dados JSON não é usado.

Como alternativa, você pode definir explicitamente o tipo de coluna em propriedades específicas para: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());
}

Coleções parametrizadas agora usam vários parâmetros por padrão

Problema de acompanhamento nº 34346

Comportamento antigo

No EF Core 9 e anterior, coleções parametrizadas em consultas LINQ (como aquelas usadas com .Contains()) foram traduzidas para SQL usando um parâmetro de matriz JSON por padrão. Considere a consulta a seguir:

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

No SQL Server, isso gerou o seguinte 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]
)

Novo comportamento

A partir do EF Core 10.0, coleções parametrizadas agora são traduzidas usando vários parâmetros escalares por padrão:

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

Por que

A nova tradução padrão fornece ao planejador de consultas informações de cardinalidade sobre a coleção, o que pode levar a melhores planos de consulta em muitos cenários. A abordagem de múltiplos parâmetros equilibra a eficiência do cache do plano através da parametrização e a otimização de consultas através do fornecimento de cardinalidade.

No entanto, cargas de trabalho diferentes podem se beneficiar de diferentes estratégias de tradução, dependendo de tamanhos de coleção, padrões de consulta e características de banco de dados.

Observação

Embora a nova tradução padrão não cause nenhuma alteração comportamental ou regressão de desempenho na maioria dos casos, a alteração na forma como as consultas são traduzidas para SQL pode ter consequências adversas em alguns cenários.

Os aplicativos criados com o EF Core 8 ou 9 e que dependem das características de desempenho da conversão de parâmetros da matriz JSON (usando OPENJSON ou funções específicas de banco de dados semelhantes) podem apresentar diferenças de desempenho ao atualizar para o EF Core 10. Isso é especialmente relevante para consultas com grandes coleções ou padrões de consulta específicos que se beneficiaram da estratégia de tradução anterior.

Se você tiver regressões de desempenho após a atualização, considere usar as estratégias de mitigação abaixo para reverter para o comportamento anterior globalmente ou para consultas específicas.

Atenuações

Se você encontrar problemas com o novo comportamento padrão (como regressões de desempenho), poderá configurar o modo de tradução globalmente:

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

Os modos disponíveis são:

  • ParameterTranslationMode.MultipleParameters - O novo padrão (vários parâmetros escalares)
  • ParameterTranslationMode.Constant - Incorporar valores como constantes (comportamento padrão antes da versão EF8)
  • ParameterTranslationMode.Parameter - Usa o parâmetro de matriz JSON (padrão EF8-9)

Você também pode controlar a tradução 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 obter mais informações sobre a tradução de coleção parametrizada, consulte a documentação.

ExecuteUpdateAsync agora aceita um lambda regular, não uma expressão.

Acompanhamento do problema #32018

Comportamento antigo

Anteriormente, ExecuteUpdate aceitava um argumento de árvore de expressões (Expression<Func<...>>) para os definidores de coluna.

Novo comportamento

Começando com o EF Core 10.0, ExecuteUpdate agora aceita um argumento de não expressão (Func<...>) para os setters de coluna. Se você criar expressões em árvore para definir dinamicamente o argumento dos definidores de coluna, seu código não será mais compilado, mas poderá ser substituído por uma alternativa muito mais simples (veja abaixo).

Por que

O fato de o parâmetro setters de coluna ser uma árvore de expressão dificultou bastante a construção dinâmica dos setters de coluna, em que alguns setters só estão presentes com base em alguma condição (consulte Mitigações abaixo para obter um exemplo).

Atenuações

O código que estava construindo árvores de expressão para criar dinamicamente os definidores de coluna precisará ser reescrito, mas o resultado será muito mais simples. Por exemplo, vamos supor que queremos atualizar as visualizações de um Blog, mas condicionalmente também atualizar seu Nome. Como o argumento setters era uma árvore de expressão, código como o seguinte precisava 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);

Criar árvores de expressão manualmente é complicado e propenso a erros e tornou esse cenário comum muito mais difícil do que deveria. A partir do EF 10, agora você pode escrever o seguinte em vez disso:

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

Os nomes das colunas de tipo complexo agora são tornados únicos.

Problema de acompanhamento nº 4970

Comportamento antigo

Anteriormente, ao mapear tipos complexos para colunas de tabela, se várias propriedades em diferentes tipos complexos tivessem o mesmo nome de coluna, elas compartilhariam silenciosamente a mesma coluna.

Novo comportamento

A partir do EF Core 10.0, os nomes de coluna de tipo complexo são diferenciados adicionando-se um número ao final se já existir outra coluna com o mesmo nome na tabela.

Por que

Isso evita a corrupção de dados que podem ocorrer quando várias propriedades são mapeadas involuntariamente para a mesma coluna.

Atenuações

Se você precisar de várias propriedades para compartilhar a mesma coluna, configure-as explicitamente usando Property e 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"));
});

Propriedades aninhadas de tipo complexo usam o caminho completo nos nomes de coluna

Comportamento antigo

Anteriormente, as propriedades em tipos complexos aninhados eram mapeadas para colunas usando apenas o nome do tipo declarador. Por exemplo, EntityType.Complex.NestedComplex.Property foi mapeado para a coluna NestedComplex_Property.

Novo comportamento

A partir do EF Core 10.0, as propriedades de tipos complexos aninhados utilizam o caminho completo até a propriedade como parte do nome da coluna. Por exemplo, EntityType.Complex.NestedComplex.Property agora está mapeado para a coluna Complex_NestedComplex_Property.

Por que

Isso proporciona uma melhor exclusividade dos nomes das colunas e torna mais claro qual propriedade corresponde a qual coluna.

Atenuações

Se você precisar manter os nomes de coluna antigos, configure-os explicitamente usando Property e HasColumnName:

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

Assinatura da IDiscriminatorPropertySetConvention foi alterada.

Comportamento antigo

Anteriormente, IDiscriminatorPropertySetConvention.ProcessDiscriminatorPropertySet aceitava IConventionEntityTypeBuilder como um parâmetro.

Novo comportamento

A partir do EF Core 10.0, a assinatura do método foi alterada para ser usada IConventionTypeBaseBuilder em vez de IConventionEntityTypeBuilder.

Por que

Essa alteração permite que a convenção funcione com tipos de entidade e tipos complexos.

Atenuações

Atualize suas implementações de convenção personalizada para usar a nova assinatura:

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

Os métodos IRelationalCommandDiagnosticsLogger adicionam o parâmetro logCommandText

Problema de acompanhamento nº 35757

Comportamento antigo

Aceitavam anteriormente, em métodos IRelationalCommandDiagnosticsLogger como CommandReaderExecuting, CommandReaderExecuted, CommandScalarExecuting e outros, um parâmetro command que representava o comando de banco de dados que estava sendo executado.

Novo comportamento

A partir do EF Core 10.0, esses métodos agora exigem um parâmetro adicional logCommandText . Esse parâmetro contém o texto do comando SQL que será registrado em log, que pode ter dados confidenciais redigidos quando EnableSensitiveDataLogging não estiver habilitado.

Por que

Essa alteração oferece suporte ao novo recurso de ocultar constantes embutidas do log por padrão. Quando o EF insere valores de parâmetro em SQL (por exemplo, ao usar EF.Constant()), esses valores agora são removidos dos logs, a menos que o registro em log de dados confidenciais esteja explicitamente habilitado. O logCommandText parâmetro fornece o SQL redigido para fins de log, enquanto o command parâmetro contém o SQL real que é executado.

Atenuações

Se você tiver uma implementação personalizada de IRelationalCommandDiagnosticsLogger, precisará atualizar suas assinaturas de método para incluir o novo parâmetro logCommandText. Por exemplo:

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
}

O logCommandText parâmetro contém o SQL a ser registrado (com constantes embutidas potencialmente redigidas), enquanto command.CommandText contém o SQL real que será executado no banco de dados.

Alterações interruptivas do Microsoft.Data.Sqlite

Resumo

Alteração interruptiva Impacto
Usar GetDateTimeOffset sem especificar um fuso horário agora pressupõe UTC por padrão Alta
Escrever DateTimeOffset na coluna REAL agora é feito em UTC Alta
A utilização de GetDateTime com um deslocamento agora retorna valor em UTC Alta

Alterações de alto impacto

Usar GetDateTimeOffset sem especificar um fuso horário agora pressupõe UTC por padrão

Problema de acompanhamento nº 36195

Comportamento antigo

Antes, ao usar GetDateTimeOffset em um carimbo de data/hora textual que não tivesse um deslocamento (por exemplo, 2014-04-15 10:47:16), o Microsoft.Data.Sqlite presumia que o valor estava no fuso horário local. Ou seja, o valor foi analisado como 2014-04-15 10:47:16+02:00 (supondo que o fuso horário local fosse UTC+2).

Novo comportamento

A partir do Microsoft.Data.Sqlite 10.0, ao usar GetDateTimeOffset em um timestamp textual que não tenha um deslocamento, o Microsoft.Data.Sqlite assumirá que o valor está em UTC.

Por que

A intenção é alinhar-se ao comportamento do SQLite, onde carimbos de data/hora sem deslocamento são interpretados como UTC.

Atenuações

O código deve ser ajustado adequadamente.

Como um recurso último/temporário, você pode reverter para o comportamento anterior definindo Microsoft.Data.Sqlite.Pre10TimeZoneHandling a opção AppContext para true, consulte AppContext para consumidores de biblioteca para obter mais detalhes.

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

A gravação de DateTimeOffset na coluna REAL agora é gravada em UTC

Problema de acompanhamento nº 36195

Comportamento antigo

Anteriormente, ao gravar um DateTimeOffset valor em uma coluna REAL, o Microsoft.Data.Sqlite gravava o valor sem levar em conta o deslocamento.

Novo comportamento

A partir do Microsoft.Data.Sqlite 10.0, ao gravar um DateTimeOffset valor em uma coluna REAL, o Microsoft.Data.Sqlite converterá o valor em UTC antes de fazer as conversões e escrevê-lo.

Por que

O valor gravado estava incorreto, não alinhado com o comportamento do SQLite, em que os carimbos de data/hora REAIS são assumidos como UTC.

Atenuações

O código deve ser ajustado adequadamente.

Como um recurso último/temporário, você pode reverter para o comportamento anterior definindo Microsoft.Data.Sqlite.Pre10TimeZoneHandling a opção AppContext para true, consulte AppContext para consumidores de biblioteca para obter mais detalhes.

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

A utilização de GetDateTime com um deslocamento agora retorna valor em UTC

Problema de acompanhamento nº 36195

Comportamento antigo

Anteriormente, ao usar GetDateTime em um carimbo de data/hora textual que tivesse um deslocamento (por exemplo, 2014-04-15 10:47:16+02:00), o Microsoft.Data.Sqlite retornava o valor com DateTimeKind.Local, mesmo que o deslocamento não fosse local. O tempo foi analisado corretamente levando em conta o deslocamento.

Novo comportamento

A partir do Microsoft.Data.Sqlite 10.0, ao usar GetDateTime em um carimbo de data/hora textual que tenha um deslocamento, o Microsoft.Data.Sqlite converterá o valor em UTC e o retornará com DateTimeKind.Utc.

Por que

Embora a hora tenha sido analisada corretamente, ela dependia do fuso horário local configurado pelo computador, o que poderia levar a resultados inesperados.

Atenuações

O código deve ser ajustado adequadamente.

Como um recurso último/temporário, você pode reverter para o comportamento anterior definindo Microsoft.Data.Sqlite.Pre10TimeZoneHandling a opção AppContext para true, consulte AppContext para consumidores de biblioteca para obter mais detalhes.

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