Quartz.NET is a full-featured job scheduling library ported from the Java Quartz framework. It handles cron expressions, interval triggers, persistent job stores, clustering, and integrates cleanly with .NET dependency injection. If you need something more sophisticated than a Timer or IHostedService, Quartz.NET is the go-to choice.
Getting Started
dotnet add package Quartz.Extensions.Hosting
This package includes the core scheduler, DI integration, and hosted service support.
Defining a Job
A job implements IJob:
public class SendDailyReportJob : IJob
{
private readonly IReportService _reportService;
private readonly ILogger<SendDailyReportJob> _logger;
public SendDailyReportJob(
IReportService reportService,
ILogger<SendDailyReportJob> logger)
{
_reportService = reportService;
_logger = logger;
}
public async Task Execute(IJobExecutionContext context)
{
_logger.LogInformation("Generating daily report");
var report = await _reportService.GenerateAsync();
await _reportService.SendAsync(report);
_logger.LogInformation("Daily report sent successfully");
}
}
Jobs are resolved from the DI container, so you can inject any registered service.
Configuration
Configure the scheduler, jobs, and triggers in Program.cs:
builder.Services.AddQuartz(q =>
{
q.UseMicrosoftDependencyInjectionJobFactory();
var jobKey = new JobKey("daily-report");
q.AddJob<SendDailyReportJob>(opts => opts
.WithIdentity(jobKey)
.StoreDurably());
q.AddTrigger(opts => opts
.ForJob(jobKey)
.WithIdentity("daily-report-trigger")
.WithCronSchedule("0 0 8 * * ?")); // Every day at 08:00
});
builder.Services.AddQuartzHostedService(opts =>
{
opts.WaitForJobsToComplete = true;
});
Cron Expressions
Quartz cron expressions have 7 fields: seconds, minutes, hours, day-of-month, month, day-of-week, and year. Common patterns:
// Every 5 minutes
"0 0/5 * * * ?"
// Weekdays at 09:00
"0 0 9 ? * MON-FRI"
// First day of every month at midnight
"0 0 0 1 * ?"
// Every 30 seconds
"0/30 * * * * ?"
Interval Triggers
For simpler scheduling, use WithSimpleSchedule:
q.AddTrigger(opts => opts
.ForJob(jobKey)
.WithIdentity("cleanup-trigger")
.WithSimpleSchedule(x => x
.WithIntervalInMinutes(15)
.RepeatForever()));
Persistent Job Store
By default, Quartz.NET keeps jobs in memory. If your application restarts, all scheduled state is lost. For production, use a persistent store:
builder.Services.AddQuartz(q =>
{
q.UsePersistentStore(store =>
{
store.UseProperties = true;
store.UseSqlServer(options =>
{
options.ConnectionString =
builder.Configuration.GetConnectionString("Quartz")!;
options.TablePrefix = "QRTZ_";
});
store.UseNewtonsoftJsonSerializer();
});
});
Quartz.NET provides SQL scripts to create the required tables for SQL Server, PostgreSQL, MySQL, and SQLite.
Passing Data to Jobs
Use the JobDataMap to pass parameters:
q.AddJob<ProcessBatchJob>(opts => opts
.WithIdentity("process-batch")
.UsingJobData("batchSize", 100)
.UsingJobData("region", "eu-west"));
Access the data in the job:
public async Task Execute(IJobExecutionContext context)
{
var batchSize = context.MergedJobDataMap.GetInt("batchSize");
var region = context.MergedJobDataMap.GetString("region");
await ProcessBatchAsync(batchSize, region);
}
Preventing Concurrent Execution
If a job takes longer than the trigger interval, you might get overlapping executions. Prevent this with the DisallowConcurrentExecution attribute:
[DisallowConcurrentExecution]
public class SlowProcessingJob : IJob
{
public async Task Execute(IJobExecutionContext context)
{
// Only one instance runs at a time
await Task.Delay(TimeSpan.FromMinutes(5));
}
}
Listening for Job Events
Implement IJobListener to react to job execution:
public class JobExecutionListener : IJobListener
{
private readonly ILogger<JobExecutionListener> _logger;
public JobExecutionListener(ILogger<JobExecutionListener> logger)
=> _logger = logger;
public string Name => "JobExecutionListener";
public Task JobToBeExecuted(IJobExecutionContext context, CancellationToken ct)
{
_logger.LogInformation("Job {JobKey} starting", context.JobDetail.Key);
return Task.CompletedTask;
}
public Task JobWasExecuted(IJobExecutionContext context,
JobExecutionException? exception, CancellationToken ct)
{
if (exception is not null)
_logger.LogError(exception, "Job {JobKey} failed", context.JobDetail.Key);
else
_logger.LogInformation("Job {JobKey} completed", context.JobDetail.Key);
return Task.CompletedTask;
}
public Task JobExecutionVetoed(IJobExecutionContext context, CancellationToken ct)
=> Task.CompletedTask;
}
Quartz.NET is well suited for scheduled, recurring work — daily reports, periodic cleanup, timed batch processing. For fire-and-forget background jobs triggered by user actions, Hangfire (covered separately) is often a better fit.