Complete Guide to Implementing Task Scheduling with Quartz.NET

Task scheduling is a backbone feature in modern software development, enabling applications to execute tasks at predetermined times without human intervention.

This automation is crucial for tasks like database maintenance, batch processing, or even mundane daily tasks, ensuring efficiency and reliability in the application's operations. Task scheduling can also help to balance the load on a system by scheduling tasks to run during off-peak times.

This article ventures deep into Quartz.NET's functionalities, culminating in a practical example that not only demonstrates its capabilities but also illustrates how to bring theory into practice.

Introduction to Quartz.NET

Quartz.NET stands as a powerful, open-source job scheduling library written for .NET applications. Born from the Java-based Quartz scheduler, Quartz.NET adapts and extends its predecessor's capabilities for the .NET ecosystem.

The library was created by Marko Lahma and it is maintained by the community, with new versions released at least every 6 months, guaranteeing a stable library. The source code for Quartz.NET can be found on GitHub.

GitHub - quartznet/quartznet: Quartz Enterprise Scheduler .NET
Quartz Enterprise Scheduler .NET. Contribute to quartznet/quartznet development by creating an account on GitHub.

Quartz.NET simplifies the creation and management of scheduled tasks. Here are some foundational elements:

  • Jobs: Where the task's logic lives.
  • Triggers: Dictate when a job should run.

The job is a class inheriting from IJob containing the Task Execute(IJobExecutionContext context) method, which will be called when the job is triggered. You can use all the classic mechanisms, such as dependency injection, to access its services. It is declared as follows

IJobDetail job = JobBuilder.Create<MyJob>()
    .WithIdentity("MyJob", "MyJobGroup")
    .Build();

The trigger is the definition of the rules for starting a job through a Schedule corresponding to the periodicity of the trigger.

For example, a cron trigger could be set up with a schedule like "0 0 * * * ?" to run a job at the start of every hour.

ITrigger revokeTrigger = TriggerBuilder.Create()
    .WithIdentity("MyTrigger", "MyJobGroup")
    .StartNow()
    .WithCronSchedule("0 0 * * * ?")
    .Build();

Jobs are associated with triggers when they are registered in the scheduler.

// scheduler is a IScheduler
scheduler.ScheduleJob(revokeJob, revokeTrigger)

In this way, it's possible to have several jobs associated with a single trigger, if you want to run several different tasks at the same time.

Practical Example: Automating Goat Work Management

Consider a scenario where we need to ensure our goats head to work after 8 hours of sleep. This whimsical yet practical example demonstrates how Quartz.NET can be employed to manage such a task:

The first step is to define the job schedule. It's very simple to write, in this case with 0 0 * * * ? to start the job exactly every hour at 0 minutes and 0 seconds.

To enable this parameter to be changed on the fly without having to redeploy the application, it is possible to pass the parameters directly into a configuration file such as appsettings.json using this structure :

"Jobs" :  [
  {
    "Schedule": "0 0 * * * ?",
    "JobName": "GetGoatsToWorkJob",
    "JobGroup": "GetGoatsToWorkGroup",
    "TriggerName": "GetGoatsToWorkTrigger"
  }
]

These parameters are then retrieved from the configuration file with a structure exactly the same as that defined in the json file.

public class JobSetting
{
    public string Schedule { get; set; }
    public string JobName { get; set; }
    public string JobGroup { get; set; }
    public string TriggerName { get; set; }
}

public class JobSettings {
    public JobSetting[] Jobs { get; set; }
}

The declaration of jobs to be integrated is made directly at the dependency injection level, using the ISchedulerFactory. This allows you to retrieve an instance of an IScheduler to register the jobs to be launched in the background.

var schedulerFactory = app.Services.GetRequiredService<ISchedulerFactory>();
var scheduler = await schedulerFactory.GetScheduler();

// app is a WebApplication
var jobSettings = app.Configuration.GetSection(nameof(JobSettings)).Get<JobSettings>();

var goatJobConfig = jobSettings!.Jobs.First(c => c.JobName == nameof(GetGoatsToWorkJob));
await RegisterJob<GetGoatsToWorkJob>(jobConfigSetting, scheduler);

Quartz.NET job registration is also generic, so that it is the same for all jobs declared in the configuration.

private static async Task RegisterJob<T>(JobSetting jobSetting, IScheduler scheduler) where T : IJob
{
    IJobDetail job = JobBuilder.Create<T>()
        .WithIdentity(jobConfigSetting.JobName, jobConfigSetting.JobGroup)
        .Build();

    ITrigger trigger = TriggerBuilder.Create()
        .WithIdentity(jobConfigSetting.TriggerName, jobConfigSetting.JobGroup)
        .StartNow()
        .WithCronSchedule(jobConfigSetting.Schedule)
        .Build();
    
    await scheduler.ScheduleJob(job, trigger);
}

All that's left is to build the job to meet our needs, using dependency injection to access the database context.

public class GetGoatsToWorkJob : IJob
{
    private readonly GoatDbCtx _dbContext;
    
    public GetGoatsToWorkJob(GoatDbCtx dbContext)
    {
        _dbContext = dbContext;
    }
    
    public async Task Execute(IJobExecutionContext context)
    {
        // Add the job logic here
        dbContext.DoSomething();
        ...
    }
}

Advanced options

Unique execution

Some scheduled jobs cannot be executed several times at the same time. This is often the case, for example, when data is touched, first read and then modified, as is the case with databases.

To avoid this, you can specify that a job can only have one instance at a time by adding the attribute [DisallowConcurrentExecution] to the job class.

Outsource scheduling

By default, job management is in-memory to meet standard scheduling needs, but this can be changed. It's also possible to store them in external components such as SqlServer, MongoDb or Redis.

Clustering

It is also possible to have clustered management of the scheduler, particularly in the case of deployment on several containerized instances of a single application.

For the moment, integrated support for Quartz.NET is only available with ADO.NET.

Conclusion

Quartz.NET presents a versatile and powerful framework for integrating sophisticated scheduling capabilities into .NET applications.

Through its extensive feature set and the practical example of scheduling goat work shifts, it's clear that Quartz.NET can cater to a wide array of scheduling needs, from the simple to the complex. By understanding its core concepts and leveraging its capabilities, developers can automate tasks within their applications, enhancing efficiency and reliability.

Whether you're managing goat work shifts or automating critical data processing tasks, Quartz.NET stands as a reliable partner in the journey towards more autonomous and efficient applications.

Home | Quartz.NET
Open-source scheduling framework for .NET.

Have a goat day 🐐