Módulo 5 · Worker Service — Capítulo 03

Configuração, DI e Logging

Injetar dependências dentro de um serviço que vive para sempre exige um cuidado que não existe em MVC/API — e logging estruturado é o que torna um processo sem tela depurável.

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.

Atenção

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

C# Worker.cs — escopo correto por ciclo
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

JSON appsettings.json
{
  "ConnectionStrings": {
    "Default": "Server=localhost;Database=CursoDb;Trusted_Connection=True;"
  },
  "Worker": {
    "IntervaloEmMinutos": 5
  }
}
C# lendo configuração tipada
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);
        }
    }
}
Dica

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:

C# log estruturado vs. concatenado
// 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ívelQuando usar
LogTrace/LogDebugDetalhes finos, só úteis durante desenvolvimento/depuração
LogInformationEventos normais do fluxo (worker iniciou, processou N itens)
LogWarningAlgo inesperado, mas não impede a operação de continuar
LogErrorUma operação falhou (o catch do capítulo anterior)
LogCriticalFalha 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.

📌 Resumo do capítulo

  • Um BackgroundService é Singleton — nunca injete serviços Scoped direto no construtor; use IServiceScopeFactory para criar um escopo por execução.
  • IOptions<T> lê configuração tipada do appsettings.json, evitando valores mágicos.
  • Logging estruturado ({'{'}Parametro{'}'}, não concatenação) torna os logs pesquisáveis em ferramentas de observabilidade.

✏️ Praticando

  1. Ajuste o Worker do capítulo anterior para resolver IPedidoRepository via IServiceScopeFactory, um escopo por ciclo.
  2. Crie WorkerOptions com o intervalo configurável via appsettings.json, e confirme que mudar o valor no arquivo altera o comportamento sem recompilar.
  3. Substitua qualquer log concatenado por log estruturado com parâmetros nomeados.