Modern applications often require automated execution based on specific triggers.
By offloading tasks that don’t require immediate user interaction to background jobs, applications can improve efficiency, reliability and resource management.
In .NET, we have excellent libraries for job scheduling, with the best choice depending on the specific needs of your project.
If you've been following me for a while, you know that my first choice for scheduling jobs is Quartz.NET.
In future blogs, we will definitely cover alternatives such as Hangfire and Coravel.
Quartz
Quartz.NET is an amazing open-source job scheduling library for .NET applications.
With Quartz, we can schedule and run background jobs at specific times, intervals and more. This feature-rich library offers:
- Time-based scheduling
- Recurring jobs
- Persistent job storage
- Built-in load balancing support
Here are some of the use cases I’ve implemented with Quartz:
- Automatic email, in-app, and SMS notifications
- Data cleanup tasks
- Generating reports at specific time intervals
- Implementing the Outbox Pattern to separate the main operation from its side effects
Additionally, Quartz.NET offers a range of plugins to extend its functionality.
Getting Started
To get started with Quartz.NET, you'll first need to install the necessary NuGet packages. You can do this via the NuGet Package Manager or by running the following command in the Package Manager Console:
dotnet add package Quartz.Extensions.Hosting
For Quartz 3.2 or later, it is preferred to use Quartz.Extensions.Hosting for a clean integration with ASP.NET Core. If you are using Quartz 3.1 or earlier, you can use the Quartz.AspNetCore package
Once the packages are installed, we can create and set up our first scheduled job.
Simple Background Job
Creating a job in Quartz.NET involves defining a job and a trigger.
The job specifies the task, while the trigger defines the schedule.
A job in Quartz.NET implements IJob, which has an Execute method that runs when the job is triggered. Here's a simple job example:
public class VerifyProductsJob(IApplicationDbContext dbContext) : IJob
{
public async Task Execute(IJobExecutionContext context)
{
var products = await GetProducts(context.CancellationToken);
foreach (var product in products)
{
product.Verify();
}
await dbContext.SaveChangesAsync();
}
}
[DisallowConcurrentExecution] attribute ensures that the job does not run concurrently.
Configuration
After creating our simple job, we can now integrate Quartz.NET into our application and configure the job.
Here is a simple and minimal setup to integrate Quartz.NET into your ASP.NET Core application:
builder.Services.AddQuartz();
builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
AddQuartz registers Quartz.NET with the dependency injection. Configure allows us to specify additional settings for Quartz.NET.
AddQuartzHostedService registers the Quartz.NET hosted service, which is responsible for running the scheduler.
WaitForJobsToComplete ensures that when the application is stopping, it will wait for all currently running jobs to complete before it fully shuts down.
Next step is to configure a trigger for our job. Quartz.NET offers a variety of different triggers to tailor to your needs:
- Simple Trigger: Use for fixed intervals or delays to define the frequency of execution.
- Cron Trigger: Creates scheduling using cron expressions.
- Calendar Interval Trigger: Use for intervals.
- Daily Time Interval Trigger: Schedule jobs to run at specific times during the day.
Here’s an example of how to configure your job with a simple trigger that repeats indefinitely:
builder.Services.AddQuartz(q =>
{
q.AddJob<VerifyProductsJob>(c => c
.StoreDurably()
.WithIdentity(nameof(VerifyProductsJob)));
q.AddTrigger(opts => opts
.ForJob(nameof(VerifyProductsJob))
.WithIdentity(nameof(VerifyProductsJob))
.WithSimpleSchedule(x => x
.WithIntervalInSeconds(5)
.RepeatForever()));
});
Cron Expressions
A cron expression consists of six or seven fields, each representing a specific time unit. This powerful feature allows you to define complex schedules with precision, from simple intervals to intricate patterns.
The fields in a cron expression are: seconds, minutes, hours, day of month, month, day of week, and optionally year.
Each field can contain specific values, ranges, or special characters to create flexible schedules.
┌──────────── second (0-59)
│ ┌────────── minute (0-59)
│ │ ┌──────── hour (0-23)
│ │ │ ┌────── day of month (1-31)
│ │ │ │ ┌──── month (1-12)
│ │ │ │ │ ┌── day of week (0-6)
* * * * * *
Special characters such as * for all values, ? is for no specific value, and / is for increments. For example, an expression like "0/5 14 * * ?" triggers every 5 minutes starting at 2 PM.
With cron expressions, you can automate tasks to run at exactly the right moment. Here is an example of implementation with Quartz.NET:
services.AddQuartz(configure =>
{
configure
.AddJob<VerifyProductsJob>(nameof(VerifyProductsJob))
.AddTrigger(trigger => trigger
.ForJob(nameof(VerifyProductsJob))
.WithCronSchedule("0/5 14 * * ?"));
});
Job Stores
Job stores Job Stores are responsible for persisting job and trigger data. Quartz.NET is capable of running jobs in-memory or with persistence, depending on your needs.
NOTE: Never use a JobStore instance directly in your code.
RAMJobStore
RAMJobStore is the simplest JobStore to use, it is also the most performant. Because it keeps all of its data in RAM, it's lightning-fast.
Default configuration uses RAMJobStore as job store implementation.
The drawback is that when your application ends all of the scheduling information is lost.
ADOJobStore
ADOJobStore keeps all of its data in a database. Since we're now persisting data in a database, you'll need to configure it, and it won't be as fast as in-memory storage.
To use AdoJobStore, you must first create a set of database tables for Quartz.NET to use. You can find table-creation SQL scripts in the database/tables.
It's also worth noting that in these scripts, all the tables have the prefix QRTZ_. However, you can use any prefix you prefer, as long as you specify it in the AdoJobStore configuration.
Here is an simple example of the configuration with SQL Server:
builder.Services.AddQuartz(q =>
{
q.UsePersistentStore(cfg =>
{
cfg.UseProperties = true;
cfg.UseNewtonsoftJsonSerializer();
cfg.UseSqlServer(builder.Configuration.GetConnectionString("Database")!);
});
});
StoreDurably will store job definition if no triggers are associated with it. Not really needed in our example but you should definitely know about this method.
Plugins
Quartz.Plugins provides some useful ready-made plugins for your convenience. Plugins are configured by using either DI configuration extensions or adding required configuration keys.
- LoggingJobHistoryPlugin - Logs a history of all job executions and writes the entries to the configured logging infrastructure.
- ShutdownHookPlugin - Catches the event of the VM terminating and tells the scheduler to shut down.
- XMLSchedulingDataProcessorPlugin - Loads XML files to add jobs and schedule them with triggers as the scheduler is initialized.
- JobInterruptMonitorPlugin - Monitors long-running jobs and notifies the scheduler to attempt interrupting them if enabled.
With them, you can further customize and enhance how your scheduling solution works.
Conclusion
Whether you're building a small application or a large enterprise system, Quartz.NET provides the tools you need to efficiently manage background processes and ensure your application runs smoothly.
It's a reliable, highly customizable tool with additional plugins to help you create scheduled jobs.
Job stores handle the actual storage of jobs and triggers, ensuring that they are available even if the application restarts or fails.
If you want to check out examples I created, you can find the source code here:
Source CodeI hope you enjoyed it, subscribe and get a notification when a new blog is up!
Happy scheduling!