1. O problema do Scoped dentro de um Singleton
Lembrando o Módulo 3 (capítulo 6): Scoped significa "uma instância por
requisição HTTP". Mas um Worker Service não tem requisições — o hosted service
em si é registrado como Singleton (existe uma única vez, para sempre). Isso cria um problema: se
o Worker recebesse um repositório Scoped
(como IPedidoRepository, que geralmente envolve um
DbContext) diretamente no construtor, essa mesma instância
seria reaproveitada para sempre — incluindo qualquer estado ou conexão que devesse ter vida curta.
Injetar um serviço Scoped direto no construtor de um
BackgroundService (que é Singleton) geralmente lança uma exceção em
tempo de execução, ou pior, funciona silenciosamente de um jeito errado. A solução é criar um
escopo manualmente a cada execução.
2. IServiceScopeFactory — criando um escopo por execução
public class Worker : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<Worker> _logger;
public Worker(IServiceScopeFactory scopeFactory, ILogger<Worker> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using (var escopo = _scopeFactory.CreateScope())
{
var repositorio = escopo.ServiceProvider.GetRequiredService<IPedidoRepository>();
await ProcessarPedidosPendentesAsync(repositorio, stoppingToken);
} // o escopo (e tudo Scoped dentro dele) é descartado aqui
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
}
Cada iteração do laço cria um escopo novo, resolve IPedidoRepository
(e, por trás dele, um DbContext novo) dentro desse escopo, e descarta
tudo ao final — exatamente como aconteceria naturalmente ao fim de uma requisição HTTP em MVC/API.
3. Configuração — lendo appsettings.json
{
"ConnectionStrings": {
"Default": "Server=localhost;Database=CursoDb;Trusted_Connection=True;"
},
"Worker": {
"IntervaloEmMinutos": 5
}
}
public class WorkerOptions
{
public int IntervaloEmMinutos { get; set; } = 5;
}
// Program.cs
builder.Services.Configure<WorkerOptions>(builder.Configuration.GetSection("Worker"));
// Worker.cs
public class Worker : BackgroundService
{
private readonly WorkerOptions _opcoes;
public Worker(IOptions<WorkerOptions> opcoes) => _opcoes = opcoes.Value;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var intervalo = TimeSpan.FromMinutes(_opcoes.IntervaloEmMinutos);
while (!stoppingToken.IsCancellationRequested)
{
// ...
await Task.Delay(intervalo, stoppingToken);
}
}
}
IOptions<T> evita valores "mágicos" espalhados pelo código
(o mesmo princípio do capítulo 14 do Módulo 1) — o intervalo do worker vira configurável sem
recompilar, útil para ajustar em produção sem um novo deploy.
4. Logging estruturado
Sem tela, o log é a única janela para o que um worker está fazendo. ILogger<T>
(já usado nos exemplos acima) suporta parâmetros estruturados — não apenas concatenar strings:
// evite — perde estrutura, difícil de consultar depois num sistema de log centralizado
_logger.LogInformation("Processados " + total + " pedidos em " + tempo + "ms");
// prefira — os valores viram campos pesquisáveis (ex: no Application Insights, Seq, ElasticSearch)
_logger.LogInformation("Processados {Total} pedidos em {TempoMs}ms", total, tempo);
| Nível | Quando usar |
|---|---|
LogTrace/LogDebug | Detalhes finos, só úteis durante desenvolvimento/depuração |
LogInformation | Eventos normais do fluxo (worker iniciou, processou N itens) |
LogWarning | Algo inesperado, mas não impede a operação de continuar |
LogError | Uma operação falhou (o catch do capítulo anterior) |
LogCritical | Falha grave o suficiente para ameaçar a aplicação inteira |
5. Onde isso te leva
Com DI e logging configurados corretamente, o próximo capítulo cobre a variação mais comum de workloads em background: tarefas que rodam em horários específicos, não só em intervalos fixos — o equivalente C# de um cron job.