1. Intervalo fixo vs. horário específico
O Task.Delay visto nos capítulos anteriores resolve bem "execute a
cada N minutos", mas não resolve "execute todo dia às 3h da manhã" — depois de um restart do
processo em horário aleatório, um simples delay fixo perde a referência de quando deveria rodar
de fato.
2. PeriodicTimer — a forma moderna de intervalos fixos
Desde o .NET 6, PeriodicTimer é a forma recomendada de esperar em
intervalos regulares — mais eficiente que Task.Delay em laço, porque
não acumula atraso (drift) entre execuções:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var temporizador = new PeriodicTimer(TimeSpan.FromMinutes(5));
while (await temporizador.WaitForNextTickAsync(stoppingToken))
{
using var escopo = _scopeFactory.CreateScope();
var repositorio = escopo.ServiceProvider.GetRequiredService<IPedidoRepository>();
await ProcessarPedidosPendentesAsync(repositorio, stoppingToken);
}
}
WaitForNextTickAsync devolve true a cada
"tick" do intervalo configurado, e false quando o
stoppingToken é cancelado — encerrando o while
automaticamente, sem checagem manual extra.
3. Calculando o horário até a próxima execução (estilo cron)
Para "todo dia às 3h", calcule quanto tempo falta até o próximo horário-alvo e espere exatamente isso:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var agora = DateTime.Now;
var proximaExecucao = agora.Date.AddHours(3); // hoje às 3h
if (agora > proximaExecucao)
proximaExecucao = proximaExecucao.AddDays(1); // já passou — agenda para amanhã
var tempoAteExecutar = proximaExecucao - agora;
_logger.LogInformation("Próxima execução em {Tempo}", tempoAteExecutar);
await Task.Delay(tempoAteExecutar, stoppingToken);
using var escopo = _scopeFactory.CreateScope();
var repositorio = escopo.ServiceProvider.GetRequiredService<IRelatorioRepository>();
await GerarRelatorioDiarioAsync(repositorio, stoppingToken);
}
}
Essa lógica de "calcular o próximo horário-alvo" é reaproveitável para qualquer agenda fixa —
só muda a conta de proximaExecucao. Para agendas mais elaboradas
(a cada segunda e quinta às 9h, por exemplo), bibliotecas como Cronos (que
interpreta expressões cron de verdade) evitam reescrever essa matemática de datas manualmente.
4. Expressões cron com a biblioteca Cronos
var expressao = CronExpression.Parse("0 3 * * *"); // todo dia às 3h
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var proxima = expressao.GetNextOccurrence(DateTime.UtcNow);
if (proxima is null) break;
var atraso = proxima.Value - DateTime.UtcNow;
await Task.Delay(atraso, stoppingToken);
await GerarRelatorioDiarioAsync(stoppingToken);
}
}
| Expressão cron | Significado |
|---|---|
0 3 * * * | Todo dia às 3h |
*/15 * * * * | A cada 15 minutos |
0 9 * * 1,4 | Segundas e quintas às 9h |
0 0 1 * * | No primeiro dia de cada mês, à meia-noite |
5. Onde isso te leva
Com agendamento coberto, o próximo capítulo trata de como esse mesmo processo roda de fato em produção — como Serviço do Windows ou daemon Linux, sobrevivendo a reinicializações do servidor.