1. IHostedService — o contrato mínimo
Qualquer classe registrada como "hosted service" precisa implementar dois métodos:
StartAsync (chamado quando a aplicação inicia) e
StopAsync (chamado quando ela está encerrando):
public interface IHostedService
{
Task StartAsync(CancellationToken cancellationToken);
Task StopAsync(CancellationToken cancellationToken);
}
Na prática, você raramente implementa IHostedService diretamente —
a classe abstrata BackgroundService já cuida da parte repetitiva
(começar, parar, tratar cancelamento) e expõe um único método para você preencher:
2. BackgroundService — o ponto de partida real
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger) => _logger = logger;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker executando às {Hora}", DateTimeOffset.Now);
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
}
| Peça | Papel |
|---|---|
ExecuteAsync | Onde toda a lógica de fundo mora — chamado uma vez, e deveria conter o laço de repetição |
stoppingToken | Sinalizado quando a aplicação está sendo encerrada — checá-lo é o que permite parar graciosamente |
Task.Delay(..., stoppingToken) | Pausa entre execuções, mas cancelável imediatamente se o token disparar (em vez de esperar o delay inteiro) |
Sempre passe stoppingToken para Task.Delay
e para qualquer chamada assíncrona dentro do laço (incluindo chamadas ao SQL Server). Sem isso, a
aplicação pode demorar muito mais que o esperado para encerrar quando o processo recebe um
pedido de parada (por exemplo, durante um deploy).
3. Registrando o worker
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
builder.Services.AddScoped<IPedidoRepository>(_ =>
new PedidoRepository(builder.Configuration.GetConnectionString("Default")!));
var host = builder.Build();
host.Run();
AddHostedService<Worker>() é o registro que diz ao .NET "inicie
esta classe junto com a aplicação". É possível registrar múltiplos hosted services no mesmo
processo — cada um roda seu próprio ExecuteAsync de forma independente.
4. Tratamento de erro dentro do laço
Uma exceção não tratada dentro de ExecuteAsync derruba o worker
inteiro — diferente de uma exceção numa Action de API (Módulo 4, capítulo 6), que afeta só aquela
requisição. Por isso, o laço de um worker precisa do próprio tratamento de erro:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await ProcessarPedidosPendentesAsync(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Falha ao processar pedidos pendentes");
// não relança — uma falha nesta execução não deveria matar o worker inteiro
}
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
Este é um dos poucos lugares legítimos onde capturar Exception
genericamente e não relançar (capítulo 8 do Módulo 1) faz sentido — o objetivo
aqui é justamente isolar uma falha de uma execução para que o processo inteiro continue vivo e
tente de novo no próximo ciclo. A regra "não engula exceções" continua valendo: sempre logue.
5. Onde isso te leva
Com a estrutura básica de execução contínua dominada, o próximo capítulo cobre como injetar dependências corretamente dentro de um worker (um detalhe diferente do MVC/API, por causa do tempo de vida do próprio serviço) e como configurar logging estruturado.