Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
O EF Core 9.0 (EF9) foi lançado em novembro de 2024 e é uma versão de Suporte de Curto Prazo (STS). O EF9 terá suporte até 10 de novembro de 2026.
O EF9 está disponível como builds diários que contêm todos os recursos EF9 mais recentes e ajustes de API. Os exemplos aqui usam esses builds diários.
Dica
Você pode executar e depurar os 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 .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 versão preliminar. Todos os exemplos são configurados para usar os builds diários do 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 substanciais 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 Azure Cosmos DB práticas recomendadas. As principais melhorias de alto nível estão listadas abaixo; para obter uma lista completa, confira esta questão épica.
Aviso
Como parte das melhorias que estão sendo feitas no provedor, várias mudanças significativas e de alto impacto tiveram que ser feitas; se você estiver atualizando um aplicativo existente, leia cuidadosamente a seção de mudanças disruptivas.
Melhorias na consulta com chaves de partição e IDs de documento
Cada documento armazenado em um banco de dados 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 dos dados, de modo que o banco de dados possa ser dimensionado de forma eficaz. Para obter mais informações sobre a escolha de chaves de partição, consulte Particionamento e escala horizontal no Azure Cosmos DB.
No EF 9.0, o provedor de Azure Cosmos DB é significativamente melhor em identificar comparações de chave 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 os encargos de RU. 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 API (ReadItem) extremamente eficiente, que busca diretamente o documento considerando a chave de partição e a ID. Esse é o tipo de leitura mais eficiente e econômico que você pode executar no Azure Cosmos DB; se 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 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.
Azure Cosmos DB originalmente suportava uma única chave de partição, mas desde então expandiu os recursos de particionamento para também dar suporte a subpartitioning 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
Essa definição de chave de partição segue o exemplo dado em Conseque 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 consultar, o EF extrai automaticamente os valores de chave de partição de consultas e os aplica à API de consulta Azure Cosmos DB para garantir que as consultas sejam restritas adequadamente ao menor número de partições possível. Por exemplo, considere a seguinte consulta LINQ que fornece 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, userId e sessionId e os passará para a API de consulta Azure Cosmos DB 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","00aa00aa-bb11-cc22-dd33-44ee44ee44ee",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","00aa00aa-bb11-cc22-dd33-44ee44ee44ee",7.0].
Para obter mais informações, consulte a documentação sobre como consultar com chaves de partição.
Recursos de consulta LINQ consideravelmente aprimorados
No EF 9.0, os recursos de tradução LINQ do provedor de 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, mas estes são os principais destaques:
- 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. Confira as novidades no EF8: coleções primitivas para obter mais informações.
- 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,Containse muitos outros. - Suporte para operadores agregados, como
CounteSum. - Traduções de funções adicionais (consulte a documentação de mapeamentos de funções para a lista completa de traduções com suporte):
- Traduções para membros de componentes
DateTimeeDateTimeOffset(DateTime.Year,DateTimeOffset.Month...). -
IsDefined e CoalesceUndefined agora permitem lidar com valores
undefined. -
string.Contains,StartsWitheEndsWithagora dão suporte aStringComparison.OrdinalIgnoreCase.
- Traduções para membros de componentes
Para obter a lista completa de melhorias de consulta, confira este tópico:
Modelagem aprimorada alinhada aos padrões de Azure Cosmos DB e JSON
O EF 9.0 mapeia para Azure Cosmos DB documentos de maneiras mais naturais para um banco de dados de documento 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 entre ferramentas e sistemas externos com documentos JSON gerados por EF; esses sistemas externos geralmente não estão cientes dos valores discriminatórios de EF, que são, por padrão, derivados de tipos de .NET.
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 de alteração interruptiva 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, System.Text.Json do .NET também dá suporte ao polimorfismo, usando $type como seu nome de propriedade discriminatória padrão (docs).
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 interruptiva para obter detalhes sobre como reverter para a nomenclatura anterior.
Pesquisa de similaridade vetorial (versão prévia)
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. Azure Cosmos DB permite que você armazene vetores diretamente em seus documentos junto com o restante dos dados, o que significa que você pode executar todas as suas consultas em um único banco de dados. Isso pode simplificar consideravelmente sua arquitetura e eliminar a necessidade de uma solução adicional dedicada de banco de dados vetorial no seu stack tecnológico. Para saber mais sobre a pesquisa de vetores do Azure Cosmos DB, consulte a documentação.
Depois que o contêiner Azure Cosmos DB estiver configurado corretamente, usar a pesquisa de vetor por meio do EF é uma questão simples 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 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 obter mais informações, consulte a documentação sobre a pesquisa de vetor.
Suporte à paginação
O provedor de Azure Cosmos DB agora permite paginar resultados de consulta por meio de tokens de continuaçã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 = firstPage.ContinuationToken;
foreach (var post in page.Values)
{
// Display/send the posts to the user
}
O novo operador ToPageAsync retorna um CosmosPage<T>, 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 obter mais informações, consulte a seção de documentação sobre paginação.
FromSql para consultas SQL mais seguras
O provedor de Azure Cosmos DB permitiu a consulta SQL por meio de 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ção
Azure Cosmos DB para NoSQL inclui um sistema de controle de acesso baseado em função (RBAC) embutido. Isso agora tem suporte do EF9 para todas as operações do plano de dados. No entanto, Azure Cosmos DB SDK não dá suporte ao RBAC para operações de plano de gerenciamento no Azure Cosmos DB. Use Azure API de Gerenciamento em vez de EnsureCreatedAsync com RBAC.
A E/S síncrona agora é bloqueada por padrã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íncrono, que é uma má prática 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 DbContext, digite:
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
Aviso
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 será lançado em uma versão futura. Incentivamos você a experimentar o suporte atual e relatar suas experiências, mas não recomendamos a implantação de aplicativos EF NativeAOT em produção.
O EF 9.0 oferece 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 dar suporte a consultas LINQ no modo NativeAOT, o EF depende da pré-compilação de consulta: esse mecanismo identifica estaticamente consultas LINQ EF e gera interceptadores C#, que contêm código para executar cada consulta específica. Isso pode reduzir significativamente o tempo de inicialização do aplicativo, pois o trabalho pesado de processamento e compilação de suas consultas LINQ em SQL não acontece mais toda vez que o aplicativo é iniciado. Em vez disso, o interceptador de cada consulta contém o SQL finalizado para essa consulta, bem como código otimizado para materializar resultados de 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 interceptor C# em seu projeto, que assumirá a execução da consulta. Em vez de processar a consulta e traduzi-la para o SQL sempre que o programa for iniciado, o interceptor tem o SQL inserido diretamente nele (para SQL Server nesse caso), permitindo que seu programa inicie 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 interceptador contém código para materializar seu objeto .NET 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 usa outro novo recurso de .NET - unsafe accessors, para injetar dados do banco de dados nos campos privados do objeto.
Se você está interessado no 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 em produção no EF 10.
Consulte a página de documentação do NativeAOT para obter mais detalhes.
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. Abaixo, algumas das melhorias mais importantes são realçadas; confira esta questão para obter uma listagem mais completa do trabalho feito 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 gerado pelo EF Core!
Tipos complexos: suporte a GroupBy e ExecuteUpdate
AgruparPor
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]
ExecutarAtualização
Dica
O código mostrado aqui vem de ExecuteUpdateSample.cs.
De maneira semelhante, o EF9 ExecuteUpdateAsync 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 ExecuteUpdateAsync.
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 significativamente o SQL em alguns casos; sinta-se à vontade para examinar algumas das alterações do 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 SQL GREATEST e LEAST.
Importante
As funções GREATEST e LEAST foram introduzidas a bancos de dados SQL Server/SQL do Azure na versão 2022. Visual Studio 2022 instala SQL Server 2019 por padrão. É recomendável 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 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 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 é traduzida 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
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 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 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 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 .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 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 EF. Método de parâmetro
O EF9 apresenta o método 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 string do ".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 alterou a forma 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 o conteúdo em JSON e o transmite como um único valor de parâmetro da 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 em 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 (somente o valor do parâmetro é alterado). No entanto, em algumas situações, isso pode levar a problemas de desempenho, pois o banco de dados não é capaz de planejar a consulta de maneira ideal. O método Constant pode ser usado para reverter para a tradução anterior.
A consulta a seguir usa Constant para esse fim:
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 apresenta TranslateParameterizedCollectionsToConstantsa opção de contexto que pode ser usada para impedir a parametrização da coleção primitiva para todas as consultas. Também adicionamos um complemento TranslateParameterizedCollectionsToParameters que força a parametrização de coleções primitivas explicitamente (este é o comportamento padrão).
Dica
O método Parameter substitui a opção de contexto. Se você quiser evitar a parametrização de coleções primitivas para a maioria das consultas (mas não todas), poderá definir a opção de contexto TranslateParameterizedCollectionsToConstants e usar Parameter para as consultas ou variáveis individuais que deseja parametrizar.
Subconsultas não correlacionadas integradas
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 = 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 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
Agregar funções em subconsultas e agregações em SQL Server
O EF9 melhora a conversão de algumas consultas complexas usando funções agregadas compostas em subconsultas ou outras funções agregadas. Veja abaixo um exemplo dessa 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, o que requer uma subconsulta ao converter em SQL. Posteriormente na consulta, esses resultados são agregados com 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]
Nas versões anteriores, o EF Core gerava SQL inválido para consultas semelhantes, tentando aplicar a operação de agregação diretamente na subconsulta. Isso não é permitido em SQL Server e resulta em uma exceção. O mesmo princípio se aplica a consultas que usam agregação em vez de 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 dá suporte a agregações em subconsultas (ou outras agregações) e não dá suporte a LATERAL JOIN (APPLY). Abaixo está o SQL da 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 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"
resultando na exceção Nullable object must have a value, pois a tradução produz o valor null para casos em que NullableIntOne ou NullableIntTwo são nulos.
Agora o EF9 lida corretamente com esses cenários, produzindo resultados consistentes com o LINQ to Objects e em diferentes provedores.
Esse aprimoramento foi contribuído por @ranma42. Muito obrigado!
Conversão dos operadores LINQ Order e OrderDescending
O EF9 permite a conversão de operações de ordenação simplificadas do LINQ (Order e OrderDescending). Eles funcionam de forma semelhante a OrderBy/OrderByDescending, mas não exigem um argumento. Em vez disso, eles aplicam a 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 valores propriamente ditos.
Confira abaixo um exemplo de consulta que aproveita os operadores de ordenação 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();
Essa consulta é equivalente à especificação a seguir:
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
Os métodos Order e OrderDescending são compatíveis apenas com 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.
Esse aprimoramento foi contribuído pelo ex-aluno da Equipe EF @bricelam. Muito obrigado!
Tradução aprimorada do operador de negação lógica (!)
O EF9 traz muitas otimizações em torno de SQL CASE/WHEN, COALESCEnegação e várias outras construções; a maioria delas foram contribuidas 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, inserimos a operação NOT na comparação.
SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE instr("p"."Content", 'Announcing') <= 0
Outro exemplo, aplicável a 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 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]
Em 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 em consultas 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 conversão foi simplificada e agora usa a operação NOT bit a bit (~):
SELECT [p].[Title], ~[p].[Archived] AS [Active]
FROM [Posts] AS [p]
Melhor suporte para SQL do Azure 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 um SQL melhor ao usar SQL do Azure ou Azure Synapse. O EF pode aproveitar os recursos específicos do banco de dados (por exemplo, tipo dedicado para JSON no SQL do Azure) ou contornar suas limitações (por exemplo, a cláusula ESCAPE não está disponível ao usar LIKE em Azure Synapse).
Outras melhorias de consulta
- O suporte à consulta de coleções primitivas introduzidas no EF8 foi estendido para dar suporte a todos os
ICollection<T>tipos. Observe que isso se aplica apenas 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 de leitura. - Novas
ToHashSetAsyncfunções para retornar os resultados de uma consulta como umHashSet(#30033, contribuído por @wertzui). -
TimeOnly.FromDateTimeeFromTimeSpanagora são traduzidos em SQL Server (#33678). -
ToStringas enumerações agora são traduzidas (#33706, contribuído por @Danevandy99). -
string.Joinagora se traduz em CONCAT_WS em contexto não agregado em SQL Server (#28899). -
EF.Functions.PatIndexagora se traduz para a função SQL ServerPATINDEX, que retorna a posição inicial da primeira ocorrência de um padrão (#33702, @smnsht). -
SumeAverageagora funcionam para decimais no SQLite (nº 33721, contribuição de @ranma42). - Correções e otimizações para
string.StartsWitheEndsWith(nº 31482). -
Convert.To*métodos agora podem aceitar argumentos do tipoobject(#33891, contribuído por @imangd). - A operação Exclusive-Or (XOR) agora é traduzida no SQL Server (#34071, contribuído por @ranma42).
- Otimizações em torno da nulidade para operações
COLLATEeAT TIME ZONE(#34263, contribuído por @ranma42). - Otimizações para
DISTINCTemIN,EXISTSe operações de conjunto (número 34381, contribuído por @ranma42).
Estas foram apenas algumas das melhorias de consulta mais importantes no EF9; confira 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 simultâneas, pois isso pode 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 runtime usando o DbContext.Database.MigrateAsync() método. É recomendável 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, quando usar projetos .NET Aspire).
Para obter mais informações, consulte Bloqueio de migração.
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 em uma transação
A maioria das operações executadas 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 em SQL Server tabelas com otimização de memória ou operações de alteração de banco de dados, como modificar a ordenação de banco de dados). Para evitar corromper o banco de dados em caso de falha de migração, é recomendável que essas operações sejam executadas isoladamente usando uma migração separada. O EF9 agora detecta um cenário em que 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.
Semeadura de dados aprimorada
O EF9 introduziu uma maneira prática de executar a propagação de dados, que é preencher o banco de dados com dados iniciais. DbContextOptionsBuilder agora contém os métodos UseSeeding e UseAsyncSeeding 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á poderá conter os dados de exemplo (que teriam sido adicionados na primeira inicialização do contexto). Desse modo, UseSeedingUseAsyncSeeding deve verificar se existem dados antes de tentar preencher o banco de dados. Isso pode ser feito emitindo uma consulta EF simples.
Confira 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 de migração
- Ao alterar uma tabela existente em uma tabela temporal SQL Server, o tamanho do código de migração foi significativamente reduzido.
Criação de modelo
Modelos compilados automaticamente
Dica
O código mostrado aqui vem do exemplo 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 é fornecido com um pacote de tarefas do MSBuild que pode atualizar automaticamente o modelo compilado quando o projeto de modelo é criado! Para começar, instale o Microsoft. EntityFrameworkCore.Tasks pacote NuGet. Por exemplo:
dotnet add package Microsoft.EntityFrameworkCore.Tasks --version 9.0.0
Dica
Use a versão do pacote no comando acima que corresponda à versão do EF Core que você está usando.
Em seguida, ative a integração definindo as propriedades EFOptimizeContext e EFScaffoldModelStage no seu arquivo .csproj. Por exemplo:
<PropertyGroup>
<EFOptimizeContext>true</EFOptimizeContext>
<EFScaffoldModelStage>build</EFScaffoldModelStage>
</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
--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.
Para obter mais informações, consulte a integração do MSBuild.
Coleções primitivas 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 de 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 suportada por uma coleção normal e mutável, se necessário. 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 oferece suporte à especificação do fator de preenchimento do SQL Server ao usar migrações do EF Core para criar chaves e índices. Na documentação 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 no 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 composto de chaves e índices primários e alternativos usando HasFillFactor. 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 que o fator de preenchimento respeite 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);
Este aprimoramento foi uma contribuição de @deano-hunter. 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 isso falha e também habilitamos a instanciação por 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 co-loca a configuração junto ao tipo de entidade. Não vamos discutir isso aqui. :-)
SQL Server HierarchyId (Identificador de Hierarquia)
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 SQL Server HierarchyId foi adicionado ao 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, pode-se usar um subnível adicional. 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.
Esse aprimoramento foi contribuído por @Rezakazemi890. Muito obrigado!
Ferramentas
Menos recompilações
A dotnet ef ferramenta de linha de comando , por padrão, cria seu projeto antes de executar a ferramenta. Isso ocorre porque não recompilar a ferramenta antes de executá-la é uma causa 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 reconstruído na próxima vez que for construído fora das ferramentas do EF.
Acreditamos que uma contribuição da comunidade de @Suchiman corrigiu 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.