Quartz.NET – Complete Guide with Code Samples

Quartz.NET is a powerful, open-source job scheduling library for .NET. It provides a robust framework for executing background tasks, ranging from simple recurring jobs to complex distributed workflows with clustering and persistence. This article compiles key concepts, capabilities, and code samples into one place.

Setup – .NET Generic Host + Quartz

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Quartz;
using Quartz.Spi;

var builder = Host.CreateDefaultBuilder(args)
    .ConfigureServices((hostContext, services) =>
    {
        services.AddQuartz(q =>
        {
            q.SchedulerId = "Scheduler-Core";
            q.UseMicrosoftDependencyInjectionJobFactory();

            q.AddJob<EmailReminderJob>(opts => opts.WithIdentity("EmailReminderJob").StoreDurably());
            q.AddTrigger(opts => opts
                .ForJob("EmailReminderJob")
                .WithIdentity("EmailReminderTrigger")
                .WithSimpleSchedule(x => x.WithIntervalInMinutes(15).RepeatForever())
            );
        });

        services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
        services.AddScoped<IEmailSender, SmtpEmailSender>();
    });

await builder.RunConsoleAsync();

Basic Job and Dependency Injection

using Quartz;

public class EmailReminderJob : IJob
{
    private readonly IEmailSender _email;

    public EmailReminderJob(IEmailSender email) => _email = email;

    public async Task Execute(IJobExecutionContext context)
    {
        var jobData = context.MergedJobDataMap;
        var userId = jobData.GetString("userId");
        await _email.SendAsync(userId, "Reminder", "This is your scheduled reminder.");
    }
}

Scheduling Types

// Simple trigger: run once after 30 seconds
var simpleTrigger = TriggerBuilder.Create()
    .WithIdentity("oneOffTrigger")
    .StartAt(DateBuilder.FutureDate(30, IntervalUnit.Second))
    .ForJob("EmailReminderJob")
    .Build();

// Cron trigger: every day at 02:30 AM
var cronTrigger = TriggerBuilder.Create()
    .WithIdentity("dailyCron")
    .WithCronSchedule("0 30 2 ? * *")
    .ForJob("EmailReminderJob")
    .Build();

JobDataMap – Passing Parameters

var job = JobBuilder.Create<EmailReminderJob>()
    .WithIdentity("EmailReminderJob-User42")
    .UsingJobData("userId", "42")
    .StoreDurably()
    .Build();

Durable Jobs, Recovery, Non-Concurrent Execution

[DisallowConcurrentExecution]
public class ReportJob : IJob { ... }

[PersistJobDataAfterExecution]
public class RecoverableJob : IJob { ... }

Persistence with PostgreSQL

services.AddQuartz(q =>
{
    q.UseMicrosoftDependencyInjectionJobFactory();
    q.UsePersistentStore(store =>
    {
        store.UsePostgres(pg =>
        {
            pg.ConnectionString = "Host=localhost;Username=quartz;Password=secret;Database=quartzdb";
            pg.TablePrefix = "qrtz_";
        }, dataSourceName: "default");

        store.UseProperties = true;
        store.UseNewtonsoftJsonSerializer();
    });

    q.SchedulerName = "ClusteredScheduler";
    q.SchedulerId = "AUTO";
});

Clustering

q.UsePersistentStore(store =>
{
    store.UsePostgres(...);
    store.UseProperties = true;
    store.Clustered = true;
    store.TablePrefix = "qrtz_";
});

Listeners (Job/Trigger/Scheduler)

public class SimpleJobListener : IJobListener
{
    public string Name => "SimpleJobListener";
    public Task JobToBeExecuted(IJobExecutionContext context, CancellationToken ct) { /*...*/ }
    public Task JobExecutionVetoed(IJobExecutionContext context, CancellationToken ct) { /*...*/ }
    public Task JobWasExecuted(IJobExecutionContext context, JobExecutionException? ex, CancellationToken ct) { /*...*/ }
}

Misfire Handling

// Cron misfire handling
.WithCronSchedule("0 0 9 ? * *", cronOpts => cronOpts.WithMisfireHandlingInstructionDoNothing());

// Simple schedule misfire handling
.WithSimpleSchedule(x => x.WithIntervalInMinutes(5).RepeatForever().WithMisfireHandlingInstructionFireNow());

Calendars – Excluding Dates

var holidayCalendar = new HolidayCalendar();
holidayCalendar.AddExcludedDate(new DateTime(2025, 12, 25));
await scheduler.AddCalendar("holidays", holidayCalendar, true, true);

Job Management – Pause, Resume, Delete

await scheduler.PauseJob(new JobKey("EmailReminderJob"));
await scheduler.ResumeJob(new JobKey("EmailReminderJob"));
await scheduler.DeleteJob(new JobKey("EmailReminderJob"));

Job Chaining

public class FirstJob : IJob
{
    public async Task Execute(IJobExecutionContext context)
    {
        var sched = context.Scheduler;
        var nextJob = JobBuilder.Create<SecondJob>().WithIdentity("Second").Build();
        var trigger = TriggerBuilder.Create().StartNow().ForJob(nextJob).Build();
        await sched.ScheduleJob(nextJob, trigger);
    }
}

Custom JobFactory

public class CustomJobFactory : IJobFactory
{
    private readonly IServiceProvider _provider;
    public CustomJobFactory(IServiceProvider provider) => _provider = provider;
    public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
        => (IJob)_provider.GetRequiredService(bundle.JobDetail.JobType);
    public void ReturnJob(IJob job) { }
}

Serialization Options

persistenceOptions.UseNewtonsoftJsonSerializer();
persistenceOptions.UseSimpleTypeSerializer();

Long-Running Jobs with Cancellation

public class LongJob : IJob
{
    public async Task Execute(IJobExecutionContext context)
    {
        var ct = context.CancellationToken;
        for (int i=0; i<100; i++)
        {
            ct.ThrowIfCancellationRequested();
            await Task.Delay(1000, ct);
        }
    }
}

Comments

Popular posts from this blog

Maxpooling vs minpooling vs average pooling

Generative AI - Prompting with purpose: The RACE framework for data analysis

Best Practices for Storing and Loading JSON Objects from a Large SQL Server Table Using .NET Core