Partilhar via


Novidades no EF Core 9

O EF Core 9 (EF9) é a próxima versão após o EF Core 8 e está programado para ser lançado em novembro de 2024.

O EF9 está disponível como compilações diárias que contêm todos os recursos e ajustes de API mais recentes do EF9. Os exemplos aqui usam esses builds diários.

Dica

Você pode executar e depurar nos exemplos baixando o código de exemplo do GitHub. Cada seção abaixo vincula ao código-fonte específico dessa seção.

O EF9 tem como destino o .NET 8 e, portanto, pode ser usado com o .NET 8 (LTS) ou uma versão prévia do .NET 9.

Dica

Os documentos do O Que Há de Novo são atualizados para cada versão prévia. Todos os exemplos são configurados para usar os builds diários EF9, que geralmente têm várias semanas adicionais de trabalho concluído em comparação com a versão prévia mais recente. Incentivamos fortemente o uso das compilações diárias ao testar novos recursos para que você não esteja fazendo seus testes com base em bits obsoletos.

Azure Cosmos DB para NoSQL

O EF 9.0 traz melhorias consideráveis para o provedor EF Core para Azure Cosmos DB; partes significativas do provedor foram reescritas para fornecer novas funcionalidades, permitir novas formas de consultas e alinhar melhor o provedor com as práticas recomendadas do Cosmos DB. As principais melhorias de alto nível estão listadas abaixo; para obter uma lista completa, consulte este problema de épico.

Aviso

Como parte das melhorias do provedor, uma série de alterações significativas de alto impacto tiveram que ser feitas; se você estiver atualizando um aplicativo existente, leia a seção Alterações interruptivas com atenção.

Melhorias na consulta com chaves de partição e IDs de documento

Cada documento armazenado no banco de dados Cosmos tem uma ID de recurso exclusiva. Além disso, cada documento pode conter uma "chave de partição" que determina o particionamento lógico dos dados, de modo que o banco de dados possa ser dimensionado de forma eficaz. Mais informações sobre a escolha de chaves de partição podem ser encontradas em Particionamento e escala horizontal no Azure Cosmos DB.

No EF 9.0, o provedor do Cosmos DB é consideravelmente melhor na identificação de comparações de chaves de partição em suas consultas LINQ e na extração delas para fazer com que suas consultas sejam enviadas apenas para a partição relevante; isso pode melhorar muito o desempenho de suas consultas e reduzir custos. Por exemplo:

var sessions = await context.Sessions
    .Where(b => b.PartitionKey == "someValue" && b.Username.StartsWith("x"))
    .ToListAsync();

Nessa consulta, o provedor reconhece automaticamente a comparação em PartitionKey; se examinarmos os logs, veremos o seguinte:

Executed ReadNext (189.8434 ms, 2.8 RU) ActivityId='8cd669ed-2ca5-4f2b-8923-338899071361', Container='test', Partition='["someValue"]', Parameters=[]
SELECT VALUE c
FROM root c
WHERE STARTSWITH(c["Username"], "x")

Observe que a cláusula WHERE não contém PartitionKey: essa comparação foi "suspensa" e é usada para executar a consulta apenas na partição relevante. Nas versões anteriores, a comparação era deixada na cláusula WHERE em muitas situações, fazendo com que a consulta fosse executada em todas as partições e resultando no aumento de custos e redução de desempenho.

Além disso, se sua consulta também fornecer um valor para a propriedade ID do documento e não incluir nenhuma outra operação de consulta, o provedor poderá aplicar uma otimização adicional:

var somePartitionKey = "someValue";
var someId = 8;
var sessions = await context.Sessions
    .Where(b => b.PartitionKey == somePartitionKey && b.Id == someId)
    .SingleAsync();

Os logs mostram o seguinte para esta consulta:

Executed ReadItem (73 ms, 1 RU) ActivityId='13f0f8b8-d481-47f0-bf41-67f7deb008b2', Container='test', Id='8', Partition='["someValue"]'

Nesse caso, nenhuma consulta SQL é enviada. Em vez disso, o provedor executa uma leitura de ponto (API ReadItem), que busca diretamente o documento com a chave de partição e o ID. Esse é o tipo de leitura mais eficiente e econômico que você pode executar no Cosmos DB; consulte a documentação do Cosmos DB para obter mais informações sobre leituras de ponto.

Para saber mais sobre como consultar com chaves de partição e leituras de ponto, consulte a página de documentação de consulta.

Chaves de partição hierárquicas

Dica

O código mostrado aqui vem de HierarchicalPartitionKeysSample.cs.

Inicialmente, o Azure Cosmos DB dava suporte a uma única chave de partição, mas desde então expandiu os recursos de particionamento para dar suporte ao subparticionamento por meio da especificação de até três níveis de hierarquia na chave de partição. O EF Core 9 oferece suporte completo para chaves de partição hierárquicas, permitindo que você aproveite o melhor desempenho e a economia de custos associados a esse recurso.

As chaves de partição são especificadas usando a API de criação de modelos, normalmente em DbContext.OnModelCreating. Deve haver uma propriedade mapeada no tipo de entidade para cada nível da chave de partição. Por exemplo, considere um tipo de entidade UserSession:

public class UserSession
{
    // Item ID
    public Guid Id { get; set; }

    // Partition Key
    public string TenantId { get; set; } = null!;
    public Guid UserId { get; set; }
    public int SessionId { get; set; }

    // Other members
    public string Username { get; set; } = null!;
}

O código a seguir especifica uma chave de partição de três níveis usando as propriedades TenantId, UserId e SessionId:

modelBuilder
    .Entity<UserSession>()
    .HasPartitionKey(e => new { e.TenantId, e.UserId, e.SessionId });

Dica

Esta definição de chave de partição segue o exemplo dado em Escolha suas chaves de partição hierárquicas da documentação do Azure Cosmos DB.

Observe como, a partir do EF Core 9, as propriedades de qualquer tipo mapeado podem ser usadas na chave de partição. Para bool e tipos numéricos, como a propriedade int SessionId, o valor é usado diretamente na chave de partição. Outros tipos, como a propriedade Guid UserId, são automaticamente convertidos em cadeias de caracteres.

Ao fazer consultas, o EF extrai automaticamente os valores de chave de partição das consultas e os aplica à API de consulta do Cosmos para garantir que as consultas sejam restritas adequadamente ao menor número possível de partições. Por exemplo, considere a seguinte consulta LINQ que fornece os valores de chave de partição:

var tenantId = "Microsoft";
var sessionId = 7;
var userId = new Guid("99A410D7-E467-4CC5-92DE-148F3FC53F4C");

var sessions = await context.Sessions
    .Where(
        e => e.TenantId == tenantId
             && e.UserId == userId
             && e.SessionId == sessionId
             && e.Username.Contains("a"))
    .ToListAsync();

Ao executar essa consulta, o EF Core extrairá os valores dos parâmetros tenantId, userId e sessionId e os passará para a API de consulta do Cosmos como o valor da chave de partição. Por exemplo, veja os logs da execução da consulta acima:

info: 6/10/2024 19:06:00.017 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing SQL query for container 'UserSessionContext' in partition '["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]' [Parameters=[]]
      SELECT c
      FROM root c
      WHERE ((c["Discriminator"] = "UserSession") AND CONTAINS(c["Username"], "a"))

Observe que as comparações de chave de partição foram removidas da cláusula WHERE e, em vez disso, são usadas como a chave de partição para execução eficiente: ["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0].

Para obter mais informações, consulte a documentação sobre consultas com chaves de partição.

Recursos de consulta LINQ consideravelmente aprimorados

No EF 9.0, os recursos de conversão LINQ do provedor do Cosmos DB foram bastante expandidos e o provedor agora pode executar significativamente mais tipos de consulta. A lista completa de melhorias de consulta é muito longa, mas estes são os principais destaques:

  • O provedor do Cosmos agora dá suporte total às coleções primitivas do EF, permitindo que você execute consultas LINQ em coleções de, por exemplo, ints ou cadeias de caracteres. Consulte O que há de novo no EF8: coleções primitivas para mais informações.
  • Também foi adicionado suporte para consultas arbitrárias em coleções não primitivas.
  • Agora há suporte para muitos operadores LINQ adicionais: indexação em coleções, Length/Count, ElementAt, Contains e muitos outros.
  • Foi adicionado suporte para operadores agregados, como Count e Sum.
  • Foram adicionadas muitas traduções de função (consulte a documentação de mapeamentos de função para obter a lista completa de traduções suportadas):
    • Foram adicionadas traduções para DateTime e DateTimeOffset membros componentes (DateTime.Year, DateTimeOffset.Month...).
    • EF.Functions.IsDefined e EF.Functions.CoalesceUndefined agora permitem lidar com valores undefined.
    • string.Contains, StartsWith e EndsWith agora dão suporte a StringComparison.OrdinalIgnoreCase.

Para obter a lista completa de melhorias de consulta, consulte este problema:

Modelagem aprimorada alinhada aos padrões Cosmos e JSON

O EF 9.0 é mapeado para documentos do Cosmos DB de maneiras mais naturais para um banco de dados de documentos baseado em JSON e ajuda a interoperar com outros sistemas que acessam seus documentos. Embora isso implique alterações significativas, existem APIs que permitem reverter para o comportamento pré-9.0 em todos os casos.

Propriedades id simplificadas sem discriminadores

Primeiro, as versões anteriores do EF inseriram o valor discriminador na propriedade JSON id, produzindo documentos como os seguintes:

{
    "id": "Blog|1099",
    ...
}

Isso foi feito para permitir que documentos de diferentes tipos (por exemplo, Blog e Post) e o mesmo valor de chave (1099) existissem na mesma partição de contêiner. A partir do EF 9.0, a propriedade id contém apenas o valor da chave:

{
    "id": 1099,
    ...
}

Essa é uma maneira mais natural de mapear para JSON e facilita a interação de ferramentas e sistemas externos com documentos JSON gerados pelo EF; esses sistemas externos geralmente não estão cientes dos valores discriminadores do EF, que são derivados de tipos .NET por padrão.

Observe que essa é uma alteração considerável, pois o EF não poderá mais consultar documentos existentes com o formato antigo id. Uma API foi introduzida para reverter para o comportamento anterior. Consulte a nota sobre alteração significativa e a documentação para obter mais detalhes.

Propriedade discriminatória renomeada para $type

A propriedade discriminatória antes era chamada de Discriminator. O EF 9.0 altera o padrão para $type:

{
    "id": 1099,
    "$type": "Blog",
    ...
}

Isso segue o padrão emergente para polimorfismo JSON, permitindo melhor interoperabilidade com outras ferramentas. Por exemplo, o System.Text.Json do .NET também oferece suporte ao polimorfismo, usando $type como seu nome de propriedade discriminatória padrão (documentos).

Observe que esta é uma alteração significativa, pois o EF não poderá mais consultar documentos existentes com o antigo nome da propriedade discriminatória. Consulte a nota de alteração significativa para obter detalhes sobre como reverter para a nomenclatura anterior.

Pesquisa de similaridade vetorial (versão prévia)

O Azure Cosmos DB agora oferece suporte de visualização para pesquisa de similaridade de vetor. A busca em vetores é uma parte fundamental de alguns tipos de aplicativos, incluindo IA, pesquisa semântica e outros. O suporte do Cosmos DB para busca em vetores permite armazenar seus dados e vetores e executar suas consultas em um único banco de dados, o que pode simplificar consideravelmente sua arquitetura e eliminar a necessidade de uma solução de banco de dados vetorial adicional e dedicada em sua pilha. Para saber mais sobre a busca em vetores do Cosmos DB, consulte a documentação.

Depois que o contêiner do Cosmos DB estiver configurado corretamente, basta adicionar uma propriedade de vetor e configurá-la para usar a busca em vetores por meio do EF:

public class Blog
{
    ...

    public float[] Vector { get; set; }
}

public class BloggingContext
{
    ...

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .Property(b => b.Embeddings)
            .IsVector(DistanceFunction.Cosine, dimensions: 1536);
    }
}

Feito isso, use a função EF.Functions.VectorDistance() em consultas LINQ para realizar a pesquisa de similaridade vetorial:

var blogs = await context.Blogs
    .OrderBy(s => EF.Functions.VectorDistance(s.Vector, vector))
    .Take(5)
    .ToListAsync();

Para saber mais, consulte a documentação sobre busca em vetores.

Suporte à paginação

O provedor do Cosmos DB agora permite a paginação por meio de resultados de consulta por meio de tokens de continuação, o que é muito mais eficiente e econômico do que o uso tradicional de Skip e Take:

var firstPage = await context.Posts
    .OrderBy(p => p.Id)
    .ToPageAsync(pageSize: 10, continuationToken: null);

var continuationToken = page.ContinuationToken;
foreach (var post in page.Values)
{
    // Display/send the posts to the user
}

O novo operador ToPageAsync retorna um CosmosPage, que expõe um token de continuação que pode ser usado para retomar a consulta com eficiência em um ponto posterior, buscando os próximos 10 itens:

var nextPage = await context.Sessions.OrderBy(s => s.Id).ToPageAsync(10, continuationToken);

Para saber mais, consulte a seção da documentação sobre paginação.

FromSql para consultas SQL mais seguras

O provedor do Cosmos DB permitiu a consulta SQL por meio do FromSqlRaw. No entanto, essa API pode ser suscetível a ataques de injeção de SQL quando os dados fornecidos pelo usuário são interpolados ou concatenados no SQL. No EF 9.0, agora você pode usar o novo método FromSql, que sempre integra dados parametrizados como um parâmetro fora do SQL:

var maxAngle = 8;
_ = await context.Blogs
    .FromSql($"SELECT VALUE c FROM root c WHERE c.Angle1 <= {maxAngle}")
    .ToListAsync();

Para saber mais, consulte a seção da documentação sobre paginação.

Acesso baseado em função

O Azure Cosmos DB for NoSQL inclui um sistema interno de controle de acesso baseado em função (RBAC). Agora há suporte pelo EF9 tanto para gerenciamento quanto para uso de contêineres. Nenhuma alteração é necessária para o código do aplicativo. Confira o Problema nº 32197 para obter mais informações.

A E/S síncrona agora é bloqueada por padrão

O Azure Cosmos DB for NoSQL não dá suporte a (bloqueio de) APIs síncronas do código do aplicativo. Anteriormente, o EF mascarava isso bloqueando para você em chamadas assíncronas. No entanto, isso incentiva o uso de E/S síncrona, o que é uma prática ruim e pode causar deadlocks. Portanto, a partir do EF 9, uma exceção é gerada quando há a tentativa de usar o acesso síncrono. Por exemplo:

A E/S síncrona ainda pode ser usada por enquanto, configurando o nível de aviso adequadamente. Por exemplo, em OnConfiguring no seu tipo DbContext:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.ConfigureWarnings(b => b.Ignore(CosmosEventId.SyncNotSupported));

Porém, observe que planejamos remover totalmente o suporte síncrono no EF 11, portanto, comece a atualizar para usar métodos assíncronos como ToListAsync e SaveChangesAsync o mais rápido possível!

AOT e consultas pré-compiladas

Conforme mencionado na introdução, há muito trabalho em andamento nos bastidores para permitir que o EF Core seja executado sem a compilação just-in-time (JIT). Em vez disso, o EF compila ahead of time (AOT) tudo o que é necessário para executar consultas no aplicativo. Essa compilação AOT e o processamento relacionado ocorrerão como parte da criação e publicação do aplicativo. Neste ponto da versão EF9, não há muita coisa disponível que você, desenvolvedor do aplicativo, pode usar. Porém, para os interessados, os problemas concluídos no EF9 que dão suporte à compilação AOT e consultas pré-compiladas são:

Verifique aqui exemplos de como usar consultas pré-compiladas à medida que a experiência avança.

Tradução de LINQ e SQL

Como em todas as versões, o EF9 inclui um grande número de melhorias nos recursos de consulta do LINQ. Novas consultas podem ser traduzidas e muitas traduções SQL para cenários com suporte foram aprimoradas, para oferecer melhor desempenho e legibilidade.

O número de melhorias é grande demais para listar aqui. A seguir, algumas das melhorias mais importantes são destacadas; consulte esta edição para obter uma lista mais completa do trabalho realizado na versão 9.0.

Gostaríamos de mencionar Andrea Canciani (@ranma42) por suas inúmeras contribuições de alta qualidade para otimizar o SQL gerado pelo EF Core!

Tipos complexos: suporte a GroupBy e ExecuteUpdate

GroupBy

Dica

O código mostrado aqui vem de ComplexTypesSample.cs.

O EF9 dá suporte ao agrupamento por uma instância de tipo complexa. Por exemplo:

var groupedAddresses = await context.Stores
    .GroupBy(b => b.StoreAddress)
    .Select(g => new { g.Key, Count = g.Count() })
    .ToListAsync();

O EF traduz isso como agrupamento por cada membro do tipo complexo, que se alinha à semântica de tipos complexos como objetos de valor. Por exemplo, no SQL do Azure:

SELECT [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode], COUNT(*) AS [Count]
FROM [Stores] AS [s]
GROUP BY [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode]

ExecuteUpdate

Dica

O código mostrado aqui vem de ExecuteUpdateSample.cs.

De maneira semelhante, o EF9 ExecuteUpdate também foi aprimorado para aceitar propriedades de tipo complexo. No entanto, cada membro do tipo complexo deve ser especificado explicitamente. Por exemplo:

var newAddress = new Address("Gressenhall Farm Shop", null, "Beetley", "Norfolk", "NR20 4DR");

await context.Stores
    .Where(e => e.Region == "Germany")
    .ExecuteUpdateAsync(s => s.SetProperty(b => b.StoreAddress, newAddress));

Isso gera SQL que atualiza cada coluna mapeada para o tipo complexo:

UPDATE [s]
SET [s].[StoreAddress_City] = @__complex_type_newAddress_0_City,
    [s].[StoreAddress_Country] = @__complex_type_newAddress_0_Country,
    [s].[StoreAddress_Line1] = @__complex_type_newAddress_0_Line1,
    [s].[StoreAddress_Line2] = NULL,
    [s].[StoreAddress_PostCode] = @__complex_type_newAddress_0_PostCode
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Germany'

Antes, era necessário listar manualmente as diferentes propriedades do tipo complexo em sua chamada ExecuteUpdate.

Remover elementos desnecessários do SQL

Antes, o EF às vezes produzia SQL com elementos que não eram realmente necessários; na maioria dos casos, eles eram necessários possivelmente em um estágio anterior do processamento SQL e foram deixados para trás. O EF9 agora remove a maioria desses elementos, resultando em um SQL mais compacto e, em alguns casos, mais eficiente.

Poda de tabela

Como primeiro exemplo, o SQL gerado pelo EF às vezes continha JOINs para tabelas que não eram realmente necessárias na consulta. Considere o seguinte modelo, que usa o mapeamento de herança TPT (tabela por tipo):

public class Order
{
    public int Id { get; set; }
    ...

    public Customer Customer { get; set; }
}

public class DiscountedOrder : Order
{
    public double Discount { get; set; }
}

public class Customer
{
    public int Id { get; set; }
    ...

    public List<Order> Orders { get; set; }
}

public class BlogContext : DbContext
{
    ...

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Order>().UseTptMappingStrategy();
    }
}

Se executarmos a seguinte consulta para obter todos os Clientes com pelo menos um Pedido:

var customers = await context.Customers.Where(o => o.Orders.Any()).ToListAsync();

O EF8 gerou o seguinte SQL:

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
    SELECT 1
    FROM [Orders] AS [o]
    LEFT JOIN [DiscountedOrders] AS [d] ON [o].[Id] = [d].[Id]
    WHERE [c].[Id] = [o].[CustomerId])

Observe que a consulta continha uma junção para a tabela DiscountedOrders, embora nenhuma coluna tenha sido referenciada nela. O EF9 gera um SQL podado sem a junção:

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
    SELECT 1
    FROM [Orders] AS [o]
    WHERE [c].[Id] = [o].[CustomerId])

Poda de projeção

Da mesma forma, vamos examinar a seguinte consulta:

var orders = await context.Orders
    .Where(o => o.Amount > 10)
    .Take(5)
    .CountAsync();

No EF8, essa consulta gerou o seguinte SQL:

SELECT COUNT(*)
FROM (
    SELECT TOP(@__p_0) [o].[Id]
    FROM [Orders] AS [o]
    WHERE [o].[Amount] > 10
) AS [t]

Observe que a projeção [o].[Id] não é necessária na subconsulta, pois a expressão SELECT externa apenas conta as linhas. Em vez disso, o EF9 gera o seguinte:

SELECT COUNT(*)
FROM (
    SELECT TOP(@__p_0) 1 AS empty
    FROM [Orders] AS [o]
    WHERE [o].[Amount] > 10
) AS [s]

... e a projeção está vazia. Isso pode não parecer muito, mas pode simplificar consideravelmente o SQL em alguns casos; veja algumas das alterações de SQL nos testes para ver o efeito.

Traduções envolvendo GREATEST/LEAST

Dica

O código mostrado aqui vem de LeastGreatestSample.cs.

Várias novas traduções foram introduzidas que usam as funções SQL GREATEST e LEAST.

Importante

As funções GREATEST e LEAST foram introduzidas no SQL Server/banco de dados SQL do Azure na versão de 2022. O Visual Studio 2022 instala o SQL Server 2019 por padrão. Recomendamos instalar o SQL Server Developer Edition 2022 para experimentar essas novas traduções no EF9.

Por exemplo, as consultas que usam Math.Max ou Math.Min agora são traduzidas para o SQL do Azure usando GREATEST e LEAST, respectivamente. Por exemplo:

var walksUsingMin = await context.Walks
    .Where(e => Math.Min(e.DaysVisited.Count, e.ClosestPub.Beers.Length) > 4)
    .ToListAsync();

Essa consulta é traduzida para o seguinte SQL ao usar a execução do EF9 no SQL Server 2022:

SELECT [w].[Id], [w].[ClosestPubId], [w].[DaysVisited], [w].[Name], [w].[Terrain]
FROM [Walks] AS [w]
INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id]
WHERE LEAST((
    SELECT COUNT(*)
    FROM OPENJSON([w].[DaysVisited]) AS [d]), (
    SELECT COUNT(*)
    FROM OPENJSON([p].[Beers]) AS [b])) >

Math.Min e Math.Max também podem ser usados nos valores de uma coleção primitiva. Por exemplo:

var pubsInlineMax = await context.Pubs
    .SelectMany(e => e.Counts)
    .Where(e => Math.Max(e, threshold) > top)
    .ToListAsync();

Essa consulta é traduzida para o seguinte SQL ao usar a execução do EF9 no SQL Server 2022:

SELECT [c].[value]
FROM [Pubs] AS [p]
CROSS APPLY OPENJSON([p].[Counts]) WITH ([value] int '$') AS [c]
WHERE GREATEST([c].[value], @__threshold_0) > @__top_1

Por fim, RelationalDbFunctionsExtensions.Least e RelationalDbFunctionsExtensions.Greatest podem ser usados para invocar diretamente a função Least ou Greatest no SQL. Por exemplo:

var leastCount = await context.Pubs
    .Select(e => EF.Functions.Least(e.Counts.Length, e.DaysVisited.Count, e.Beers.Length))
    .ToListAsync();

Essa consulta é traduzida para o seguinte SQL ao usar a execução do EF9 no SQL Server 2022:

SELECT LEAST((
    SELECT COUNT(*)
    FROM OPENJSON([p].[Counts]) AS [c]), (
    SELECT COUNT(*)
    FROM OPENJSON([p].[DaysVisited]) AS [d]), (
    SELECT COUNT(*)
    FROM OPENJSON([p].[Beers]) AS [b]))
FROM [Pubs] AS [p]

Forçar ou impedir a parametrização da consulta

Dica

O código mostrado aqui vem de QuerySample.cs.

Exceto em alguns casos especiais, o EF Core parametriza variáveis usadas em uma consulta LINQ, mas inclui constantes no SQL gerado. Por exemplo, considere o método de consulta a seguir:

async Task<List<Post>> GetPosts(int id)
    => await context.Posts
        .Where(e => e.Title == ".NET Blog" && e.Id == id)
        .ToListAsync();

Isso se traduz para o seguinte SQL e parâmetros ao usar o SQL do Azure:

Executed DbCommand (1ms) [Parameters=[@__id_0='1'], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] = @__id_0

Observe que o EF criou uma constante no SQL para "Blog do .NET" porque esse valor não será alterado de consulta para consulta. O uso de uma constante permite que esse valor seja examinado pelo mecanismo de banco de dados ao criar um plano de consulta, resultando potencialmente em uma consulta mais eficiente.

Por outro lado, o valor de id é parametrizado, pois a mesma consulta pode ser executada com muitos valores diferentes para id. Nesse caso, a criação de uma constante resultaria na poluição do cache de consulta com muitas consultas que diferem apenas em valores id. Isso é muito ruim para o desempenho geral do banco de dados.

De um modo geral, esses padrões não devem ser alterados. No entanto, o EF Core 8.0.2 introduz um método EF.Constant que força o EF a usar uma constante mesmo que um parâmetro seja usado por padrão. Por exemplo:

async Task<List<Post>> GetPostsForceConstant(int id)
    => await context.Posts
        .Where(e => e.Title == ".NET Blog" && e.Id == EF.Constant(id))
        .ToListAsync();

A tradução agora contém uma constante para o valor id:

Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] = 1

O EF9 apresenta o método EF.Parameter para fazer o oposto. Ou seja, força o EF a usar um parâmetro mesmo que o valor seja uma constante no código. Por exemplo:

async Task<List<Post>> GetPostsForceParameter(int id)
    => await context.Posts
        .Where(e => e.Title == EF.Parameter(".NET Blog") && e.Id == id)
        .ToListAsync();

A tradução agora contém um parâmetro para a cadeia de caracteres "Blog do. NET":

Executed DbCommand (1ms) [Parameters=[@__p_0='.NET Blog' (Size = 4000), @__id_1='1'], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = @__p_0 AND [p].[Id] = @__id_1

Subconsultas não corelacionadas embutidas

Dica

O código mostrado aqui vem de QuerySample.cs.

No EF8, um IQueryable referenciado em outra consulta pode ser executado como uma viagem de ida e volta de banco de dados separada. Por exemplo, considere a seguinte consulta LINQ:

var dotnetPosts = context
    .Posts
    .Where(p => p.Title.Contains(".NET"));

var results = dotnetPosts
    .Where(p => p.Id > 2)
    .Select(p => new { Post = p, TotalCount = dotnetPosts.Count() })
    .Skip(2).Take(10)
    .ToArray();

No EF8, a consulta para dotnetPosts é executada como uma viagem de ida e volta e, em seguida, os resultados finais são executados como uma segunda consulta. Por exemplo, no SQL Server:

SELECT COUNT(*)
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%'

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2
ORDER BY (SELECT 1)
OFFSET @__p_1 ROWS FETCH NEXT @__p_2 ROWS ONLY

No EF9, o IQueryable no dotnetPosts é embutido, resultando em uma única viagem de ida e volta do banco de dados:

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata], (
    SELECT COUNT(*)
    FROM [Posts] AS [p0]
    WHERE [p0].[Title] LIKE N'%.NET%')
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2
ORDER BY (SELECT 1)
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY

As consultas que usam Count != 0 são otimizadas

Dica

O código mostrado aqui vem de QuerySample.cs.

No EF8, a seguinte consulta LINQ foi traduzida para usar a função SQL COUNT:

var blogsWithPost = await context.Blogs
    .Where(b => b.Posts.Count > 0)
    .ToListAsync();

O EF9 agora gera uma tradução mais eficiente usando EXISTS:

SELECT "b"."Id", "b"."Name", "b"."SiteUri"
FROM "Blogs" AS "b"
WHERE EXISTS (
    SELECT 1
    FROM "Posts" AS "p"
    WHERE "b"."Id" = "p"."BlogId")

Semântica C# para operações de comparação em valores anuláveis

No EF8, as comparações entre elementos anuláveis não foram executadas corretamente para alguns cenários. Em C#, se um ou ambos os operandos forem nulos, o resultado de uma operação de comparação será false; caso contrário, os valores contidos dos operandos serão comparados. No EF8, costumávamos traduzir comparações usando semântica nula de banco de dados. Isso produziria resultados diferentes de uma consulta semelhante usando LINQ to Objects. Além disso, produziríamos resultados diferentes quando a comparação fosse feita em filtro vs projeção. Algumas consultas também produziriam resultados diferentes entre o Sql Server e o Sqlite/Postgres.

Por exemplo, a consulta:

var negatedNullableComparisonFilter = await context.Entities
    .Where(x => !(x.NullableIntOne > x.NullableIntTwo))
    .Select(x => new { x.NullableIntOne, x.NullableIntTwo }).ToListAsync();

geraria o seguinte SQL:

SELECT [e].[NullableIntOne], [e].[NullableIntTwo]
FROM [Entities] AS [e]
WHERE NOT ([e].[NullableIntOne] > [e].[NullableIntTwo])

que filtra entidades cujo NullableIntOne ou NullableIntTwo são definidos como nulos.

No EF9, produzimos:

SELECT [e].[NullableIntOne], [e].[NullableIntTwo]
FROM [Entities] AS [e]
WHERE CASE
    WHEN [e].[NullableIntOne] > [e].[NullableIntTwo] THEN CAST(0 AS bit)
    ELSE CAST(1 AS bit)
END = CAST(1 AS bit)

Comparação semelhante realizada em uma projeção:

var negatedNullableComparisonProjection = await context.Entities.Select(x => new
{
    x.NullableIntOne,
    x.NullableIntTwo,
    Operation = !(x.NullableIntOne > x.NullableIntTwo)
}).ToListAsync();

resultou no seguinte SQL:

SELECT [e].[NullableIntOne], [e].[NullableIntTwo], CASE
    WHEN NOT ([e].[NullableIntOne] > [e].[NullableIntTwo]) THEN CAST(1 AS bit)
    ELSE CAST(0 AS bit)
END AS [Operation]
FROM [Entities] AS [e]

que retorna false para entidades cujo NullableIntOne ou NullableIntTwo são definidos como nulos (em vez de true esperado em C#). Executar o mesmo cenário no Sqlite gerou:

SELECT "e"."NullableIntOne", "e"."NullableIntTwo", NOT ("e"."NullableIntOne" > "e"."NullableIntTwo") AS "Operation"
FROM "Entities" AS "e"

o que resulta na exceção Nullable object must have a value, pois a tradução produz o valor null para casos em que NullableIntOne ou são nulos NullableIntTwo.

Agora o EF9 lida corretamente com esses cenários, produzindo resultados consistentes com o LINQ to Objects e em diferentes provedores.

Este aprimoramento foi fornecido por @ranma42. Muito obrigado!

Tradução aprimorada do operador de negação lógica (!)

O EF9 traz muitas otimizações que envolvem SQL CASE/WHEN, COALESCE, negação e várias outras construções; a maioria delas foi fornecidas por Andrea Canciani (@ranma42) - muito obrigado por tudo isso! A seguir, detalharemos apenas algumas dessas otimizações a respeito de negação lógica.

Vamos examinar a seguinte consulta:

var negatedContainsSimplification = await context.Posts
    .Where(p => !p.Content.Contains("Announcing"))
    .Select(p => new { p.Content }).ToListAsync();

No EF8, produziríamos o seguinte SQL:

SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE NOT (instr("p"."Content", 'Announcing') > 0)

No EF9, "empurramos" NOT a operação para a comparação:

SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE instr("p"."Content", 'Announcing') <= 0

Outro exemplo aplicável ao SQL Server é uma operação condicional negada.

var caseSimplification = await context.Blogs
    .Select(b => !(b.Id > 5 ? false : true))
    .ToListAsync();

No EF8 usado para resultar em blocos aninhados CASE:

SELECT CASE
    WHEN CASE
        WHEN [b].[Id] > 5 THEN CAST(0 AS bit)
        ELSE CAST(1 AS bit)
    END = CAST(0 AS bit) THEN CAST(1 AS bit)
    ELSE CAST(0 AS bit)
END
FROM [Blogs] AS [b]

No EF9, removemos o aninhamento:

SELECT CASE
    WHEN [b].[Id] > 5 THEN CAST(1 AS bit)
    ELSE CAST(0 AS bit)
END
FROM [Blogs] AS [b]

No SQL Server, ao projetar uma propriedade booliana negada:

var negatedBoolProjection = await context.Posts.Select(x => new { x.Title, Active = !x.Archived }).ToListAsync();

O EF8 geraria um bloqueio CASE porque as comparações não podem aparecer na projeção diretamente nas consultas do SQL Server:

SELECT [p].[Title], CASE
   WHEN [p].[Archived] = CAST(0 AS bit) THEN CAST(1 AS bit)
   ELSE CAST(0 AS bit)
END AS [Active]
FROM [Posts] AS [p]

No EF9, essa tradução foi simplificada e agora usa exclusivo ou (^):

SELECT [p].[Title], [p].[Archived] ^ CAST(1 AS bit) AS [Active]
FROM [Posts] AS [p]

Outras melhorias de consulta

  • O suporte à consulta de coleções primitivas introduzido no EF8 foi estendido para dar suporte a todos os tipos ICollection<T>. Observe que isso se aplica somente a coleções de parâmetros e embutidas – coleções primitivas que fazem parte de entidades ainda estão limitadas a matrizes, listas e no EF9, também a matrizes/listas somente leitura.
  • Novas funções ToHashSetAsync para retornar os resultados de uma consulta como um HashSet (#30033, contribuição de @wertzui).
  • TimeOnly.FromDateTime e FromTimeSpan agora são traduzidos no SQL Server (#33678).
  • ToString por meio de enums agora é traduzido (#33706, contribuição de @Danevandy99).
  • string.Join agora se traduz em CONCAT_WS em contexto não agregado no SQL Server (#28899).
  • EF.Functions.PatIndex agora se traduz na função SQL Server PATINDEX, que retorna a posição inicial da primeira ocorrência de um padrão (#33702, @smnsht).
  • Sum e Average agora funcionam para decimais no SQLite (#33721, contribuição de @ranma42).
  • Correções e otimizações para string.StartsWith e EndsWith (#31482).
  • Os métodos Convert.To* agora podem aceitar o argumento do tipo object (#33891, contribuição de @imangd).

As opções acima foram apenas algumas das melhorias de consulta mais importantes no EF9; consulte esta edição para ver uma lista mais completa.

Migrações

Migrações de tabelas temporais aprimoradas

A migração criada ao alterar uma tabela existente em uma tabela temporal foi reduzida em tamanho para o EF9. Por exemplo, no EF8, ao transformar uma única tabela existente em uma tabela temporal, o resultado é a seguinte migração:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AlterTable(
        name: "Blogs")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AlterColumn<string>(
        name: "SiteUri",
        table: "Blogs",
        type: "nvarchar(max)",
        nullable: false,
        oldClrType: typeof(string),
        oldType: "nvarchar(max)")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AlterColumn<string>(
        name: "Name",
        table: "Blogs",
        type: "nvarchar(max)",
        nullable: false,
        oldClrType: typeof(string),
        oldType: "nvarchar(max)")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AlterColumn<int>(
        name: "Id",
        table: "Blogs",
        type: "int",
        nullable: false,
        oldClrType: typeof(int),
        oldType: "int")
        .Annotation("SqlServer:Identity", "1, 1")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart")
        .OldAnnotation("SqlServer:Identity", "1, 1");

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodEnd",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodStart",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
}

No EF9, a mesma operação agora resulta em uma migração muito menor:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AlterTable(
        name: "Blogs")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodEnd",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:TemporalIsPeriodEndColumn", true);

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodStart",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:TemporalIsPeriodStartColumn", true);
}

Criação de modelo

Modelos compilados automaticamente

Dica

O código mostrado aqui vem do modelo NewInEFCore9.CompiledModels.

Os modelos compilados podem melhorar o tempo de inicialização para aplicativos com modelos grandes - ou seja, contagens de tipos de entidades na casa das centenas ou milhares. Nas versões anteriores do EF Core, um modelo compilado precisava ser gerado manualmente, usando a linha de comando. Por exemplo:

dotnet ef dbcontext optimize

Depois de executar o comando, uma linha como .UseModel(MyCompiledModels.BlogsContextModel.Instance) deve ser adicionada ao OnConfiguring para informar o EF Core para usar o modelo compilado.

A partir do EF9, essa linha .UseModel não é mais necessária quando o tipo DbContext do aplicativo está no mesmo projeto/assembly do modelo compilado. O modelo compilado será detectado e usado automaticamente. Isso pode ser visto com o log EF sempre que ele estiver criando o modelo. A execução de um aplicativo simples mostra o EF criando o modelo quando o aplicativo é iniciado:

Starting application...
>> EF is building the model...
Model loaded with 2 entity types.

A saída da execução dotnet ef dbcontext optimize no projeto do modelo é:

PS D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model> dotnet ef dbcontext optimize

Build succeeded in 0.3s

Build succeeded in 0.3s
Build started...
Build succeeded.
>> EF is building the model...
>> EF is building the model...
Successfully generated a compiled model, it will be discovered automatically, but you can also call 'options.UseModel(BlogsContextModel.Instance)'. Run this command again when the model is modified.
PS D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model> 

Observe que a saída do log indica que o modelo foi criado ao executar o comando. Se executarmos o aplicativo novamente, após a recompilação, mas sem fazer nenhuma alteração no código, a saída será:

Starting application...
Model loaded with 2 entity types.

Observe que o modelo não foi criado ao iniciar o aplicativo porque o modelo compilado foi detectado e usado automaticamente.

Integração do MSBuild

Com a abordagem acima, o modelo compilado ainda precisa ser gerado novamente manualmente quando os tipos de entidade ou configuração DbContext forem alterados. No entanto, o EF9 vem com o MSBuild e o pacote de destinos que pode atualizar automaticamente o modelo compilado quando o projeto do modelo for criado! Para começar, instale o pacote NuGet Microsoft.EntityFrameworkCore.Tasks. Por exemplo:

dotnet add package Microsoft.EntityFrameworkCore.Tasks --version 9.0.0-preview.4.24205.3

Dica

Use a versão do pacote no comando acima que corresponda à versão do EF Core que você está usando.

Em seguida, habilite a integração definindo a propriedade EFOptimizeContext em seu arquivo .csproj. Por exemplo:

<PropertyGroup>
    <EFOptimizeContext>true</EFOptimizeContext>
</PropertyGroup>

Há propriedades adicionais e opcionais do MSBuild para controlar como o modelo é criado, equivalentes às opções passadas na linha de comando para o dotnet ef dbcontext optimize. Estão incluídos:

Propriedade do MSBuild Descrição
EFOptimizeContext Defina como true para habilitar os modelos compilados automaticamente.
DbContextName A classe DbContext a ser usada. Somente nome de classe ou totalmente qualificado com namespaces. Se essa opção for omitida, o Entity Framework Core encontrará a classe de contexto. Se existirem várias classes de contexto, essa opção será obrigatória.
EFStartupProject Caminho relativo para o projeto de inicialização. O valor padrão é a pasta atual.
EFTargetNamespace O namespace que será utilizado em todas as classes geradas. O padrão é gerado a partir do namespace raiz e do diretório de saída, além de CompiledModels.

Em nosso exemplo, precisamos especificar o projeto de inicialização:

<PropertyGroup>
  <EFOptimizeContext>true</EFOptimizeContext>
  <EFStartupProject>..\App\App.csproj</EFStartupProject>
</PropertyGroup>

Agora, se criarmos o projeto, poderemos ver o registro em log no momento da criação indicando que o modelo compilado está sendo criado:

Optimizing DbContext...
dotnet exec --depsfile D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.deps.json
  --additionalprobingpath G:\packages 
  --additionalprobingpath "C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages" 
  --runtimeconfig D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.runtimeconfig.json G:\packages\microsoft.entityframeworkcore.tasks\9.0.0-preview.4.24205.3\tasks\net8.0\..\..\tools\netcoreapp2.0\ef.dll dbcontext optimize --output-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\obj\Release\net8.0\ 
  --namespace NewInEfCore9 
  --suffix .g 
  --assembly D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\bin\Release\net8.0\Model.dll --startup-assembly D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.dll 
  --project-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model 
  --root-namespace NewInEfCore9 
  --language C# 
  --nullable 
  --working-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App 
  --verbose 
  --no-color 
  --prefix-output 

E a execução do aplicativo mostra que o modelo compilado foi detectado e, portanto, o modelo não foi criado novamente:

Starting application...
Model loaded with 2 entity types.

Agora, sempre que o modelo for alterado, o modelo compilado será recriado automaticamente assim que o projeto for criado.

[OBSERVAÇÃO!] Estamos trabalhando em alguns problemas de desempenho com alterações feitas no modelo compilado no EF8 e no EF9. Confira Problema 33483# para obter mais informações.

Coleções primitivas somente leitura

Dica

O código mostrado aqui vem de PrimitiveCollectionsSample.cs.

O EF8 introduziu suporte para mapeamento de matriz e listas mutáveis de tipos primitivos. Isso foi expandido no EF9 para incluir coleções/listas somente leitura. Especificamente, o EF9 dá suporte a coleções tipadas como IReadOnlyList, IReadOnlyCollectionou ReadOnlyCollection. Por exemplo, no código a seguir, DaysVisited será mapeado por convenção como uma coleção primitiva de datas:

public class DogWalk
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ReadOnlyCollection<DateOnly> DaysVisited { get; set; }
}

A coleção somente leitura pode ser apoiada por uma coleção normal e mutável, se desejado. Por exemplo, no código a seguir, DaysVisited pode ser mapeado como uma coleção primitiva de datas, enquanto ainda permite que o código na classe manipule a lista subjacente.

    public class Pub
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public IReadOnlyCollection<string> Beers { get; set; }

        private List<DateOnly> _daysVisited = new();
        public IReadOnlyList<DateOnly> DaysVisited => _daysVisited;
    }

Essas coleções podem ser usadas em consultas de maneira normal. Por exemplo, esta consulta LINQ:

var walksWithADrink = await context.Walks.Select(
    w => new
    {
        WalkName = w.Name,
        PubName = w.ClosestPub.Name,
        Count = w.DaysVisited.Count(v => w.ClosestPub.DaysVisited.Contains(v)),
        TotalCount = w.DaysVisited.Count
    }).ToListAsync();

O que se traduz para o seguinte SQL no SQLite:

SELECT "w"."Name" AS "WalkName", "p"."Name" AS "PubName", (
    SELECT COUNT(*)
    FROM json_each("w"."DaysVisited") AS "d"
    WHERE "d"."value" IN (
        SELECT "d0"."value"
        FROM json_each("p"."DaysVisited") AS "d0"
    )) AS "Count", json_array_length("w"."DaysVisited") AS "TotalCount"
FROM "Walks" AS "w"
INNER JOIN "Pubs" AS "p" ON "w"."ClosestPubId" = "p"."Id"

Especificar fator de preenchimento para chaves e índices

Dica

O código mostrado aqui vem de ModelBuildingSample.cs.

O EF9 dá suporte à especificação do fator de preenchimento do SQL Server ao usar Migrações do EF Core para criar chaves e índices. Nos documentos do SQL Server, "Quando um índice é criado ou recriado, o valor do fator de preenchimento determina a porcentagem de espaço em cada página em nível folha a ser preenchida com dados, reservando o restante em cada página como espaço livre para crescimento futuro".

O fator de preenchimento pode ser definido em um único ou em um composto de chaves e índices primários e alternativos. Por exemplo:

modelBuilder.Entity<User>()
    .HasKey(e => e.Id)
    .HasFillFactor(80);

modelBuilder.Entity<User>()
    .HasAlternateKey(e => new { e.Region, e.Ssn })
    .HasFillFactor(80);

modelBuilder.Entity<User>()
    .HasIndex(e => new { e.Name })
    .HasFillFactor(80);

modelBuilder.Entity<User>()
    .HasIndex(e => new { e.Region, e.Tag })
    .HasFillFactor(80);

Quando aplicado a tabelas existentes, isso alterará as tabelas para o fator de preenchimento para a restrição:

ALTER TABLE [User] DROP CONSTRAINT [AK_User_Region_Ssn];
ALTER TABLE [User] DROP CONSTRAINT [PK_User];
DROP INDEX [IX_User_Name] ON [User];
DROP INDEX [IX_User_Region_Tag] ON [User];

ALTER TABLE [User] ADD CONSTRAINT [AK_User_Region_Ssn] UNIQUE ([Region], [Ssn]) WITH (FILLFACTOR = 80);
ALTER TABLE [User] ADD CONSTRAINT [PK_User] PRIMARY KEY ([Id]) WITH (FILLFACTOR = 80);
CREATE INDEX [IX_User_Name] ON [User] ([Name]) WITH (FILLFACTOR = 80);
CREATE INDEX [IX_User_Region_Tag] ON [User] ([Region], [Tag]) WITH (FILLFACTOR = 80);

@deano-hunter contribuiu com este aprimoramento. Muito obrigado!

Tornar as convenções de construção de modelo existentes mais extensíveis

Dica

O código mostrado aqui vem de CustomConventionsSample.cs.

As convenções de criação de modelo público para aplicativos foram introduzidas no EF7. No EF9, facilitamos a extensão de algumas das convenções existentes. Por exemplo, o código para mapear propriedades por atributo no EF7 é o seguinte:

public class AttributeBasedPropertyDiscoveryConvention : PropertyDiscoveryConvention
{
    public AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
        : base(dependencies)
    {
    }

    public override void ProcessEntityTypeAdded(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionContext<IConventionEntityTypeBuilder> context)
        => Process(entityTypeBuilder);

    public override void ProcessEntityTypeBaseTypeChanged(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionEntityType? newBaseType,
        IConventionEntityType? oldBaseType,
        IConventionContext<IConventionEntityType> context)
    {
        if ((newBaseType == null
             || oldBaseType != null)
            && entityTypeBuilder.Metadata.BaseType == newBaseType)
        {
            Process(entityTypeBuilder);
        }
    }

    private void Process(IConventionEntityTypeBuilder entityTypeBuilder)
    {
        foreach (var memberInfo in GetRuntimeMembers())
        {
            if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
            {
                entityTypeBuilder.Property(memberInfo);
            }
            else if (memberInfo is PropertyInfo propertyInfo
                     && Dependencies.TypeMappingSource.FindMapping(propertyInfo) != null)
            {
                entityTypeBuilder.Ignore(propertyInfo.Name);
            }
        }

        IEnumerable<MemberInfo> GetRuntimeMembers()
        {
            var clrType = entityTypeBuilder.Metadata.ClrType;

            foreach (var property in clrType.GetRuntimeProperties()
                         .Where(p => p.GetMethod != null && !p.GetMethod.IsStatic))
            {
                yield return property;
            }

            foreach (var property in clrType.GetRuntimeFields())
            {
                yield return property;
            }
        }
    }
}

No EF9, isso pode ser simplificado para o seguinte:

public class AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
    : PropertyDiscoveryConvention(dependencies)
{
    protected override bool IsCandidatePrimitiveProperty(
        MemberInfo memberInfo, IConventionTypeBase structuralType, out CoreTypeMapping? mapping)
    {
        if (base.IsCandidatePrimitiveProperty(memberInfo, structuralType, out mapping))
        {
            if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
            {
                return true;
            }

            structuralType.Builder.Ignore(memberInfo.Name);
        }

        mapping = null;
        return false;
    }
}

Atualizar ApplyConfigurationsFromAssembly para chamar construtores não públicos

Nas versões anteriores do EF Core, o método ApplyConfigurationsFromAssembly criava apenas tipos de configuração instanciados com construtores públicos sem parâmetros. No EF9, aprimoramos as mensagens de erro geradas quando ocorre uma falha e também habilitamos a instanciação por meio de um construtor não público. Isso é útil ao colocar as configurações em uma classe aninhada privada que nunca deve ser instanciada pelo código da aplicação. Por exemplo:

public class Country
{
    public int Code { get; set; }
    public required string Name { get; set; }

    private class FooConfiguration : IEntityTypeConfiguration<Country>
    {
        private FooConfiguration()
        {
        }

        public void Configure(EntityTypeBuilder<Country> builder)
        {
            builder.HasKey(e => e.Code);
        }
    }
}

Como observação, algumas pessoas consideram este padrão uma abominação, pois acopla o tipo de entidade à configuração. Outras pessoas acham que é muito útil porque coloca a configuração com o tipo de entidade. Não vamos discutir isso aqui. :-)

HierarchyId do SQL Server

Dica

O código mostrado aqui vem de HierarchyIdSample.cs.

Geração de caminho açúcar para HierarchyId

O suporte de primeira classe para o tipo de HierarchyId do SQL Server foi adicionado no EF8. No EF9, um método de açúcar foi adicionado para facilitar a criação de novos nós filho na estrutura da árvore. Por exemplo, as seguintes consultas de código para uma entidade existente com uma propriedade HierarchyId:

var daisy = await context.Halflings.SingleAsync(e => e.Name == "Daisy");

Essa propriedade HierarchyId pode ser usada para criar nós filho sem qualquer manipulação de cadeia de caracteres explícita. Por exemplo:

var child1 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1), "Toast");
var child2 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 2), "Wills");

Se daisy tiver um HierarchyId de /4/1/3/1/, child1 obterá o HierarchyId "/4/1/3/1/1/1/1/", e child2 obterá o HierarchyId "/4/1/3/1/2/".

Para criar um nó entre esses dois filhos, um subnível adicional pode ser usado. Por exemplo:

var child1b = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1, 5), "Toast");

Isso cria um nó com um HierarchyId de /4/1/3/1/1.5/, colocando-o entre child1 e child2.

@Rezakazemi890 contribuiu com este aprimoramento. Muito obrigado!

Ferramentas

Menos recompilações

A ferramenta de linha de comando dotnet ef, por padrão, cria seu projeto antes de executar a ferramenta. Isso ocorre porque não recompilar antes de executar a ferramenta é uma fonte comum de confusão quando as coisas não funcionam. Desenvolvedores experientes podem usar a opção --no-build para evitar esse build, o que pode ser lento. No entanto, até mesmo a opção --no-build pode fazer com que o projeto seja recompilado na próxima vez que for criado fora das ferramentas do EF.

Acreditamos que uma contribuição da comunidade de @Suchiman tenha corrigido isso. No entanto, também estamos conscientes de que os ajustes em torno dos comportamentos do MSBuild têm uma tendência a ter consequências não intencionais, por isso estamos pedindo a pessoas como você que testem isso e relatem quaisquer experiências negativas.