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.

Tempo de vida do DbContext no ASP.NET Core

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.

DbContext não é thread-safe

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.

Program.cscsharp
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.

OrderController.cscsharp
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:

csharp
// 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.

Program.cscsharp
builder.Services.AddDbContextFactory<AppDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

Com a factory registrada, injete-a em vez do DbContext diretamente:

DashboardService.cscsharp
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.

Program.cscsharp
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:

Program.cscsharp
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.

BatchProcessor.cscsharp
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.

Program.cs para Blazor Servercsharp
builder.Services.AddDbContextFactory<AppDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

Nos componentes Blazor, injete a factory e crie contextos de curta vida:

OrderList.razor.cscsharp
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.

OrderProcessingService.cscsharp
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:

OrderProcessingService.cs (versão factory)csharp
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.

csharp
// 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>() ou AddPooledDbContextFactory<T>().
  • Aplicação Blazor Server: Usar AddDbContextFactory<T>() com contextos de curta vida criados por operação.
  • Serviços em background: Usar IServiceScopeFactory ou IDbContextFactory<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.

Desafio do dia

Você saberia encontrar o bug em .NET?

Um trecho real, um bug escondido, uma tentativa por dia. Sem conta para testar.

Anthony Fillion-Maillet

Escrito por

Anthony Fillion-Maillet

Fundador 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

#dotnet
#entity-framework
#aspnet-core
#async
#performance

Compartilhar

Artigos relacionados