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

Agendamento de Tarefas (Timers e Cron-like)

Nem todo trabalho de fundo roda "a cada X minutos" — às vezes precisa ser "toda madrugada às 3h". Duas formas de resolver isso em .NET.

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:

C# PeriodicTimer
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:

C# executando em um horário fixo diário
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);
    }
}
Dica

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

C# usando expressão cron (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 cronSignificado
0 3 * * *Todo dia às 3h
*/15 * * * *A cada 15 minutos
0 9 * * 1,4Segundas 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.

📌 Resumo do capítulo

  • PeriodicTimer é a forma moderna de esperar em intervalos fixos, sem acumular atraso.
  • Para horários específicos (estilo cron), calcule o tempo até o próximo horário-alvo e use Task.Delay.
  • Para agendas mais elaboradas, uma biblioteca de expressões cron (como Cronos) evita reescrever a matemática de datas manualmente.

✏️ Praticando

  1. Reescreva o worker do capítulo 3 usando PeriodicTimer em vez de Task.Delay em laço.
  2. Implemente um worker que roda uma única vez por dia, em um horário configurável via appsettings.json.