Use Quartz.Net for background and recurring jobs within an ASP.NET Core 3.0 application

Doing background processing in a computer software is something very common. The idea behind it is to be able to process data without blocking or freezing the user interface or even trigger a function at a regular interval.

In this post, I'll show you how to implement background jobs, recurring jobs and delayed jobs within an ASP.NET Core 3.0 application using Quartz.Net. (All the code should work with ASP.NET Core 2 too) When it comes to background job in .NET, there is two well-known solutions: Hangfire and Quartz.Net. I would say Hangfire is the most modern and the easier one for basic usage. It is a turnkey solution but whenever you want to do more complex stuff, it becomes tricky or impossible. This is why I've chosen to use Quartz.Net on some of my personal projects and why I'm writing this article.

For the following, I am assuming you know the basics of C# and ASP.NET Core application development. If it's not the case and you're interested in building awesome web applications using one of the most loved programming languages, I encourage you to watch the official tutorials by Microsoft right here.

Let's start!

The first thing you'll need is an ASP.NET Core application, if you don't have one already, you can create one using the command line or your favorite IDE. For this post, I've created a Web API project without authentication using .NET Core 3.0.

Next you'll need to add a package reference to Quartz.Net NuGet package by typing the following command:

dotnet add package Quartz

Then we create and inject a IScheduler as a singleton in our service collection (Startup.cs):

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    var scheduler = StdSchedulerFactory.GetDefaultScheduler().GetAwaiter().GetResult();
    services.AddSingleton(scheduler);
}

The role of the scheduler is to poll and fire jobs. It is possible to use multiple schedulers in one application, but for simplicity, we will only use one (singleton) in this example.

Next step is to create a new class implementing IHostingService which will start and stop the scheduler for our application:

public class QuartzHostedService : IHostedService
{
    private readonly IScheduler _scheduler;

    public QuartzHostedService(IScheduler scheduler)
    {
        _scheduler = scheduler;
    }

    public Task StartAsync(CancellationToken ct)
    {
         return _scheduler.Start(ct);
    }

    public Task StopAsync(CancellationToken ct)
    {
        return _scheduler.Shutdown(ct);
    }
}

Do not forget to register the hosted service in the services collection as follows:

services.AddHostedService<QuartzHostedService>();

Then we create a job class implementing the IJob interface from Quartz.Net:

public class TestJob : IJob
{
    public Task Execute(IJobExecutionContext context)
    {
        Console.WriteLine("Hello from a job!");
        return Task.CompletedTask;
    }
}

In order to start a TestJob on a fire-and-forget mode from a service, you need to create a IJobDetail using the JobBuilder, create a ITrigger using the TriggerBuilder and finally call the IScheduler.ScheduleJob() method:

public Task StartTestJob()
{
    var jobDetails = JobBuilder
        .CreateForAsync<TestJob>()
        .WithIdentity("MyJobName")
        .WithDescription("My job description")
        .Build();
        
    var trigger = TriggerBuilder
        .Create()
        .StartNow()
        .Build();

    return _scheduler.ScheduleJob(jobDetails, trigger);
}

(Be careful: a lot of methods inside Quartz.Net are asynchronous but does not have the 'Async' suffix in their names)

The JobBuilder static class is used to wrap a job in a IJobDetail containing the job type and some metadata. In the other hand, the TriggerBuilder is used to create a trigger for jobs. In our example it is a simple trigger which will fire the job once directly after registering it to the scheduler.

From now, whenever you call the method StartTestJob(), you should see a Hello from a job! message in the console. You can call the service method from a simple HTTP GET controller method for testing purpose.

[HttpGet]
public void Get()
{
    _testService.StartTestJob();
}

Dependency injection

Next big step is to use dependency injection within our jobs. The solution I'll propose is pretty simple and straightforward. For this we are going to create a custom IJobFactory like this:

public class MyJobFactory : IJobFactory
{
    private readonly IServiceProvider _serviceProvider;

    public MyJobFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    
    public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
    {
        return ActivatorUtilities.CreateInstance(_serviceProvider, bundle.JobDetail.JobType) as IJob;
    }
    
    public void ReturnJob(IJob job)
    {
        if (job is IDisposable disposableJob)
            disposableJob.Dispose();
    }
}

And then add it to the scheduler:

var scheduler = StdSchedulerFactory.GetDefaultScheduler().GetAwaiter().GetResult();
scheduler.JobFactory = new MyJobFactory(services.BuildServiceProvider());
services.AddSingleton(scheduler);

As of now we can inject services via the job class' constructor like this:

public class TestJob : IJob
{
    private readonly ILogger<TestJob> _logger;
    
    public TestJob(ILogger<TestJob> logger)
    {
        _logger = logger;
    }
    
    public Task Execute(IJobExecutionContext context)
    {
        _logger.LogInformation("Hello from job logger!");
        return Task.CompletedTask;
    }
}

Then you should see the hello world message coming from the logger in the console:

Recurring and/or delayed job

To delay a job, you simply need to replace .StartNow() on the trigger builder with .StartAt(dateTimeOffset) . Example for starting a job after 15 minutes:

var trigger = TriggerBuilder
    .Create()
    .StartAt(DateTimeOffset.Now.AddMinutes(15))
    .Build();

There is two ways to create recurring jobs: using CRON expressions or the Quartz' SimpleScheduleBuilder with a fluent flow.

Example of a CRON trigger to fire a job every 5 minutes:

var trigger = TriggerBuilder
    .Create()
    .StartNow()
    .WithCronSchedule("*/5 * * * *")
    .Build();

Same trigger but using the SimpleScheduleBuilder :

var trigger = TriggerBuilder
    .Create()
    .StartNow()
    .WithSimpleSchedule(builder =>
        builder.WithIntervalInMinutes(5)
            .RepeatForever()
    )
    .Build();

To be precise, these two triggers will not work exactly the same. If you register the job at 6:03, the first trigger using CRON will fire the job "at every 5 minutes" like 6:05, 6:10, 6:15... But the second one will fire the job every 5 minutes starting from now, so 6:03, 6:08, 6:13... I invite you to read the Wikipedia page of CRON to learn more about it.

Hope you guys enjoyed it! Please comment if you have any question or suggestion.

What to do next?

You could try to save jobs in a database, so you won't lose them when the application stops. Quartz.Net has a persistence feature, so you can store your jobs in a database easily with multiple provider available (SqlServer, PostgreSQL, SQLite and more).

One another idea would be to make your jobs resilient (retry on error).