# 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. - Published: 2026-04-15 - Updated: 2026-04-15 - Author: SharpSkill - Tags: dotnet, entity-framework-core, performance, best-practices - Reading time: 10 min --- 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 roda apenas no .NET 10** > > 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` 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. ```csharp // ProductRepository.cs public async Task> 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: ```csharp // Program.cs builder.Services.AddDbContext(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. ```csharp // OrderRepository.cs public async Task 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. > **Quando usar queries únicas** > > 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. ```csharp // PromotionService.cs 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: ```csharp // CleanupService.cs public async Task PurgeExpiredSessionsAsync(CancellationToken ct) { await _context.Sessions .Where(s => s.ExpiresAt < DateTime.UtcNow) .ExecuteDeleteAsync(ct); } ``` ## 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. ```csharp // ReportService.cs public async Task> 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. ```csharp // AppDbContext.cs protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .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: ```csharp // InvoiceRepository.cs public async Task> 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. ```csharp // Program.cs builder.Services.AddDbContext(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. > **Lógica de retry e transações** > > 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. ```csharp // AppDbContext.cs - OnModelCreating modelBuilder.Entity(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: ```csharp // Program.cs (apenas desenvolvimento) builder.Services.AddDbContext(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. ```csharp // CompiledQueries.cs public static class UserQueries { // compilada uma vez, reutilizada em cada chamada public static readonly Func> 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. ```csharp // 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: ```csharp var orders = await _context.Orders .Where(o => EF.Parameter(orderIds).Contains(o.Id)) // forçar modo JSON .ToListAsync(ct); ``` ## Conclusão - Usar `AsNoTracking()` e projeções `Select` em 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 `ExecuteUpdateAsync` e `ExecuteDeleteAsync` para operações em massa - Adotar o operador `LeftJoin` do EF Core 10 para substituir a cadeia verbosa `GroupJoin`/`SelectMany`/`DefaultIfEmpty` - Configurar os filtros de consulta nomeados para gerenciar exclusão lógica e multi-tenancy de forma independente - Configurar `EnableRetryOnFailure` e 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](/technologies/dotnet/interview-questions/ef-core-advanced) cobre esses padrões em contexto de entrevista técnica, e o [guia de Clean Architecture com .NET](/blog/dotnet/clean-architecture-dotnet-practical-guide) demonstra como estruturar as camadas de repositório e serviço que encapsulam essas queries. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pt/blog/dotnet/ef-core-performance-best-practices