Start Debugging

Quartz.NET 4.2 Transforma Expressões Cron Inválidas em Erros de Compilação

O Quartz.NET 4.2.0 traz um analisador Roslyn que rejeita literais cron não interpretáveis como QZ0001 em tempo de compilação, além de um gerador de código-fonte que transforma os atributos [QuartzJob] e [CronTrigger] em um registro AddDeclaredJobs(). Veja o que ele verifica, como é o código gerado e como desativá-lo.

A maioria dos usuários do Quartz.NET já enviou para produção uma expressão cron que parecia correta na revisão e lançou uma FormatException na inicialização. O Quartz.NET 4.2.0, lançado em 25 de setembro de 2026 e seguido pelo patch 4.2.1 em 27 de setembro, move essa falha para o compilador. O pacote Quartz agora traz seu próprio analisador e gerador de código-fonte, sem nenhum pacote extra para instalar.

QZ0001: o parser de cron é executado em tempo de compilação

O analisador inspeciona todo literal ou constante cron passado para WithCronSchedule, CronScheduleBuilder.Create, os construtores de CronExpression, CronCalendar e CronTriggerImpl. Ele não usa uma segunda gramática: ele vincula os próprios fontes do parser do scheduler, e um corpus de paridade com 128 expressões mantém os dois em concordância. Se o compilador aceita um literal, o scheduler também aceita.

q.AddTrigger(t => t
    .ForJob(jobKey)
    .WithCronSchedule("0 12 * * 1-5"));
// error QZ0001, reported on the literal with the parser's own message

Esse exemplo é o erro clássico: uma expressão crontab de cinco campos copiada do Linux. O Quartz lê seis ou sete campos (segundos primeiro) e exige ? em um dos dois campos de dia, então a versão do Quartz é "0 0 12 ? * MON-FRI". Se você realmente quer a gramática Unix, passe CronFormat.Unix como literal e o analisador valida contra ela.

Três outras regras vêm junto:

Declarando o job na própria classe

A segunda metade do recurso é um gerador que lê [QuartzJob] e [CronTrigger] dos seus tipos IJob:

[QuartzJob(Name = "cleanup", Group = "maintenance")]
[CronTrigger("0 0 0/6 * * ?")]
[CronTrigger("0 0 12 ? * MON-FRI", Name = "cleanup-weekday-noon", TimeZone = "Europe/Helsinki")]
public sealed class CleanupJob : IJob
{
    public ValueTask Execute(IJobExecutionContext context, CancellationToken cancellationToken = default)
        => default;
}

services.AddQuartz(q => q.AddDeclaredJobs());
services.AddQuartzHostedService();

AddDeclaredJobs() é gerado no seu assembly como uma extensão internal de IQuartzBuilder. Ele contém exatamente as chamadas AddJob<T> e AddTrigger<T> que você mesmo teria escrito manualmente, então não há varredura de assembly e nada que precise ser mantido como raiz (root) para o trimming ou o Native AOT. As strings cron nos atributos passam pelo QZ0001 como qualquer outro literal. Defina <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> se quiser ler o QuartzDeclaredJobs.g.cs gerado.

O gerador tem suas próprias barreiras de proteção: QZ1001 rejeita o atributo em um tipo que não é um IJob concreto, QZ1002 rejeita duas declarações com a mesma identidade, e QZ1003 rejeita um [CronTrigger] sem [QuartzJob]. Um job sem trigger é forçado a Durable = true para que o store não o exclua imediatamente.

Notas de atualização

O analisador vem habilitado por padrão, o que significa que um projeto existente com um literal quebrado para de compilar. Essa é a intenção, mas fique atento também ao QZ0003 sob TreatWarningsAsErrors. Para desativar tudo, defina isto no arquivo do projeto:

<PropertyGroup>
  <DisableQuartzAnalyzers>true</DisableQuartzAnalyzers>
</PropertyGroup>

As notas de lançamento apontam que ExcludeAssets="analyzers" na referência do pacote não o desativa no SDK do .NET 10. As severidades individuais ainda podem ser ajustadas no .editorconfig.

Se você usa um store de jobs persistente, a versão 4.2 também exige a migração database/migrations/4.2/add_continuations_<dialect>.sql antes que o primeiro nó 4.2 seja iniciado, por causa do novo recurso de continuações de trigger. E se você habilitar o novo histórico de execução baseado em banco de dados em um schema criado a partir de tables_sqlServerMOT.sql ou tables_sqlServer_Below2016.sql, vá direto para a 4.2.1, que corrige a coluna RETRY_ATTEMPT que estava faltando.

Se você ainda está decidindo se o Quartz é o scheduler certo, eu o comparei com as alternativas em Hangfire vs Quartz.NET vs IHostedService.

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Voltar