DEV Community

Cover image for O erro do scoped em singleton: por que injetar um DbContext num BackgroundService quebra tudo
Paulo Walraven
Paulo Walraven

Posted on

O erro do scoped em singleton: por que injetar um DbContext num BackgroundService quebra tudo

Você cria um BackgroundService, injeta um DbContext no construtor, roda — e por um tempo parece funcionar. Até começarem os bugs que ninguém consegue reproduzir: estado obsoleto, conexões descartadas, dados que não batem. Esse é, talvez, o erro mais comum ao construir hosted services no .NET.

Ao final deste post você vai entender por que esse padrão quebra os tempos de vida (lifetimes) das suas dependências e qual é o jeito correto de acessar serviços scoped dentro de um hosted service.

O conflito de tempos de vida

Aqui está o detalhe que muita gente não percebe: hosted services são registrados como Singletons. Uma única instância é criada para toda a vida da aplicação.

Só que muitos serviços comuns no ASP.NET são scoped — uma instância por escopo (tipicamente, por requisição web). Os exemplos clássicos:

  • DbContext (Entity Framework)
  • Repositórios
  • Serviços de unit of work

Quando você registra um DbContext, isso acontece de forma padrão:

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
Enter fullscreen mode Exit fullscreen mode

AddDbContext registra o AppDbContext como scoped. Esse é o comportamento padrão do EF Core, e é o correto para o ciclo de uma requisição.

Por que injetar direto quebra tudo

Um hosted service vive durante toda a vida da aplicação. Um serviço scoped foi feito para viver apenas dentro de um escopo específico — por exemplo, uma requisição web.

Então, se você injetar a dependência scoped diretamente no construtor do hosted service, você viola esse limite de tempo de vida:

public class JobProcessor : BackgroundService
{
    private readonly AppDbContext _dbContext; // ⚠️ scoped dentro de um singleton

    public JobProcessor(AppDbContext dbContext) // isto é o que NÃO queremos
    {
        _dbContext = dbContext;
    }
    // ...
}
Enter fullscreen mode Exit fullscreen mode

As consequências de quebrar essa fronteira:

  • Tempos de vida incorretos de serviço
  • Dependências descartadas sendo usadas
  • Estado obsoleto preso em memória pela vida inteira da aplicação
  • Comportamento imprevisível geral

E imprevisibilidade é a última coisa que você quer quando o assunto é falar com o banco de dados.

O padrão correto: IServiceScopeFactory

Em vez de injetar a dependência scoped, você injeta um IServiceScopeFactory e cria um escopo sempre que precisar resolver serviços scoped.

A fábrica de escopos foi projetada exatamente para isso: te entregar um escopo que você controla, dentro do seu hosted service.

public class JobProcessor : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public JobProcessor(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            Console.WriteLine("Verificando jobs...");

            // 1. cria um escopo (com using, para ser descartado depois)
            using var scope = _scopeFactory.CreateScope();

            // 2. resolve o DbContext dentro desse escopo
            var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();

            // 3. usa normalmente
            await ProcessJob(dbContext);

            await Task.Delay(1000, stoppingToken);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Os três pontos que fazem isso funcionar:

  1. CreateScope() dentro do loop cria um IServiceScope novo a cada iteração.
  2. A declaração using garante que o escopo (e tudo o que ele resolveu) seja descartado ao final.
  3. GetRequiredService<AppDbContext>() resolve o DbContext a partir do ServiceProvider daquele escopo — respeitando o lifetime scoped.

Loop de hosted service criando e descartando um escopo por iteração para resolver o DbContext

A ideia central: você cria o escopo para o DbContext porque o DbContext é scoped, mas o lugar onde o processamento acontece roda como singleton. O escopo é a ponte segura entre os dois mundos.

O aprendizado que evita horas de debug

Guarde essa frase: um hosted service é um singleton. Então, sempre que precisar de uma dependência scoped — como um DbContext — você deve criar um escopo primeiro.

Fazer isso garante duas coisas críticas:

  • As dependências scoped são criadas corretamente a cada uso.
  • E, igualmente importante, são descartadas corretamente ao final — sem conexões vazando nem estado preso por toda a vida da aplicação.

Conclusão

Injetar um DbContext direto no construtor de um BackgroundService é o tipo de erro que não estoura na hora — ele apodrece silenciosamente até virar um bug intermitente em produção. A correção é barata: injete IServiceScopeFactory, crie um escopo com using dentro do loop e resolva suas dependências scoped a partir dele.

Abra seu hosted service agora e confira: tem algum serviço scoped no construtor? Se tiver, você já sabe o que refatorar. No próximo post, vamos falar sobre outro pilar de hosted services em produção: graceful shutdown e o CancellationToken que você não pode ignorar.

Top comments (0)