Partilhar via


O que há de novo no EF Core 9

O EF Core 9 (EF9) é o próximo lançamento 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 mais recentes do EF9 e ajustes de API. Aqui, as amostras usam essas compilações diárias.

Dica

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

O EF9 destina-se ao .NET 8 e, portanto, pode ser usado com .NET 8 (LTS) ou .NET 9.

Dica

Os documentos de O que há de novo são atualizados para cada prévia. Todas as amostras estão configuradas para utilizar as compilações diárias do EF9, que geralmente apresentam várias semanas adicionais de trabalho concluído em comparação com a visualização mais recente. Recomendamos fortemente o uso das compilações diárias ao testar novos recursos para que você não esteja fazendo seus testes contra bits obsoletos.

Azure Cosmos DB para NoSQL

O EF 9.0 traz melhorias substanciais para o provedor EF Core para o 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 Azure Cosmos DB. As principais melhorias de alto nível estão listadas abaixo; para a lista completa, consulte esta questão épica.

Advertência

Como parte das melhorias no provedor, uma série de alterações significativas e impactantes teve que ser feita; se estiver a atualizar uma aplicação existente, leia a secção Alterações Importantes cuidadosamente.

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

Cada documento armazenado em um banco de dados do Azure Cosmos DB tem uma ID de recurso exclusiva. Além disso, cada documento pode conter uma "chave de partição" que determina o particionamento lógico de dados de modo que o banco de dados possa ser efetivamente dimensionado. Mais informações sobre como escolher chaves de partição podem ser encontradas em particionamento e dimensionamento horizontal no Azure Cosmos DB.

No EF 9.0, o provedor do Azure Cosmos DB é significativamente melhor em identificar comparações de chaves de partição em suas consultas LINQ e extraí-las para garantir que suas consultas sejam enviadas apenas para a partição relevante; isso pode melhorar muito o desempenho de suas consultas e reduzir as cobranças de RU. Por exemplo:

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

Nesta 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 "levantada" 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 em 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"]'

Aqui, nenhuma consulta SQL é enviada. Em vez disso, o provedor executa uma de leitura de ponto de extremamente eficiente ( API), que busca diretamente o documento dado a chave de partição e ID. Este é o tipo de leitura mais eficiente e económico que pode executar no Azure Cosmos DB; consulte a documentação do Azure Cosmos DB para obter mais informações sobre leituras pontuais.

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

Chaves de partição hierárquica

Dica

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

O Azure Cosmos DB originalmente dava suporte a uma única chave de partição, mas desde então expandiu os recursos de particionamento para também 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 total 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 construção de modelo, 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, UserIde SessionId:

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

Dica

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

Observe como, a partir do EF Core 9, propriedades de qualquer tipo mapeado podem ser usadas na chave de partição. Para tipos bool e 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 convertidos automaticamente em cadeias de caracteres.

Ao consultar, o EF extrai automaticamente os valores de chave de partição das consultas e os aplica à API de consulta do Azure Cosmos DB para garantir que as consultas sejam restringidas adequadamente ao menor número possível de partições. Por exemplo, considere a seguinte consulta LINQ que fornece todos os três valores de chave de partição na hierarquia:

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, userIde sessionId e os passará para a API de consulta do Azure Cosmos DB como o valor da chave de partição. Por exemplo, consulte 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 utilizadas como chave de partição para uma execução eficiente: ["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0].

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

Recursos de consulta LINQ significativamente melhorados

No EF 9.0, os recursos de tradução LINQ do provedor do Azure Cosmos DB foram muito expandidos e o provedor agora pode executar significativamente mais tipos de consulta. A lista completa de melhorias de consulta é muito longa para listar, mas aqui estão os principais destaques:

  • Suporte completo para coleções primitivas do EF, permitindo que você execute consultas LINQ em coleções de, por exemplo, ints ou strings. Consulte O que há de novo no EF8 em coleções primitivas para obter mais informações.
  • Suporte para consultas arbitrárias sobre coleções não primitivas.
  • Muitos operadores LINQ adicionais agora são suportados: indexação em coleções, Length/Count, ElementAt, Containse muitos outros.
  • Suporte para operadores agregados, como Count e Sum.
  • Traduções de funções adicionais (consulte a documentação de mapeamentos de função para obter a lista completa de traduções suportadas):
    • Traduções para DateTime e DateTimeOffset membros componentes (DateTime.Year, DateTimeOffset.Month...).
    • EF.Functions.IsDefined e EF.Functions.CoalesceUndefined agora permitem lidar com undefined valores.
    • string.Contains, StartsWith e EndsWith agora suportam StringComparison.OrdinalIgnoreCase.

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

Modelagem aprimorada alinhada aos padrões do Azure Cosmos DB e JSON

O EF 9.0 mapeia para documentos do Azure 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 quebrar alterações, existem APIs que permitem reverter para o comportamento anterior à 9.0 em todos os casos.

Propriedades id simplificadas sem discriminadores

Primeiro, versões anteriores do EF inseriram o valor discriminator 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 dentro da mesma partição de contêiner. A partir do EF 9.0, a propriedade id contém apenas o valor da chave:

{
    "id": 1099,
    ...
}

Esta é uma maneira mais natural de mapear para JSON e torna mais fácil para ferramentas e sistemas externos interagirem com documentos JSON gerados pelo EF; esses sistemas externos geralmente não estão cientes dos valores do discriminador EF, que são, por padrão, derivados de tipos .NET.

Observe que esta é uma alteração significativa, uma vez que o EF deixará de poder consultar documentos existentes com o antigo formato id. Uma API foi introduzida para reverter para o comportamento anterior; consulte a nota de alteração de quebra e a documentação para mais detalhes.

Propriedade discriminadora renomeada para $type

A propriedade discriminadora padrão foi anteriormente nomeada Discriminator. EF 9.0 altera o padrão para $type:

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

Isso segue o padrão emergente para polimorfismo JSON, permitindo uma melhor interoperabilidade com outras ferramentas. Por exemplo, o System.Text.Json do .NET também suporta polimorfismo, usando $type como seu nome de propriedade discriminador padrão (docs).

Observe que esta é uma alteração de quebra, uma vez que o EF não poderá mais consultar documentos existentes com o antigo nome de propriedade do discriminador. Consulte o aviso de alteração importante na nota para mais detalhes sobre como reverter para a nomeação anterior.

Pesquisa de semelhança vetorial (pré-visualização)

O Azure Cosmos DB agora oferece suporte de visualização para pesquisa de semelhança vetorial. A pesquisa vetorial é uma parte fundamental de alguns tipos de aplicação, incluindo IA, pesquisa semântica e outros. O Azure Cosmos DB permite que você armazene vetores diretamente em seus documentos junto com o restante de seus dados, o que significa que você pode executar todas as suas consultas em um único banco de dados. Isso pode simplificar consideravelmente a sua arquitetura e remover a necessidade de uma solução de base de dados vetorial adicional e dedicada na sua stack. Para saber mais sobre a pesquisa vetorial do Azure Cosmos DB, consulte a documentação.

Depois que seu contêiner do Azure Cosmos DB estiver configurado corretamente, usar a pesquisa vetorial via EF é uma simples questão de adicionar uma propriedade de vetor e configurá-la:

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 executar a pesquisa de semelhança vetorial:

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

Para obter mais informações, consulte a documentação do sobre pesquisa vetorial.

Suporte de paginação

O fornecedor do Azure Cosmos DB agora permite a paginação de resultados de consulta através de tokens de continuação , o que é muito mais eficiente e rentável 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 = firstPage.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 de forma eficiente em um ponto posterior, buscando os próximos 10 itens:

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

Para obter mais informações, consulte a seção de documentação sobre paginação.

FromSql para consultas SQL mais seguras

O provedor do Azure Cosmos DB permitiu consultas SQL via 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 obter mais informações, consulte a seção de documentação sobre paginação.

Acesso baseado em funções

O Azure Cosmos DB para NoSQL inclui um sistema RBAC (controle de acesso baseado em função) interno. Agora, isso é suportado pelo EF9 para todas as operações do plano de dados. No entanto, o SDK do Azure Cosmos DB não oferece suporte ao RBAC para operações de plano de gerenciamento no Azure Cosmos DB. Use a API de Gerenciamento do Azure em vez de EnsureCreatedAsync com RBAC.

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

O Azure Cosmos DB para NoSQL não dá suporte a APIs síncronas (bloqueio) 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íncronas, o que é uma prática incorreta, e pode causar bloqueios. Portanto, a partir do EF 9, uma exceção é lançada quando se tenta 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 DbContext digite:

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

Observe, no entanto, que planejamos remover totalmente o suporte de sincronização 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

Advertência

NativeAOT e pré-compilação de consulta são recursos altamente experimentais e ainda não são adequados para uso em produção. O suporte descrito abaixo deve ser visto como infraestrutura para o recurso final, que provavelmente será lançado com o EF 10. Recomendamos que você experimente o suporte atual e relate suas experiências, mas recomendamos que não implante aplicativos EF NativeAOT na produção.

O EF 9.0 traz suporte inicial e experimental para .NET NativeAOT, permitindo a publicação de aplicativos compilados antecipadamente que usam o EF para acessar bancos de dados. Para suportar consultas LINQ no modo NativeAOT, o EF depende de de pré-compilação de consulta: esse mecanismo identifica estaticamente consultas EF LINQ e gera intercetadores C#, que contêm código para executar cada consulta específica. Isso pode reduzir significativamente o tempo de inicialização do seu aplicativo, já que o trabalho pesado de processamento e compilação de suas consultas LINQ em SQL não acontece mais toda vez que seu aplicativo é iniciado. Em vez disso, o intercetador de cada consulta contém o SQL finalizado para essa consulta, bem como código otimizado para materializar os resultados do banco de dados como objetos .NET.

Por exemplo, dado um programa com a seguinte consulta EF:

var blogs = await context.Blogs.Where(b => b.Name == "foo").ToListAsync();

O EF gerará um intercetador C# em seu projeto, que assumirá a execução da consulta. Em vez de processar a consulta e traduzi-la para SQL sempre que o programa é iniciado, o intercetador tem o SQL incorporado diretamente nela (para o SQL Server, neste caso), permitindo que seu programa seja iniciado muito mais rapidamente:

var relationalCommandTemplate = ((IRelationalCommandTemplate)(new RelationalCommand(materializerLiftableConstantContext.CommandBuilderDependencies, "SELECT [b].[Id], [b].[Name]\nFROM [Blogs] AS [b]\nWHERE [b].[Name] = N'foo'", new IRelationalParameter[] { })));

Além disso, o mesmo intercetador contém código para materializar seu objeto .NET a partir dos resultados do banco de dados:

var instance = new Blog();
UnsafeAccessor_Blog_Id_Set(instance) = dataReader.GetInt32(0);
UnsafeAccessor_Blog_Name_Set(instance) = dataReader.GetString(1);

Isso utiliza outro novo recurso do .NET - acessores sem segurança, para injetar dados do banco de dados nos campos privados do seu objeto.

Se você está interessado em NativeAOT e gosta de experimentar recursos de ponta, experimente! Apenas esteja ciente de que o recurso deve ser considerado instável, e atualmente tem muitas limitações; esperamos estabilizá-lo e torná-lo mais adequado para uso de produção no EF 10.

Consulte a página de documentação do NativeAOT para obter mais detalhes.

Tradução 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 suportados foram melhoradas, para melhor desempenho e legibilidade.

O número de melhorias é muito grande para listá-las todas aqui. Abaixo, destacam-se algumas das melhorias mais importantes; Consulte esta edição para obter uma lista mais completa do trabalho realizado na versão 9.0.

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

Tipos complexos: suporte a GroupBy e ExecuteUpdate

AgruparPor

Dica

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

O EF9 oferece suporte ao agrupamento por uma instância de tipo complexo. Por exemplo:

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

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

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.

Da mesma forma, no 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'

Anteriormente, você tinha que listar manualmente as diferentes propriedades do tipo complexo em sua chamada ExecuteUpdate.

Remover elementos desnecessários do SQL

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

Poda de mesa

Como um 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 utiliza o mapeamento de herança tipo-por-tabela (TPT) :

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 uma Encomenda:

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 à tabela DiscountedOrders, embora nenhuma coluna tenha sido referenciada nela. O EF9 gera um SQL simplificado 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, já que a expressão externa SELECT simplesmente 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 significativamente o SQL em alguns casos; pode verificar algumas das alterações SQL nos testes para ver o efeito.

Traduções envolvendo MAIOR/MENOR

Dica

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

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

Importante

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

Por exemplo, consultas usando 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 é convertida para o seguinte SQL ao usar o EF9 em execução 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 é convertida para o seguinte SQL ao usar o EF9 em execução 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

Finalmente, 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 é convertida para o seguinte SQL ao usar o EF9 em execução 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 seguinte método de consulta:

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

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

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 ".NET Blog" 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, uma vez que a mesma consulta pode ser executada com muitos valores diferentes para id. Criar uma constante nesse caso 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 método EF.Parameter

O EF9 introduz o método EF.Parameter para fazer o oposto. Ou seja, force 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 ".NET Blog":

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

Coleções primitivas parametrizadas

O EF8 mudou a maneira como algumas consultas que usam coleções primitivas são traduzidas. Quando uma consulta LINQ contém uma coleção primitiva parametrizada, o EF converte seu conteúdo em JSON e passa como um único valor de parâmetro para a consulta:

async Task<List<Post>> GetPostsPrimitiveCollection(int[] ids)
    => await context.Posts
        .Where(e => e.Title == ".NET Blog" && ids.Contains(e.Id))
        .ToListAsync();

Isso resultará na seguinte tradução no SQL Server:

Executed DbCommand (5ms) [Parameters=[@__ids_0='[1,2,3]' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] IN (
    SELECT [i].[value]
    FROM OPENJSON(@__ids_0) WITH ([value] int '$') AS [i]
)

Isso permite ter a mesma consulta SQL para diferentes coleções parametrizadas (apenas o valor do parâmetro muda), mas em algumas situações pode levar a problemas de desempenho, pois o banco de dados não é capaz de planejar a consulta de forma ideal. O método EF.Constant pode ser usado para reverter para a tradução anterior.

A consulta a seguir usa EF.Constant para esse efeito:

async Task<List<Post>> GetPostsForceConstantCollection(int[] ids)
    => await context.Posts
        .Where(
            e => e.Title == ".NET Blog" && EF.Constant(ids).Contains(e.Id))
        .ToListAsync();

O SQL resultante é o seguinte:

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] IN (1, 2, 3)

Além disso, o EF9 introduz TranslateParameterizedCollectionsToConstantsopção de contexto que pode ser usada para evitar a parametrização de coleção primitiva para todas as consultas. Também adicionamos uma TranslateParameterizedCollectionsToParameters complementar que força a parametrização de coleções primitivas explicitamente (este é o comportamento padrão).

Dica

O método EF.Parameter substitui a opção de contexto. Se quiser impedir a parametrização de coleções primitivas para a maioria das consultas (mas não todas), você pode definir a opção de contexto TranslateParameterizedCollectionsToConstants e usar EF.Parameter para as consultas ou variáveis individuais que deseja parametrizar.

Subconsultas não correlacionadas inseridas

Dica

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

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

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

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

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 ao 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

Agregar funções em subconsultas e agregações no SQL Server

O EF9 melhora a tradução de algumas consultas complexas usando funções agregadas compostas sobre subconsultas ou outras funções agregadas. Abaixo está um exemplo de tal consulta:

var latestPostsAverageRatingByLanguage = await context.Blogs
    .Select(x => new
    {
        x.Language,
        LatestPostRating = x.Posts.OrderByDescending(xx => xx.PublishedOn).FirstOrDefault()!.Rating
    })
    .GroupBy(x => x.Language)
    .Select(x => x.Average(xx => xx.LatestPostRating))
    .ToListAsync();

Primeiro, Select calcula LatestPostRating para cada Post que requer uma subconsulta ao traduzir para SQL. Mais adiante na consulta, estes resultados são agregados usando a operação Average. O SQL resultante tem a seguinte aparência quando executado no SQL Server:

SELECT AVG([s].[Rating])
FROM [Blogs] AS [b]
OUTER APPLY (
    SELECT TOP(1) [p].[Rating]
    FROM [Posts] AS [p]
    WHERE [b].[Id] = [p].[BlogId]
    ORDER BY [p].[PublishedOn] DESC
) AS [s]
GROUP BY [b].[Language]

Em versões anteriores, o EF Core gerava SQL inválido para consultas semelhantes, tentando aplicar a operação de agregação diretamente sobre a subconsulta. Isso não é permitido no SQL Server e resulta em uma exceção. O mesmo princípio aplica-se a uma consulta que utiliza agregação sobre outra agregação.

var topRatedPostsAverageRatingByLanguage = await context.Blogs.
Select(x => new
{
    x.Language,
    TopRating = x.Posts.Max(x => x.Rating)
})
.GroupBy(x => x.Language)
.Select(x => x.Average(xx => xx.TopRating))
.ToListAsync();

Observação

Essa alteração não afeta o Sqlite, que suporta agregações em subconsultas (ou outras agregações) e não suporta LATERAL JOIN (APPLY). Abaixo está o SQL para a primeira consulta em execução no Sqlite:

SELECT ef_avg((
    SELECT "p"."Rating"
    FROM "Posts" AS "p"
    WHERE "b"."Id" = "p"."BlogId"
    ORDER BY "p"."PublishedOn" DESC
    LIMIT 1))
FROM "Blogs" AS "b"
GROUP BY "b"."Language"

As consultas usando 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 gera agora uma tradução mais eficiente utilizando 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 são comparados. No EF8 usamos para traduzir comparações usando semântica nula de banco de dados. Isso produziria resultados diferentes de consultas semelhantes 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 cujos NullableIntOne ou NullableIntTwo estão definidos como nulos.

Na 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 cujos NullableIntOne ou NullableIntTwo estão definidos como nulos (em vez de true esperados em C#). Executando o mesmo cenário no Sqlite gerado:

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

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

O EF9 agora lida adequadamente com esses cenários, produzindo resultados consistentes com o LINQ to Objects e entre diferentes provedores.

Este reforço foi contribuído por @ranma42. Muito obrigado!

Tradução de operadores Order e OrderDescending LINQ

O EF9 permite a tradução de operações de encomenda simplificadas LINQ (Order e OrderDescending). Estes funcionam de forma semelhante à OrderBy/OrderByDescending mas não requerem um argumento. Em vez disso, eles aplicam ordenação padrão - para entidades, isso significa ordenar com base em valores de chave primária e, para outros tipos, ordenar com base nos próprios valores.

Abaixo está um exemplo de consulta que tira proveito dos operadores de pedidos simplificados:

var orderOperation = await context.Blogs
    .Order()
    .Select(x => new
    {
        x.Name,
        OrderedPosts = x.Posts.OrderDescending().ToList(),
        OrderedTitles = x.Posts.Select(xx => xx.Title).Order().ToList()
    })
    .ToListAsync();

Esta consulta é equivalente ao seguinte:

var orderByEquivalent = await context.Blogs
    .OrderBy(x => x.Id)
    .Select(x => new
    {
        x.Name,
        OrderedPosts = x.Posts.OrderByDescending(xx => xx.Id).ToList(),
        OrderedTitles = x.Posts.Select(xx => xx.Title).OrderBy(xx => xx).ToList()
    })
    .ToListAsync();

e produz o seguinte SQL:

SELECT [b].[Name], [b].[Id], [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata], [p0].[Title], [p0].[Id]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
LEFT JOIN [Posts] AS [p0] ON [b].[Id] = [p0].[BlogId]
ORDER BY [b].[Id], [p].[Id] DESC, [p0].[Title]

Observação

Order e OrderDescending métodos são suportados apenas para coleções de entidades, tipos complexos ou escalares - eles não funcionarão em projeções mais complexas, por exemplo, coleções de tipos anônimos contendo várias propriedades.

Esta melhoria foi contribuída pelo ex-aluno da EF Team @bricelam. Muito obrigado!

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

O EF9 traz muitas otimizações ao redor do SQL CASE/WHEN, COALESCE, negação e várias outras construções; a maioria destas contribuições foi realizada por Andrea Canciani (@ranma42) - agradecemos imensamente por todas estas contribuições! Abaixo, detalharemos apenas algumas dessas otimizações em torno da 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" a operação NOT na 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, costumava resultar em blocos CASE aninhados:

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 bool negada:

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

O EF8 geraria um bloco 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, esta tradução foi simplificada e agora usa NOT bit-a-bit (~):

SELECT [p].[Title], ~[p].[Archived] AS [Active]
FROM [Posts] AS [p]

Melhor suporte para Azure SQL e Azure Synapse

O EF9 permite mais flexibilidade ao especificar o tipo de SQL Server que está sendo direcionado. Em vez de configurar o EF com UseSqlServer, agora você pode especificar UseAzureSql ou UseAzureSynapse. Isso permite que o EF produza SQL melhor ao usar o Azure SQL ou o Azure Synapse. O EF pode aproveitar as funcionalidades específicas da base de dados (por exemplo, tipo dedicado para JSON no Azure SQL) ou contornar as suas limitações (por exemplo, a cláusula ESCAPE não está disponível quando se utiliza LIKE na Azure Synapse).

Outras melhorias na consulta

  • O suporte a consultas de coleções primitivas introduzido no EF8 foi estendido para suportar todos os tipos de ICollection<T>. Observe que isso se aplica apenas a coleções de parâmetros e coleções em linha - coleções primitivas que fazem parte de entidades ainda estão limitadas a matrizes, listas e no EF9, que também são matrizes/listas de somente leitura.
  • Novas funções ToHashSetAsync para retornar os resultados de uma consulta como um HashSet (#30033, colaborado por @wertzui).
  • TimeOnly.FromDateTime e FromTimeSpan agora estão traduzidos no SQL Server (#33678).
  • Agora o ToString sobre enums está traduzido (#33706, contribuído por @Danevandy99).
  • string.Join agora se traduz em CONCAT_WS em contexto não agregado no SQL Server (#28899).
  • EF.Functions.PatIndex agora se traduz para a função PATINDEX do SQL Server, 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, contribuído por @ranma42).
  • Correções e otimizações para string.StartsWith e EndsWith (#31482).
  • Convert.To* métodos agora podem aceitar argumentos do tipo object (#33891, contribuído por @imangd).
  • A operação (XOR) Exclusive-Or agora está traduzida no SQL Server (#34071, contribuído por @ranma42).
  • Otimizações em torno da anulabilidade para operações COLLATE e AT TIME ZONE (#34263, contribuído por @ranma42).
  • Otimizações para DISTINCT em relação a IN, EXISTS e operações de conjuntos (#34381, contribuído por @ranma42).

As melhorias acima foram apenas algumas das mais importantes melhorias de consulta no EF9; Consulte esta edição para obter uma listagem mais completa.

Migrações

Proteção contra migrações simultâneas

O EF9 introduz um mecanismo de bloqueio para proteger contra várias execuções de migração acontecendo simultaneamente, pois isso poderia deixar o banco de dados em um estado corrompido. Isso não acontece quando as migrações são implantadas no ambiente de produção usando métodos recomendados, mas pode acontecer se as migrações forem aplicadas em tempo de execução usando o método DbContext.Database.MigrateAsync(). Recomendamos aplicar migrações na implantação, em vez de como parte da inicialização do aplicativo, mas isso pode resultar em arquiteturas de aplicativos mais complicadas (por exemplo, ao usar projetos .NET Aspire).

Observação

Se você estiver usando o banco de dados Sqlite, consulte possíveis problemas associados a esse recurso.

Avisar quando várias operações de migração não puderem ser executadas dentro de uma transação

A maioria das operações realizadas durante as migrações é protegida por uma transação. Isso garante que, se por algum motivo a migração falhar, o banco de dados não acabará em um estado corrompido. No entanto, algumas operações não são encapsuladas em uma transação (por exemplo, operações de em tabelas otimizadas para memória do SQL Serverou operações de alteração de banco de dados, como modificar o agrupamento de banco de dados). Para evitar corromper o banco de dados em caso de falha na migração, é recomendável que essas operações sejam executadas isoladamente usando uma migração separada. O EF9 agora deteta um cenário quando uma migração contém várias operações, uma das quais não pode ser encapsulada em uma transação, e emite um aviso.

Melhoria na semeadura de dados

O EF9 introduziu uma maneira conveniente de executar a propagação de dados, que é preencher o banco de dados com dados iniciais. DbContextOptionsBuilder agora contém UseSeeding e UseAsyncSeeding métodos que são executados quando o DbContext é inicializado (como parte de EnsureCreatedAsync).

Observação

Se o aplicativo tiver sido executado anteriormente, o banco de dados já pode conter os dados de exemplo (que teriam sido adicionados na primeira inicialização do contexto). Como tal, UseSeedingUseAsyncSeeding deve verificar se existem dados antes de tentar preencher a base de dados. Isso pode ser conseguido emitindo uma simples consulta EF.

Aqui está um exemplo de como esses métodos podem ser usados:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFDataSeeding;Trusted_Connection=True;ConnectRetryCount=0")
        .UseSeeding((context, _) =>
        {
            var testBlog = context.Set<Blog>().FirstOrDefault(b => b.Url == "http://test.com");
            if (testBlog == null)
            {
                context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
                context.SaveChanges();
            }
        })
        .UseAsyncSeeding(async (context, _, cancellationToken) =>
        {
            var testBlog = await context.Set<Blog>().FirstOrDefaultAsync(b => b.Url == "http://test.com", cancellationToken);
            if (testBlog == null)
            {
                context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
                await context.SaveChangesAsync(cancellationToken);
            }
        });

Mais informações podem ser encontradas aqui.

Outras melhorias na migração

  • Ao alterar uma tabela existente em uma tabela temporal do SQL Server, o tamanho do código de migração foi significativamente reduzido.

Construção de modelos

Modelos compilados automaticamente

Dica

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

Os modelos compilados podem melhorar o tempo de inicialização para aplicações com modelos grandes, ou seja, com contagens de tipos de entidade nas centenas ou milhares. Nas versões anteriores do EF Core, um modelo compilado tinha que 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 dizer ao EF Core para usar o modelo compilado.

A partir do EF9, essa linha de .UseModel não é mais necessária quando o tipo de DbContext do aplicativo está no mesmo projeto/assembly que o modelo compilado. Em vez disso, o modelo compilado será detetado e usado automaticamente. Isso pode ser visto ao ter o log do EF sempre que estiver construindo o modelo. A execução de um aplicativo simples mostra o EF construindo 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 de dotnet ef dbcontext optimize no projeto 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 de log indica que o modelo de foi criado ao executar o comando. Se agora executarmos o aplicativo novamente, após a reconstrução, mas sem fazer nenhuma alteração de código, a saída é:

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 detetado e usado automaticamente.

Integração com MSBuild

Com a abordagem acima, o modelo compilado ainda precisa ser regenerado manualmente quando os tipos de entidade ou a configuração DbContext são alterados. No entanto, o EF9 vem com um pacote de tarefas do MSBuild que pode atualizar automaticamente o modelo compilado quando o projeto de modelo é construído! Para começar, instale o pacote Microsoft.EntityFrameworkCore.Tasks NuGet. Por exemplo:

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

Dica

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

Em seguida, habilite a integração definindo as propriedades EFOptimizeContext e EFScaffoldModelStage em seu arquivo .csproj. Por exemplo:

<PropertyGroup>
    <EFOptimizeContext>true</EFOptimizeContext>
    <EFScaffoldModelStage>build</EFScaffoldModelStage>
</PropertyGroup>

Agora, se construirmos o projeto, podemos ver o registro em log no momento da compilação indicando que o modelo compilado está sendo construído:

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
  --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 detetado e, portanto, o modelo não é construído novamente:

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

Agora, sempre que o modelo mudar, o modelo compilado será automaticamente reconstruído assim que o projeto for construído.

Para obter mais informações, consulte integração do MSBuild.

Coleções primitivas de somente leitura

Dica

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

O EF8 introduziu suporte para matrizes de mapeamento e listas mutáveis de tipos primitivos. Isso foi expandido no EF9 para incluir coleções/listas somente leitura. Especificamente, o EF9 oferece suporte a coleções digitadas como IReadOnlyList, IReadOnlyCollectionou ReadOnlyCollection. Por exemplo, no código a seguir, o 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 suportada, se desejado, por uma coleção normal e mutável. 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 da 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 no 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 oferece suporte à especificação do de fator de preenchimento do SQL Server ao usar as migrações do EF Core para criar chaves e índices. Nos documentos do SQL Server, "Quando um índice é criado ou reconstruído, o valor do fator de preenchimento determina a porcentagem de espaço em cada página de nível de 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 chaves e índices primários e alternativos únicos ou compostos. 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 ajustará as tabelas ao fator de preenchimento conforme 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);

Esta melhoria foi contribuída por @deano-hunter. Muito obrigado!

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

Dica

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

As convenções públicas de construção de modelos para aplicações foram introduzidas no EF7. No EF9, facilitámos o alargamento 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 até 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

Em versões anteriores do EF Core, o método ApplyConfigurationsFromAssembly apenas instanciava tipos de configuração com construtores públicos e sem parâmetros. No EF9, melhoramos as mensagens de erro geradas quando isso falhae também habilitamos a instanciação por construtor não público. Isto é útil ao colocar a configuração numa 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 um aparte, algumas pessoas pensam que este padrão é uma abominação porque acopla o tipo de entidade à configuração. Outras pessoas acham que é muito útil porque co-localiza a configuração com o tipo de entidade. Não vamos debater isso aqui. :-)

ID da hierarquia do SQL Server

Dica

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

Açúcar para geração de caminho HierarchyId

O suporte de primeira classe para o tipo HierarchyId do SQL Server foi adicionado no EF8. No EF9, um método auxiliar foi adicionado para facilitar a criação de novos nós secundários na estrutura da árvore. Por exemplo, o código a seguir consulta 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 explícita de cadeia de caracteres. 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/, então, child1 receberá o HierarchyId "/4/1/3/1/1/", e child2 receberá o HierarchyId "/4/1/3/1/2/".

Para criar um ponto entre esses dois filhos, um subnível adicional pode ser utilizado. Por exemplo:

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

Esta ação cria um nó com um HierarchyId de /4/1/3/1/1.5/, posicionando-o entre child1 e child2.

Este reforço foi contribuído por @Rezakazemi890. Muito obrigado!

Ferramentas

Menos reconstruçõ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 reconstruir 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 essa compilação, que pode ser lenta. No entanto, mesmo a opção --no-build pode fazer com que o projeto tenha que ser reconstruído na próxima vez que for gerado fora das ferramentas do EF.

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