Entity Framework Core: Otimização de Performance e Boas Práticas em 2026
Guia completo de otimização de performance com Entity Framework Core 10 no .NET 10. AsNoTracking, queries compiladas, atualizações em lote, split queries e LeftJoin.

Entity Framework Core 10, lançado com o .NET 10 LTS em novembro de 2025, apresenta operadores LeftJoin, busca vetorial, filtros de consulta nomeados e melhorias significativas na tradução SQL. Este guia aborda as boas práticas do EF Core que impactam diretamente a velocidade das queries, a alocação de memória e a escalabilidade em produção.
EF Core 10 é uma versão Long-Term Support, com suporte até novembro de 2028. Ele requer o SDK e o runtime do .NET 10. Aplicações que ainda utilizam .NET 8 devem apontar para o EF Core 8 (LTS) até a migração ser concluída.
Rastreamento de Consultas: Quando Desativar e Por Quê
Cada chamada ao DbSet<T> anexa as entidades retornadas ao change tracker por padrão. O tracker mantém um snapshot dos valores originais das propriedades, calcula as diferenças no SaveChanges e resolve conflitos de identidade entre as navegações. Essa sobrecarga é desnecessária quando os dados seguem diretamente para uma resposta de API ou para um view model somente leitura.
public async Task<List<ProductDto>> GetActiveByCategoryAsync(
int categoryId, CancellationToken ct)
{
return await _context.Products
.AsNoTracking() // ignorar completamente o change tracker
.Where(p => p.CategoryId == categoryId && p.IsActive)
.OrderBy(p => p.Name)
.Select(p => new ProductDto // projetar para DTO no nível do banco de dados
{
Id = p.Id,
Name = p.Name,
Price = p.Price
})
.ToListAsync(ct);
}AsNoTracking remove a sobrecarga por entidade da criação de snapshots e da resolução de identidade. Combinado com Select, apenas as colunas necessárias trafegam pela rede. Em uma tabela com 50.000 registros, esse padrão tipicamente reduz as alocações de memória entre 40 e 60% em comparação com uma query completa com rastreamento.
Para contextos que nunca modificam dados, o comportamento padrão pode ser definido durante o registro:
builder.Services.AddDbContext<CatalogContext>(options =>
options.UseSqlServer(connectionString)
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));Split Queries para Evitar a Explosão Cartesiana
Carregar uma entidade com múltiplas navegações de coleção via Include gera uma única instrução SQL com JOINs. Quando duas ou mais coleções são carregadas simultaneamente, o conjunto de resultados cresce como um produto cartesiano das coleções, duplicando os dados da linha pai em cada combinação.
public async Task<Order?> GetWithDetailsAsync(int orderId, CancellationToken ct)
{
return await _context.Orders
.AsSplitQuery() // um SQL por Include
.Include(o => o.Items)
.ThenInclude(i => i.Product)
.Include(o => o.Payments)
.Include(o => o.ShippingEvents)
.FirstOrDefaultAsync(o => o.Id == orderId, ct);
}AsSplitQuery decompõe o carregamento em instruções SQL separadas por navegação. O trade-off: múltiplas viagens de ida e volta em vez de uma, mas cada conjunto de resultados permanece pequeno e evita o problema de duplicação de linhas. O EF Core 10 também corrige uma inconsistência de longa data na ordenação das split queries, garantindo que a ordenação da subconsulta corresponda à da query principal.
Queries únicas continuam preferíveis ao carregar uma única navegação de coleção ou quando a latência das viagens de ida e volta é alta (chamadas de banco de dados entre regiões). Recomenda-se fazer benchmark de ambos os modos para o padrão de acesso específico antes de tomar uma decisão.
Operações em Lote com ExecuteUpdate e ExecuteDelete
O fluxo de trabalho tradicional do EF carrega as entidades, modifica as propriedades e então chama SaveChanges. Para operações em massa que afetam milhares de linhas, isso cria milhares de instâncias rastreadas e instruções UPDATE individuais. O EF Core 7 introduziu ExecuteUpdateAsync e ExecuteDeleteAsync para enviar a operação como um único comando SQL. O EF Core 10 simplifica ainda mais a API ao aceitar uma lambda simples em vez de uma árvore de expressões.
public async Task ApplySeasonalDiscountAsync(
int categoryId, decimal discountPercent, CancellationToken ct)
{
var affected = await _context.Products
.Where(p => p.CategoryId == categoryId && p.IsActive)
.ExecuteUpdateAsync(s =>
{
s.SetProperty(p => p.Price, p => p.Price * (1 - discountPercent / 100));
s.SetProperty(p => p.LastModified, DateTime.UtcNow);
}, ct);
// affected = número de linhas atualizadas
}Isso se traduz em uma única instrução UPDATE ... SET ... WHERE. Nenhuma entidade é carregada na memória. Para uma atualização de 10.000 linhas, o tempo de execução cai de vários segundos (abordagem com rastreamento) para milissegundos.
O mesmo padrão se aplica às exclusões:
public async Task PurgeExpiredSessionsAsync(CancellationToken ct)
{
await _context.Sessions
.Where(s => s.ExpiresAt < DateTime.UtcNow)
.ExecuteDeleteAsync(ct);
}Pronto para mandar bem nas entrevistas de .NET?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
O Operador LeftJoin no EF Core 10
Antes do EF Core 10, realizar um LEFT JOIN exigia combinar GroupJoin, SelectMany e DefaultIfEmpty em um padrão específico que a maioria dos desenvolvedores precisava pesquisar toda vez. O EF Core 10 adiciona LeftJoin como um operador LINQ de primeira classe.
public async Task<List<EmployeeReportDto>> GetEmployeeDepartmentReportAsync(
CancellationToken ct)
{
return await _context.Employees
.LeftJoin(
_context.Departments,
employee => employee.DepartmentId,
department => department.Id,
(employee, department) => new EmployeeReportDto
{
FullName = employee.FirstName + " " + employee.LastName,
Department = department.Name ?? "Unassigned",
HiredAt = employee.HiredAt
})
.OrderBy(r => r.FullName)
.ToListAsync(ct);
}O SQL gerado utiliza uma cláusula LEFT JOIN padrão. O operador RightJoin também está disponível. Ambos os operadores eliminam a cadeia verbosa dos três métodos que eram anteriormente necessários.
Filtros de Consulta Nomeados para Filtragem de Múltiplas Preocupações
Os filtros de consulta globais têm sido limitados a um único filtro por tipo de entidade desde o EF Core 2.0. Aplicações que implementam tanto exclusão lógica quanto multi-tenancy tinham que combinar as condições em uma única expressão e não podiam desativá-las seletivamente. O EF Core 10 introduz os filtros de consulta nomeados.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Invoice>()
.HasQueryFilter("SoftDelete", i => !i.IsDeleted)
.HasQueryFilter("Tenant", i => i.TenantId == _tenantId);
}Endpoints administrativos podem desativar o filtro de exclusão lógica enquanto mantêm o isolamento de tenants ativo:
public async Task<List<Invoice>> GetAllIncludingDeletedAsync(CancellationToken ct)
{
return await _context.Invoices
.IgnoreQueryFilters(["SoftDelete"]) // o filtro de tenant continua aplicado
.ToListAsync(ct);
}Isso elimina a necessidade de métodos de extensão IQueryable customizados que adicionavam manualmente as condições de filtragem.
Resiliência de Conexões e Configuração do Pool
Falhas transitórias de banco de dados (interrupções de rede, failovers do Azure SQL, esgotamento do pool de conexões) causam exceções que frequentemente derrubam os pipelines de requisições. O EF Core fornece lógica de retry integrada, mas os valores padrão requerem configuração explícita.
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString, sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(10),
errorNumbersToAdd: null); // retry em todos os erros transitórios
sqlOptions.CommandTimeout(30); // timeout de comando de 30 segundos
}));Os parâmetros de pooling da string de conexão também são importantes:
Server=db.example.com;Database=AppDb;Min Pool Size=5;Max Pool Size=100;Connection Timeout=15;Min Pool Size mantém conexões prontas para picos de tráfego. Max Pool Size limita o total de conexões abertas para prevenir sobrecarga no banco de dados. O valor padrão de 100 funciona para a maioria das aplicações web, mas serviços de alta demanda podem precisar de ajustes baseados no volume real de queries concorrentes.
Retries automáticos não funcionam dentro de transações iniciadas pelo usuário. A transação inteira deve ser envolvida em uma estratégia de retry manual usando context.Database.CreateExecutionStrategy().ExecuteAsync(...) para lidar com falhas transitórias em múltiplas operações.
Estratégia de Indexação e Análise de Queries
As migrations do EF Core permitem definir índices de forma declarativa, mas escolher quais colunas indexar requer entendimento dos padrões de consulta.
modelBuilder.Entity<Order>(entity =>
{
// índice composto para padrão de consulta frequente
entity.HasIndex(o => new { o.CustomerId, o.Status, o.CreatedAt })
.HasDatabaseName("IX_Order_Customer_Status_Date");
// índice filtrado apenas para pedidos ativos
entity.HasIndex(o => o.Status)
.HasFilter("[Status] <> 'Cancelled'")
.HasDatabaseName("IX_Order_ActiveStatus");
});Índices filtrados reduzem o tamanho do índice ao excluir linhas que as queries nunca buscam. Para uma tabela com 80% de pedidos cancelados, um índice filtrado sobre status ativos pode ser 5 vezes menor e mais rápido de percorrer.
Para analisar o SQL gerado, basta habilitar o logging em desenvolvimento:
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString)
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging());Recomenda-se copiar o SQL logado no SQL Server Management Studio e executá-lo com SET STATISTICS IO ON para verificar as leituras lógicas, ou usar EXPLAIN ANALYZE no PostgreSQL. As sugestões de índices ausentes do plano de consulta frequentemente revelam as oportunidades de otimização de maior impacto.
Queries Compiladas para Caminhos Críticos
As árvores de expressão LINQ são analisadas e traduzidas para SQL a cada execução. Para queries executadas milhares de vezes por minuto (buscas de autenticação, validação de sessões), esse custo de tradução se acumula. Queries compiladas armazenam em cache a tradução na inicialização da aplicação.
public static class UserQueries
{
// compilada uma vez, reutilizada em cada chamada
public static readonly Func<AppDbContext, string, CancellationToken, Task<UserSession?>>
GetActiveSession = EF.CompileAsyncQuery(
(AppDbContext ctx, string token, CancellationToken ct) =>
ctx.UserSessions
.AsNoTracking()
.FirstOrDefault(s => s.Token == token && s.ExpiresAt > DateTime.UtcNow));
}
// Uso no middleware
var session = await UserQueries.GetActiveSession(dbContext, bearerToken, ct);Queries compiladas pulam inteiramente a fase de análise da árvore de expressões. A diferença de performance só é mensurável em caminhos de alta frequência (mais de 1.000 chamadas/minuto). Para endpoints CRUD padrão, o cache de queries integrado do EF Core já cuida da reutilização da tradução.
Tradução de Coleções Parametrizadas no EF Core 10
Queries que filtram por uma lista de IDs representam um dos padrões mais comuns no acesso a dados. O EF Core 10 altera a estratégia de tradução padrão para coleções parametrizadas. Em vez de codificar a lista como um array JSON (EF Core 8-9) ou inserir constantes inline (EF Core 7 e anteriores), o EF Core 10 traduz cada valor como um parâmetro SQL separado.
// Antes (EF Core 8-9): parâmetro de array JSON
// @__ids_0='[1,2,3]'
// SELECT ... WHERE Id IN (SELECT value FROM OPENJSON(@__ids_0))
// EF Core 10: parâmetros individuais com padding
// SELECT ... WHERE Id IN (@ids1, @ids2, @ids3)
var orderIds = new[] { 101, 205, 389 };
var orders = await _context.Orders
.Where(o => orderIds.Contains(o.Id))
.ToListAsync(ct);A nova abordagem fornece ao planejador de queries do banco de dados informações de cardinalidade enquanto evita a saturação do cache de planos através do padding de parâmetros. Para casos onde a abordagem JSON tem melhor performance (coleções muito grandes), o comportamento pode ser sobrescrito por query:
var orders = await _context.Orders
.Where(o => EF.Parameter(orderIds).Contains(o.Id)) // forçar modo JSON
.ToListAsync(ct);Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
Conclusão
- Usar
AsNoTracking()e projeçõesSelectem toda query somente leitura para eliminar a sobrecarga do change tracker - Aplicar
AsSplitQuery()ao carregar múltiplas navegações de coleção para evitar a explosão cartesiana - Substituir os padrões de carregar-modificar-salvar com rastreamento por
ExecuteUpdateAsynceExecuteDeleteAsyncpara operações em massa - Adotar o operador
LeftJoindo EF Core 10 para substituir a cadeia verbosaGroupJoin/SelectMany/DefaultIfEmpty - Configurar os filtros de consulta nomeados para gerenciar exclusão lógica e multi-tenancy de forma independente
- Configurar
EnableRetryOnFailuree ajustar os tamanhos do pool de conexões para resiliência em produção - Definir índices compostos e filtrados baseados em padrões de consulta reais, não em suposições
- Reservar queries compiladas para caminhos genuinamente críticos que excedam 1.000 execuções por minuto
- Permitir que o EF Core 10 gerencie a tradução de coleções parametrizadas por padrão, e sobrescrever apenas quando os benchmarks justificarem
Para aprofundamento: o módulo avançado de EF Core cobre esses padrões em contexto de entrevista técnica, e o guia de Clean Architecture com .NET demonstra como estruturar as camadas de repositório e serviço que encapsulam essas queries.
Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
Tags
Compartilhar
Artigos relacionados

Perguntas de Entrevista C# e .NET: Guia Completo 2026
As 25 perguntas mais comuns em entrevistas de C# e .NET. LINQ, async/await, injeção de dependência, Entity Framework e boas práticas com respostas detalhadas.

.NET 8: Construindo uma API REST com ASP.NET Core
Guia completo para construir uma API REST profissional com .NET 8 e ASP.NET Core. Controllers, Entity Framework Core, validacao e boas praticas explicadas passo a passo.

Rust: Fundamentos para Desenvolvedores Experientes em 2026
Aprenda Rust aproveitando sua experiencia previa. Ownership, borrowing, lifetimes e padroes essenciais explicados para quem vem de C++, Java ou Python.