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>();
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);
}
}
}
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);
}
}
}
Repare no que mudou. Estamos fazendo a mesma coisa de antes — chamar ProcessNextJob() — mas:
- Injetamos
IServiceScopeFactory(que é singleton-safe), não oJobProcessor. - A cada volta do loop,
CreateScope()cria um escopo fresco. -
GetRequiredService<JobProcessor>()resolve o serviço scoped dentro daquele escopo. - O
usinggarante 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
usingdescarta as dependências de forma limpa ao fim de cada ciclo. Se há umDbContextali 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)