O que há de novo no EF Core 9: os recursos que você precisa conhecer
O Entity Framework Core 9 foi lançado junto com o .NET 9 em novembro de 2024 e, depois de passar muito tempo trabalhando com ele em vários projetos, posso dizer que é um dos lançamentos mais significativos dos últimos tempos. Não porque reinventa a roda, mas porque aperfeiçoa as áreas onde o EF Core tem causado historicamente mais atrito: tradução de consultas, desempenho e trabalho com padrões de dados modernos.
Nesta postagem, examinarei os recursos que tiveram maior impacto no meu trabalho diário. Se você ainda estiver no EF Core 8 (ou mesmo 7), isso deverá lhe dar uma imagem clara do que o espera do outro lado da atualização.
EF Core 9 no ecossistema .NET 9
O EF Core 9 é direcionado ao .NET 8 e .NET 9, o que significa que você não precisa necessariamente atualizar todo o seu aplicativo para o .NET 9 para aproveitar a maioria desses recursos. Dito isso, algumas das melhorias de AOT e de desempenho estão intimamente associadas às alterações de tempo de execução do .NET 9, portanto, você aproveitará ao máximo percorrendo todo o caminho.
O lançamento segue a cadência ímpar/par estabelecida pela Microsoft: versões ímpares (como .NET 9) são Standard Term Support (STS) com 18 meses de suporte, enquanto versões pares (como .NET 8) são Long Term Support (LTS). Tenha isso em mente ao planejar seu cronograma de atualização.
Melhorias na tradução do LINQ
É aqui que a maioria dos desenvolvedores sentirá a diferença imediatamente. O EF Core 9 fez avanços significativos na tradução de expressões LINQ para SQL que realmente fazem sentido.
Melhor GroupBy Traduções
Se você já escreveu uma consulta GroupBy no EF Core e acabou com avisos de avaliação do lado do cliente ou SQL bizarro, você conhece o problema. O EF Core 9 lida com um conjunto muito mais amplo de cenários GroupBy diretamente no SQL.
var salesByCategory = await context.Products
.GroupBy(p => p.Category.Name)
.Select(g => new
{
Category = g.Key,
TotalRevenue = g.Sum(p => p.Price * p.UnitsSold),
AveragePrice = g.Average(p => p.Price),
ProductCount = g.Count()
})
.OrderByDescending(x => x.TotalRevenue)
.ToListAsync();
Nas versões anteriores, as consultas envolvendo agregações sobre propriedades de navegação dentro de GroupBy às vezes recorriam à avaliação do cliente. O EF Core 9 traduz isso de forma limpa para uma única consulta SQL com GROUP BY, SUM, AVG e COUNT.
Projeções e subconsultas complexas
Subconsultas aninhadas e projeções complexas também receberam uma grande atualização. Considere algo assim:
var orderSummaries = await context.Customers
.Select(c => new CustomerSummaryDto
{
Name = c.FullName,
TotalOrders = c.Orders.Count(),
MostRecentOrder = c.Orders
.OrderByDescending(o => o.OrderDate)
.Select(o => new OrderBriefDto
{
Id = o.Id,
Date = o.OrderDate,
Total = o.LineItems.Sum(li => li.Quantity * li.UnitPrice)
})
.FirstOrDefault(),
TopCategory = c.Orders
.SelectMany(o => o.LineItems)
.GroupBy(li => li.Product.Category.Name)
.OrderByDescending(g => g.Count())
.Select(g => g.Key)
.FirstOrDefault()
})
.ToListAsync();
O EF Core 9 agora pode traduzir toda essa expressão em SQL sem acionar a avaliação do lado do cliente. A consulta gerada usa subconsultas correlacionadas e junções laterais quando apropriado, e o plano SQL é consideravelmente mais eficiente do que as versões anteriores produziriam.
Coleções Primitivas Parametrizadas
Uma das melhorias mais destacadas do LINQ é a capacidade de passar coleções de valores primitivos diretamente para consultas:
var statusFilter = new List<string> { "Active", "Pending", "Review" };
var filteredOrders = await context.Orders
.Where(o => statusFilter.Contains(o.Status))
.ToListAsync();
No EF Core 8, isso foi traduzido usando cláusulas IN com valores embutidos, o que significava que o cache do plano de consulta não poderia ser reutilizado quando a lista fosse alterada. O EF Core 9 parametriza essas coleções adequadamente, enviando-as como um parâmetro estruturado. Isso é importante para o cache do plano de consulta no SQL Server e no PostgreSQL.## Operações em massa — ExecuteUpdate e ExecuteDelete
ExecuteUpdate e ExecuteDelete foram introduzidos no EF Core 7, mas o EF Core 9 expande o que você pode fazer com eles de maneiras significativas.
Expressões de atualização mais complexas
Agora você pode usar expressões mais complexas em ExecuteUpdate, incluindo referências a outras tabelas através de propriedades de navegação:
await context.Products
.Where(p => p.Category.IsDiscontinued)
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.IsAvailable, false)
.SetProperty(p => p.DiscontinuedDate, DateTimeOffset.UtcNow)
.SetProperty(p => p.Price, p => p.Price * 0.5m));
Isso gera uma única instrução UPDATE com um JOIN para a tabela de categorias — sem necessidade de carregar entidades na memória, sem sobrecarga de controle de alterações.
Exclusões condicionais em massa com subconsultas
Exclusões em massa com filtros de subconsulta agora são totalmente suportadas:
await context.AuditLogs
.Where(log => log.CreatedAt < DateTime.UtcNow.AddYears(-2))
.Where(log => !context.ProtectedRecords
.Any(pr => pr.AuditLogId == log.Id))
.ExecuteDeleteAsync();
Isso se traduz em uma DELETE com uma subconsulta NOT EXISTS, exatamente o que você escreveria à mão. Nenhuma entidade carregada, sem viagens de ida e volta.
Melhorias na coluna JSON
As colunas JSON têm sido um dos recursos mais interessantes nas versões recentes do EF Core, e o EF Core 9 as leva ainda mais longe.
Consultando dentro do JSON
Agora você pode filtrar e projetar dados em colunas JSON com melhor suporte de tradução:
public class Order
{
public int Id { get; set; }
public string CustomerName { get; set; }
public ShippingAddress Address { get; set; } // Stored as JSON
public List<OrderNote> Notes { get; set; } // Stored as JSON
}
public class ShippingAddress
{
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
public string Country { get; set; }
}
public class OrderNote
{
public DateTime CreatedAt { get; set; }
public string Text { get; set; }
public string Author { get; set; }
}
Configuração em seu DbContext:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>(builder =>
{
builder.OwnsOne(o => o.Address, ab =>
{
ab.ToJson();
});
builder.OwnsMany(o => o.Notes, nb =>
{
nb.ToJson();
});
});
}
Agora você pode consultar diretamente no JSON:
var newYorkOrders = await context.Orders
.Where(o => o.Address.State == "NY")
.OrderByDescending(o => o.Notes.Count)
.Select(o => new
{
o.CustomerName,
o.Address.City,
LatestNote = o.Notes
.OrderByDescending(n => n.CreatedAt)
.Select(n => n.Text)
.FirstOrDefault()
})
.ToListAsync();
O EF Core 9 gera chamadas JSON_VALUE e JSON_QUERY adequadas no SQL Server (ou equivalente em outros provedores), e a tradução cobre uma gama muito mais ampla de operações LINQ em elementos JSON do que antes.
Atualizando propriedades JSON
Um dos pontos de atrito no EF Core 8 era que a atualização de uma única propriedade dentro de uma coluna JSON faria com que todo o documento JSON fosse reescrito. O EF Core 9 melhora isso com um rastreamento de alterações mais granular para tipos mapeados em JSON, gerando atualizações mais direcionadas quando possível.
var order = await context.Orders.FindAsync(orderId);
order.Address.ZipCode = "10001";
await context.SaveChangesAsync();
Em fornecedores suportados, isto pode gerar uma modificação JSON mais direcionada em vez de reescrever todo o blob.
Tipos Complexos — Objetos de Valor Sem Identidade
Tipos complexos são um dos recursos que os profissionais de Design Orientado a Domínio estavam esperando. Ao contrário dos tipos próprios, os tipos complexos não têm identidade — são objetos de valor puro.
[ComplexType]
public record Money(decimal Amount, string Currency);
[ComplexType]
public record DateRange(DateTime Start, DateTime End);
public class Project
{
public int Id { get; set; }
public string Name { get; set; }
public Money Budget { get; set; }
public DateRange Timeline { get; set; }
}
Eles são armazenados como colunas niveladas na tabela pai — Budget_Amount, Budget_Currency, Timeline_Start, Timeline_End — sem exigir uma tabela separada ou qualquer tipo de chave.
A principal diferença em relação aos tipos próprios: os tipos complexos são comparados por valor, não por referência. Duas instâncias Money com os mesmos Amount e Currency são consideradas iguais, independentemente da entidade a que pertencem.
var expensiveProjects = await context.Projects
.Where(p => p.Budget.Amount > 100_000m && p.Budget.Currency == "USD")
.OrderByDescending(p => p.Budget.Amount)
.ToListAsync();
Isso se traduz diretamente na filtragem nas colunas niveladas – limpa, eficiente e exatamente o que você espera.
Suporte HierarchyId para SQL Server
Se você já trabalhou com dados hierárquicos no SQL Server — organogramas, árvores de categorias, sistemas de arquivos — você sabe que HierarchyId é o tipo integrado para isso. O EF Core 9 oferece suporte de primeira classe para isso.
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public string Title { get; set; }
public HierarchyId PathFromCeo { get; set; }
}
Agora você pode consultar relacionamentos hierárquicos diretamente:
var managerId = HierarchyId.Parse("/1/3/");
// All direct and indirect reports
var allReports = await context.Employees
.Where(e => e.PathFromCeo.IsDescendantOf(managerId))
.Where(e => e.PathFromCeo != managerId) // exclude the manager
.OrderBy(e => e.PathFromCeo)
.ToListAsync();
// Direct reports only (one level down)
var directReports = await context.Employees
.Where(e => e.PathFromCeo.GetAncestor(1) == managerId)
.ToListAsync();
// Get an employee's depth in the hierarchy
var employeesWithDepth = await context.Employees
.Select(e => new
{
e.Name,
e.Title,
Level = e.PathFromCeo.GetLevel()
})
.OrderBy(e => e.Level)
.ToListAsync();
```Tudo isso se traduz em métodos nativos `HierarchyId` do SQL Server. Se você estiver implementando estruturas em árvore com chaves estrangeiras autorreferenciadas e CTEs recursivas, esta é uma abordagem muito mais limpa.
## Modelos compilados e suporte AOT
Os desenvolvedores preocupados com o desempenho apreciarão o investimento contínuo em modelos compilados e suporte à compilação antecipada (AOT).
### Modelos compilados
Os modelos compilados pré-geram os metadados do modelo que o EF Core normalmente cria na inicialização. Para modelos grandes (pense em centenas de entidades), isso pode reduzir drasticamente o tempo de inicialização a frio.
```bash
dotnet ef dbcontext optimize --output-dir CompiledModels --namespace MyApp.CompiledModels
Então ligue:
builder.Services.AddDbContext<AppDbContext>(options =>
options
.UseSqlServer(connectionString)
.UseModel(MyApp.CompiledModels.AppDbContextModel.Instance));
No EF Core 9, os modelos compilados são mais completos — eles suportam mais recursos de mapeamento e geram resultados menores. Para um modelo com cerca de 400 entidades, o tempo de inicialização pode cair de vários segundos para quase instantâneo.
Progresso da compilação AOT
O suporte completo do AOT nativo para o EF Core ainda é um trabalho em andamento, mas o EF Core 9 faz avanços significativos. Muitos dos caminhos de código com muita reflexão foram refatorados para serem fáceis de cortar, e os modelos compilados são uma parte fundamental da história do AOT. Se você estiver visando cenários como Azure Functions ou microsserviços em que a inicialização a frio é importante, essas melhorias serão diretamente relevantes.
Atualizações do provedor Cosmos DB
O provedor Azure Cosmos DB continua a amadurecer com o EF Core 9. Algumas melhorias notáveis:
Manipulação de chave de partição
O provedor agora oferece suporte a chaves de partição hierárquicas e lida com filtros de chaves de partição de maneira mais inteligente:
public class TenantDocument
{
public string Id { get; set; }
public string TenantId { get; set; } // Partition key
public string Region { get; set; } // Sub-partition key
public string Content { get; set; }
}
// This query now correctly uses the partition key for routing
var docs = await context.TenantDocuments
.Where(d => d.TenantId == "tenant-42" && d.Region == "us-east")
.Where(d => d.Content.Contains("important"))
.ToListAsync();
Tradução aprimorada de LINQ para NoSQL
Mais operações LINQ agora são traduzidas para o dialeto SQL do Cosmos DB, incluindo melhor suporte para Contains, Any, operações de matriz aninhadas e funções matemáticas. As consultas que anteriormente dependiam da avaliação do cliente agora são tratadas no lado do servidor.
Suporte para pesquisa de vetores
O EF Core 9 introduz suporte antecipado para pesquisa de similaridade vetorial com o Cosmos DB, o que é útil se você estiver criando aplicativos que se integram a embeddings ou pesquisa orientada por IA:
var results = await context.Documents
.OrderBy(d => EF.Functions.VectorDistance(d.Embedding, queryVector))
.Take(10)
.ToListAsync();
Melhorias na migração
As migrações obtiveram algumas melhorias na qualidade de vida que tornam o trabalho com elas menos penoso em ambientes de equipe.
Tabelas Temporais em Migrações
As migrações agora lidam com a configuração da tabela temporal de maneira mais elegante, com suporte adequado para colunas de período e nomenclatura de tabela de histórico:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Employee>()
.ToTable("Employees", b => b.IsTemporal(t =>
{
t.HasPeriodStart("ValidFrom");
t.HasPeriodEnd("ValidTo");
t.UseHistoryTable("EmployeeHistory");
}));
}
Scripts Idempotentes
O comando Script-Migration (e seu equivalente CLI) produz scripts idempotentes melhores por padrão, com tratamento aprimorado de casos extremos em torno de alterações de esquema que dependem de dados existentes em determinados estados.
Pacotes de migração
Os pacotes de migração, que empacotam suas migrações em um executável independente para implantação, são mais confiáveis no EF Core 9, com melhores relatórios de erros e lógica de nova tentativa para falhas transitórias.
dotnet ef migrations bundle --self-contained -r linux-x64
Isso produz um binário que você pode executar em seu pipeline de CI/CD sem precisar do SDK do .NET instalado em seu destino de implantação.
Benchmarks de desempenhoAqui estão alguns benchmarks aproximados de meus próprios testes. Eles são de um projeto com cerca de 200 entidades, executado no SQL Server 2022, medido com BenchmarkDotNet. Seus números irão variar, mas as melhorias relativas devem ser semelhantes.
| Cenário | EF Núcleo 8 | EF Núcleo 9 | Melhoria |
|---|---|---|---|
| Construção de modelo (inicialização a frio) | 1.850m | 320m | ~5,8x mais rápido (compilado) |
| Consulta simples (entidade única por PK) | 0,42ms | 0,38ms | ~10% mais rápido |
| Consulta complexa (junções + agregação) | 3,1ms | 2,4ms | ~23% mais rápido |
| Atualização em massa (10 mil linhas) | 145m | 118m | ~19% mais rápido |
| Consulta de coluna JSON | 2,8ms | 1,9ms | ~32% mais rápido |
| Salvar alterações (100 entidades) | 48m | 41m | ~15% mais rápido |
A melhoria do modelo compilado é a mais dramática, mas as melhorias constantes em geral se somam — especialmente em cenários de alto rendimento, onde você executa milhares de consultas por segundo.
Atualizando do EF Core 8
Se você estiver no EF Core 8, o caminho de atualização será relativamente tranquilo. Aqui está uma lista de verificação:
1. Atualize seus pacotes:
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0" />
2. Verifique se há alterações recentes. A lista do EF Core 9 é relativamente curta em comparação com algumas versões anteriores. Os mais notáveis:
- Algumas APIs anteriormente obsoletas foram removidas
- Mudanças na forma como certas consultas
GroupBysão traduzidas (elas agora vão para o lado do servidor, o que muda o comportamento se você estiver confiando na avaliação do cliente) - Pequenas alterações na saída do andaime de migração
3. Gere novamente modelos compilados se você os estiver usando. O formato mudou, então modelos compilados antigos não funcionarão com o EF Core 9.
4. Execute seu conjunto de testes. Preste atenção especial às consultas que foram avaliadas anteriormente no cliente — elas agora podem ser avaliadas no servidor, o que geralmente é melhor, mas pode revelar diferenças de dados.
5. Verifique suas consultas do Cosmos DB se você estiver usando esse provedor. As traduções aprimoradas significam que algumas consultas serão executadas de maneira diferente (geralmente mais rápidas), mas vale a pena verificar se os resultados são idênticos.
Uma atualização mínima para um projeto típico é assim:
dotnet add package Microsoft.EntityFrameworkCore --version 9.0.0
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 9.0.0
dotnet add package Microsoft.EntityFrameworkCore.Design --version 9.0.0
dotnet build
dotnet test
Se tudo for compilado e os testes passarem, você provavelmente está em boa forma. Se você encontrar problemas, a documentação de alterações significativas do EF Core 9 contém orientações detalhadas de migração para cada alteração.
Concluindo
O EF Core 9 não é uma versão revolucionária – é uma versão evolutiva, e era exatamente isso que precisava ser. As melhorias do LINQ por si só justificam a atualização para a maioria dos projetos, e recursos como aprimoramentos de colunas JSON, tipos complexos e HierarchyId suportam padrões de abertura que antes eram estranhos ou impossíveis.
Se eu tivesse que escolher os três recursos que tiveram maior impacto em meus projetos:
- Coleções primitivas parametrizadas — porque a eficiência do cache do plano de consulta é importante em escala
- Melhorias na coluna JSON — porque o padrão de documento relacional híbrido é incrivelmente útil
- Modelos compilados — porque o tempo de inicialização afeta diretamente a produtividade do desenvolvedor e a velocidade de implantaçãoA equipe do EF Core está em uma trajetória sólida desde o EF Core 5, e a versão 9 continua essa tendência. Se você já usa o EF Core 8, a atualização é de baixo risco e alta recompensa. Se você está em algo mais antigo, nunca houve melhor momento para dar o salto.
Boa codificação – e boas consultas.