DEV Community

Cover image for IServiceScopeFactory em BackgroundService: por que injetar serviços scoped diretamente quebra
Paulo Walraven
Paulo Walraven

Posted on

IServiceScopeFactory em BackgroundService: por que injetar serviços scoped diretamente quebra

Você cria um JobProcessor, registra como scoped, injeta no seu worker, dá F5 — e o app explode com Cannot consume scoped service ... from singleton. Bem-vindo a uma das armadilhas mais confusas do .NET: o erro não está no seu JobProcessor, está em como você o injetou.

Neste post você vai entender a causa raiz desse erro de lifetime e ver o padrão correto com IServiceScopeFactory — além de descobrir por que criar um escopo novo a cada iteração do loop não é detalhe, é a parte que importa.

O cenário: tudo parece certo, mas não roda

Imagine um worker bem estruturado. Você tirou a lógica de dentro da classe Worker e criou um componente separado, o JobProcessor, registrado como scoped no program.cs:

builder.Services.AddScoped<JobProcessor>();
Enter fullscreen mode Exit fullscreen mode

Por que scoped? Porque, na vida real, o JobProcessor provavelmente vai depender de um DbContext — e contexto de banco de dados é um caso clássico de serviço com lifetime scoped. Até aqui, tudo idiomático.

Aí você injeta direto no worker:

public class Worker(JobProcessor jobProcessor) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await jobProcessor.ProcessNextJob();
            await Task.Delay(2000, stoppingToken);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Roda? Não. Você encontra exatamente o erro que estava tentando evitar.

A causa raiz: um BackgroundService é um singleton

Aqui está o detalhe que quase todo mundo esquece: um BackgroundService (hosted service) é tratado como singleton pelo container de DI. Ele é criado uma vez e vive enquanto a aplicação existe.

E o container do .NET tem uma regra de proteção: um singleton não pode consumir um serviço scoped diretamente. Faz sentido — um serviço scoped tem vida curta e amarrada a um escopo (uma requisição, uma unidade de trabalho), enquanto o singleton vive "para sempre". Se o container deixasse você capturar o scoped no construtor do singleton, você prenderia aquele DbContext de vida curta a um objeto eterno. É o caminho garantido para vazamento de recursos e bugs sutis.

Por isso o container barra na largada, com o erro de "scoped from singleton". Não é um capricho — é o runtime te protegendo de um problema pior lá na frente.

A correção: injete a factory, não o serviço

A solução é não pedir o JobProcessor no construtor. Em vez disso, injete um IServiceScopeFactory e use-o para criar um escopo dentro do loop, resolvendo o serviço scoped a cada iteração:

public class Worker(IServiceScopeFactory serviceScopeFactory) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using var scope = serviceScopeFactory.CreateScope();
            var jobProcessor = scope.ServiceProvider.GetRequiredService<JobProcessor>();

            await jobProcessor.ProcessNextJob();

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

Repare no que mudou. Estamos fazendo a mesma coisa de antes — chamar ProcessNextJob() — mas:

  1. Injetamos IServiceScopeFactory (que é singleton-safe), não o JobProcessor.
  2. A cada volta do loop, CreateScope() cria um escopo fresco.
  3. GetRequiredService<JobProcessor>() resolve o serviço scoped dentro daquele escopo.
  4. O using garante que o escopo é descartado no fim da iteração.

O JobProcessor continua registrado como scoped — está tudo bem. O que muda é que agora respeitamos o lifetime dele em vez de tentar furá-lo.

Por que um escopo fresco a cada iteração importa

É tentador pensar "ok, criei o escopo, problema resolvido". Mas o ganho real é mais profundo: um escopo novo por iteração mantém o worker seguro ao longo do tempo.

  • O worker em si pode ter vida longa (ele é o coordenador, fica de pé o tempo todo).
  • Os serviços que fazem o trabalho ganham o lifetime scoped correto — nascem e morrem dentro de cada iteração.
  • O using descarta as dependências de forma limpa ao fim de cada ciclo. Se há um DbContext ali dentro, ele é criado, usado e liberado a cada job, sem acumular estado de uma iteração para a outra.

É essa combinação que dá o melhor dos dois mundos: um worker de longa duração coordenando, e serviços de vida curta executando sem riscos de estado compartilhado ou conexões penduradas.

Conclusão

Quando você vê Cannot consume scoped service from singleton no seu Worker Service, a mensagem está dizendo uma verdade incômoda: seu BackgroundService é singleton, e ele não pode segurar um scoped direto. A correção não é mudar o lifetime do JobProcessor — é injetar IServiceScopeFactory e criar um escopo fresco a cada iteração do loop.

Da próxima vez que esbarrar nesse erro, lembre: o worker coordena, o escopo isola, o serviço executa. Já caiu nessa armadilha em algum projeto seu? Teste o padrão acima e veja como o loop fica mais previsível — e seguro com banco de dados.

Top comments (0)