Tempo de Vida do DbContext no ASP.NET Core: Performance vs Segurança de Threads em Operações Async
Domine o gerenciamento do tempo de vida do DbContext no ASP.NET Core. Aprenda quando usar scoped vs transient, como lidar com operações async de forma segura e otimizar performance com pooling de DbContext.

O gerenciamento do tempo de vida do DbContext é uma das fontes mais comuns de bugs em aplicações ASP.NET Core. O DbContext do Entity Framework Core não é thread-safe, e seu uso inadequado em operações assíncronas causa estados corrompidos, condições de corrida e exceções difíceis de reproduzir.
Uma única instância de DbContext não pode ser usada simultaneamente em múltiplas threads. Se um método async não for aguardado antes que outra operação comece no mesmo contexto, o estado interno se corrompe. Esta regra se aplica a todas as versões do EF Core, incluindo o EF Core 9.
Por Que o Tempo de Vida Scoped Funciona para Requisições Web
A injeção de dependências do ASP.NET Core registra o DbContext com tempo de vida scoped por padrão ao usar AddDbContext<T>(). Cada requisição HTTP recebe sua própria instância de DbContext, e essa instância é destruída quando a requisição termina.
Essa abordagem resolve dois problemas: garante que cada requisição tenha um estado de banco de dados isolado e alinha o tempo de vida do contexto com o padrão Unit of Work, onde as mudanças se acumulam durante o processamento da requisição e são confirmadas juntas no final.
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));O tempo de vida scoped é apropriado para fluxos padrão de requisição-resposta. Uma action de controller ou handler de Razor Page executa em uma única thread, e desde que todas as chamadas async sejam corretamente aguardadas, o DbContext permanece em um estado consistente durante toda a requisição.
public class OrderController : ControllerBase
{
private readonly AppDbContext _context;
public OrderController(AppDbContext context)
{
_context = context;
}
[HttpPost]
public async Task<IActionResult> CreateOrder(CreateOrderDto dto)
{
var order = new Order { CustomerId = dto.CustomerId, Total = dto.Total };
_context.Orders.Add(order);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
}
}A restrição chave: cada operação async deve ser completada antes que a próxima comece. O código acima está correto porque SaveChangesAsync é aguardado antes do método retornar.
O Problema de Thread Safety em Operações Paralelas
Os problemas surgem quando desenvolvedores tentam paralelizar operações de banco de dados dentro de uma mesma requisição. Considere este exemplo problemático:
// DEFEITUOSO: Não usar
public async Task<IActionResult> GetDashboard()
{
var ordersTask = _context.Orders.CountAsync();
var productsTask = _context.Products.CountAsync();
var customersTask = _context.Customers.CountAsync();
// Executando três consultas no mesmo DbContext simultaneamente
await Task.WhenAll(ordersTask, productsTask, customersTask);
return Ok(new { Orders = ordersTask.Result, Products = productsTask.Result, Customers = customersTask.Result });
}Este código inicia três consultas async sem aguardar cada uma individualmente. As três operações executam concorrentemente na mesma instância de DbContext, violando os requisitos de thread safety do EF Core. O resultado é imprevisível: às vezes funciona, às vezes lança InvalidOperationException, e às vezes retorna dados incorretos.
A documentação oficial da Microsoft afirma explicitamente que métodos async devem ser aguardados imediatamente. O change tracker interno, o estado de conexão e o cache de compilação de queries não foram projetados para acesso concorrente.
Pronto para mandar bem nas entrevistas de .NET?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
Usando IDbContextFactory para Consultas Paralelas
Quando operações de banco de dados paralelas são genuinamente necessárias, IDbContextFactory<T> fornece a solução. Esta factory cria novas instâncias de DbContext sob demanda, cada uma com seu próprio estado isolado.
builder.Services.AddDbContextFactory<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));Com a factory registrada, injete-a em vez do DbContext diretamente:
public class DashboardService
{
private readonly IDbContextFactory<AppDbContext> _contextFactory;
public DashboardService(IDbContextFactory<AppDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
public async Task<DashboardStats> GetStatsAsync()
{
// Cada task recebe sua própria instância de DbContext
var ordersTask = Task.Run(async () =>
{
await using var context = _contextFactory.CreateDbContext();
return await context.Orders.CountAsync();
});
var productsTask = Task.Run(async () =>
{
await using var context = _contextFactory.CreateDbContext();
return await context.Products.CountAsync();
});
var customersTask = Task.Run(async () =>
{
await using var context = _contextFactory.CreateDbContext();
return await context.Customers.CountAsync();
});
await Task.WhenAll(ordersTask, productsTask, customersTask);
return new DashboardStats
{
Orders = ordersTask.Result,
Products = productsTask.Result,
Customers = customersTask.Result
};
}
}Cada task cria, usa e destrói seu próprio contexto. A instrução await using garante a limpeza adequada mesmo se uma exceção ocorrer.
Pooling de DbContext para Aplicações de Alta Performance
Criar um novo DbContext envolve alocação de memória, inicialização do change tracker e configuração de caches internos. Para aplicações de alta performance processando milhares de requisições por segundo, essa sobrecarga se torna mensurável.
O pooling de DbContext resolve isso reutilizando instâncias de contexto. Quando um contexto do pool é destruído, o EF Core reseta seu estado e o retorna ao pool em vez de deixá-lo para o garbage collector.
builder.Services.AddDbContextPool<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")),
poolSize: 128);Os benchmarks de Dave Callan mostram que o pooling reduz o tempo de criação do contexto em mais de 90% em microbenchmarks. No entanto, o impacto real depende dos padrões de consulta da aplicação. Se as consultas de banco de dados dominam o tempo de processamento de requisições, a sobrecarga de criação do contexto é insignificante em comparação.
O pooling introduz uma restrição: o estado do DbContext é resetado ao retornar ao pool. Quaisquer campos ou propriedades customizados adicionados à classe DbContext perderão seus valores. O change tracker é limpo, então mudanças não confirmadas desaparecem.
Combinando Pooling com IDbContextFactory
Para aplicações que precisam tanto da performance do pooling quanto do suporte a consultas paralelas, usa-se AddPooledDbContextFactory:
builder.Services.AddPooledDbContextFactory<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")),
poolSize: 128);Este registro fornece IDbContextFactory<AppDbContext> onde os contextos vêm do pool. Cada chamada a CreateDbContext() recupera uma instância do pool, e destruí-la retorna a instância ao pool.
public class BatchProcessor
{
private readonly IDbContextFactory<AppDbContext> _contextFactory;
public BatchProcessor(IDbContextFactory<AppDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
public async Task ProcessBatchAsync(IEnumerable<OrderUpdate> updates)
{
// Processar em chunks paralelos com contextos do pool
var chunks = updates.Chunk(100);
var tasks = chunks.Select(async chunk =>
{
await using var context = _contextFactory.CreateDbContext();
foreach (var update in chunk)
{
var order = await context.Orders.FindAsync(update.OrderId);
if (order != null)
{
order.Status = update.NewStatus;
}
}
await context.SaveChangesAsync();
});
await Task.WhenAll(tasks);
}
}A análise de Milan Jovanovic fornece benchmarks adicionais mostrando a performance da factory com pool em cenários de processamento em lote.
Blazor Server: Um Caso Especial
Aplicações Blazor Server requerem cuidado extra com o tempo de vida do DbContext. Um circuito Blazor persiste através de múltiplas interações do usuário, diferente das requisições HTTP que têm limites claros.
O tempo de vida scoped padrão se torna problemático: uma única instância de DbContext vive durante toda a duração do circuito, potencialmente horas. Contextos de longa vida acumulam entidades rastreadas, aumentando o uso de memória e deixando as operações mais lentas.
builder.Services.AddDbContextFactory<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));Nos componentes Blazor, injete a factory e crie contextos de curta vida:
public partial class OrderList : ComponentBase
{
[Inject]
private IDbContextFactory<AppDbContext> ContextFactory { get; set; } = default!;
private List<Order> _orders = new();
protected override async Task OnInitializedAsync()
{
await using var context = ContextFactory.CreateDbContext();
_orders = await context.Orders
.AsNoTracking()
.OrderByDescending(o => o.CreatedAt)
.Take(50)
.ToListAsync();
}
}A chamada AsNoTracking() é particularmente importante no Blazor Server. Sem ela, cada entidade carregada permanece no change tracker, consumindo memória até o circuito terminar.
Serviços em Background e Hosted Services
Serviços em background registrados como singletons não podem injetar DbContext scoped diretamente. O serviço sobrevive a qualquer scope, criando erros de incompatibilidade de tempo de vida.
public class OrderProcessingService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
public OrderProcessingService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var pendingOrders = await context.Orders
.Where(o => o.Status == OrderStatus.Pending)
.Take(10)
.ToListAsync(stoppingToken);
foreach (var order in pendingOrders)
{
order.Status = OrderStatus.Processing;
}
await context.SaveChangesAsync(stoppingToken);
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
}O IServiceScopeFactory cria um novo scope para cada iteração. O scope, e o DbContext dentro dele, é destruído no final de cada iteração do loop.
Alternativamente, pode-se usar IDbContextFactory para um controle mais granular:
public class OrderProcessingService : BackgroundService
{
private readonly IDbContextFactory<AppDbContext> _contextFactory;
public OrderProcessingService(IDbContextFactory<AppDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await using var context = _contextFactory.CreateDbContext();
var pendingOrders = await context.Orders
.Where(o => o.Status == OrderStatus.Pending)
.Take(10)
.ToListAsync(stoppingToken);
foreach (var order in pendingOrders)
{
order.Status = OrderStatus.Processing;
}
await context.SaveChangesAsync(stoppingToken);
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
}Erros Comuns que Causam Violações de Thread Safety
Vários padrões consistentemente causam problemas de thread safety do DbContext em código de produção:
Armazenar DbContext em campos estáticos ou singletons: Um DbContext nunca deve sobreviver ao seu scope pretendido. Referências estáticas tornam a mesma instância acessível a partir de múltiplas threads.
Chamadas async fire-and-forget: Iniciar uma operação async sem aguardá-la enquanto continua usando o contexto na thread atual cria acesso concorrente.
// DEFEITUOSO: Não usar
public void UpdateAndNotify(int orderId)
{
var order = _context.Orders.Find(orderId);
order.Status = OrderStatus.Shipped;
_context.SaveChangesAsync(); // Não aguardado!
_notificationService.SendAsync(order.CustomerId); // O contexto pode ainda estar salvando
}Injetar DbContext em serviços singleton: O container de DI lança uma exceção em desenvolvimento, mas algumas configurações mascaram esse erro.
Usar DbContext através de múltiplos awaits sem entender o fluxo de execução: Cada await é um ponto de suspensão. Se o código retomado executa em uma thread diferente da esperada, acesso concorrente pode ocorrer com outros caminhos de código.
Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
Guia de Decisão para Configuração do Tempo de Vida do DbContext
Escolher a configuração correta de DbContext depende do tipo de aplicação e requisitos de performance:
- API web padrão ou aplicação MVC: Usar
AddDbContext<T>()com o tempo de vida scoped padrão. Isso cobre a maioria dos cenários corretamente. - API de alta performance (milhares de requisições/segundo): Usar
AddDbContextPool<T>()para reduzir a sobrecarga de alocação. - Aplicação que requer consultas de banco de dados paralelas: Usar
AddDbContextFactory<T>()ouAddPooledDbContextFactory<T>(). - Aplicação Blazor Server: Usar
AddDbContextFactory<T>()com contextos de curta vida criados por operação. - Serviços em background: Usar
IServiceScopeFactoryouIDbContextFactory<T>()para criar contextos dentro do serviço. - Processamento em lote com paralelismo: Usar
AddPooledDbContextFactory<T>()para performance ideal.
Para uma cobertura mais profunda de padrões async no ASP.NET Core, o módulo de programação async cobre conceitos relacionados. O módulo avançado de EF Core expande a otimização de queries e o comportamento do change tracking discutidos aqui.
Você saberia encontrar o bug em .NET?
Um trecho real, um bug escondido, uma tentativa por dia. Sem conta para testar.

Escrito por
Anthony Fillion-MailletFundador da SharpSkill
Desenvolvedor fullstack há mais de 10 anos. Dirige a SharpSkill e responde por tudo o que é publicado aqui.
Atualizado em 7 de setembro de 2026
Tags
Compartilhar
Artigos relacionados

LINQ Avançado em C# em 2026: Operadores, Performance e Perguntas de Entrevista
Dominar operadores LINQ avançados, otimizar a performance de consultas e se preparar para entrevistas técnicas C# com exemplos práticos.

.NET 9 Blazor: Desenvolvimento Full-Stack com Blazor United em 2026
.NET 9 Blazor United combina renderização estática SSR, Server e WebAssembly em um framework full-stack unificado. Tutorial prático cobrindo modos de renderização, streaming, injeção por construtor e padrões prontos para produção.

Entity Framework Core: Otimização de Performance e Boas Práticas em 2026
Domine a otimização de performance do EF Core 10 com AsNoTracking, queries compiladas, split queries, operações em lote e o novo operador LeftJoin. Exemplos práticos em C# para aplicações .NET 10 em produção.