newsletter

Scheduling Jobs With Quartz and Database Persistence With EF Core Migrations

Download source code

6 min read

Newsletter Sponsors

Bulk Insert, Update, Delete & Merge — seamlessly built for EF Core.
Explore Entity Framework Extensions

There are multiple options for job scheduling in .NET.

The simplest one is creating a Background Service in ASP.NET Core with a static Periodic Timer.

But if you need more customization options and job persistence, you need a library.

My personal favourite is Quartz.NET.

It's a fully-featured, open-source job scheduling system that can be used from the smallest apps to large-scale enterprise systems. I prefer it over Hangfire because it provides more scheduling options.

In this post, I will show you:

  • How to add Quartz.NET to your ASP.NET Core application
  • How to schedule jobs with different trigger types
  • How to dynamically create jobs and triggers
  • How to use Quartz.NET with database persistence using EF Core migrations

Let's dive in.

Getting Started with Quartz.NET

To get started with Quartz.NET, you need to install the following packages:

bash
dotnet add package Quartz dotnet add package Quartz.Extensions.Hosting

Next, you need to register Quartz.NET in your DI container:

csharp
builder.Services.AddQuartz(q => { }); builder.Services.AddQuartzHostedService(options => { // When shutting down we want jobs to complete gracefully options.WaitForJobsToComplete = true; })

Next, you need to create a class that implements the IJob interface. Here is an example of a job that creates a report:

csharp
public record ReportCreationJob(ILogger<ReportCreationJob> Logger) : IJob { private readonly ILogger<CreateReportJob> _logger; private readonly ReportDbContext _dbContext; public CreateReportJob(ILogger<CreateReportJob> logger, ReportDbContext dbContext) { _logger = logger; _dbContext = dbContext; } public async Task Execute(IJobExecutionContext context) { _logger.LogInformation("Starting CreateReportJob at {Time}", DateTime.UtcNow); // Create a new report var report = new Report { Title = $"Scheduled Report - {DateTime.UtcNow:yyyy-MM-dd HH:mm}", Content = $"This is an automatically generated report created at {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}", CreatedAt = DateTime.UtcNow }; _dbContext.Reports.Add(report); await _dbContext.SaveChangesAsync(); _logger.LogInformation("CreateReportJob completed successfully. Created report with ID: {ReportId}", report.Id); } }

And finally, register the job inside AddQuartz method:

csharp
services.AddQuartz(q => { var jobKey = new JobKey("report-job"); q.AddJob<CreateReportJob>(opts => opts.WithIdentity(jobKey)); q.AddTrigger(opts => opts .ForJob(jobKey) .StartNow() .WithSimpleSchedule(x => x .WithIntervalInHours(1) .RepeatForever() ) ); });

Here we register a Job that will create reports every hour. StartNow method makes sure that the job will be executed immediately after the application starts.

Quartz.NET supports multiple trigger types. Let's explore them.

Trigger Types in Quartz.NET

SimpleTrigger

SimpleTrigger is the simplest trigger type.

You can schedule a job to run at a specific moment in time, with no repeats:

csharp
var trigger = TriggerBuilder.Create() .WithIdentity("report-job-trigger") // Start in 5 minutes .StartAt(DateBuilder.FutureDate(5, IntervalUnit.Minute)) .Build();

Here is how to schedule a job to run at a specific time and interval with 10 repeats:

csharp
var trigger = TriggerBuilder.Create() .WithIdentity("report-job-trigger") .StartNow() .WithSimpleSchedule(x => x .WithIntervalInMinutes(1) .WithRepeatCount(10)) .Build();

You can use RepeatForever method for infinite repeats.

Find more trigger examples in the official documentation.

DailyTimeIntervalTrigger

DailyTimeIntervalTrigger allows you to schedule a job using a daily time interval.

csharp
var trigger = TriggerBuilder.Create() .WithIdentity(nameof(PushNotificationsSendingJob)) .WithDailyTimeIntervalSchedule(s => s.WithIntervalInMinutes(interval) .OnEveryDay() .StartingDailyAt(TimeOfDay.HourAndMinuteOfDay(startTime.Hours, startTime.Minutes)) .EndingDailyAt(TimeOfDay.HourAndMinuteOfDay(endTime.Hours, endTime.Minutes)) .WithMisfireHandlingInstructionDoNothing() ) .Build();

CronTrigger

CronTrigger allows you to schedule a job using a cron expression.

csharp
var trigger = TriggerBuilder.Create() .WithIdentity("report-job-trigger") .WithCronSchedule("0 0 18 LW * ?") .Build();

Here is the template for cron expressions:

┌ sec (0‑59)
│ ┌ min (0‑59)
│ │ ┌ hour (0‑23)
│ │ │ ┌ day‑of‑month (1‑31)
│ │ │ │ ┌ month (1‑12 or JAN‑DEC)
│ │ │ │ │ ┌ day‑of‑week (0‑7 or SUN‑SAT; both 0 and 7 = SUN)
│ │ │ │ │ │ ┌ year (optional)
* * * * * ? *

  • "*" means every value.
  • "," means list.
  • "-" means range.
  • "/" means step.
  • "?" means no specific value.

Examples:

0 * * * * ? Every minute 0 0 18 LW * ? Last business day of month 18:00 0 15 2 1,15 * ? At 02:15 on the 1st & 15th 0/10 * 9 ? * * Every 10 sec between 09:00‑09:59

You can also use the following helper methods:

csharp
var trigger = TriggerBuilder.Create() .WithIdentity("report-job-trigger") .WithSchedule(CronScheduleBuilder .DailyAtHourAndMinute(11, 45) .WithMisfireHandlingInstructionFireAndProceed() ) // every day .Build(); var newTrigger = TriggerBuilder.Create() .WithIdentity("report-job-trigger") .WithSchedule(CronScheduleBuilder .MonthlyOnDayAndHourAndMinute(5, 23, 0) .WithMisfireHandlingInstructionFireAndProceed() ) // every 5th day of month .Build();

Dynamically Creating Jobs and Triggers

You can create jobs and triggers dynamically at runtime.

Here is how you can create a job and its trigger:

csharp
var job = JobBuilder.Create<CreateReportJob>() .WithIdentity("report-job") .Build(); var trigger = TriggerBuilder.Create() .WithIdentity("report-job-trigger") .StartAt(DateBuilder.FutureDate(5, IntervalUnit.Minute)) .Build();

You can use ISchedulerFactory to schedule a job. Each job can have multiple triggers.

csharp
var schedulerFactory = await app.Services.GetRequiredService<ISchedulerFactory>(); var scheduler = await schedulerFactory.GetScheduler(); await scheduler.ScheduleJob(job, new[] { trigger }, true);

Let's explore a real-world example. I have created a web api method to register new notifications from the frontend:

csharp
public class NotificationRequest { public DateTime ScheduledTime { get; set; } public string Title { get; set; } = string.Empty; public string Content { get; set; } = string.Empty; } app.MapPost("/api/schedule-notification", async (NotificationRequest request, IJobSchedulerService scheduler) => { if (request.ScheduledTime <= DateTime.Now) { return Results.BadRequest("Scheduled time must be in the future"); } var jobData = new Dictionary<string, object> { { NotificationJob.TitleKey, request.Title }, { NotificationJob.ContentKey, request.Content } }; var jobId = await scheduler.ScheduleJob<NotificationJob>(request.ScheduledTime, jobData); return Results.Ok(new { JobId = jobId, Message = $"Notification '{request.Title}' scheduled for {request.ScheduledTime}" }); });

I have created a IJobSchedulerService that registers the job with a specific time and parameters (Title and Content):

csharp
var scheduler = await _schedulerFactory.GetScheduler(); // Generate a unique job ID var jobId = Guid.NewGuid().ToString(); // Create the job and add job data var jobBuilder = JobBuilder.Create<T>() .WithIdentity(jobId); // Add job data jobBuilder.UsingJobData(new JobDataMap(jobData)); var jobDetail = jobBuilder.Build(); // Create the trigger to run at the specified time var trigger = TriggerBuilder.Create() .WithIdentity($"{jobId}-trigger") .StartAt(new DateTimeOffset(scheduledTime)) .Build(); // Schedule the job await scheduler.ScheduleJob(jobDetail, trigger);

You can download the full source code at the start and the end of the post

So far, so good, but here is a problem.

By default, Quartz.NET stores jobs and triggers data in memory. If you restart the application, all jobs will be lost.

And this is not something you want in production.

Instead, you can persist jobs in the database. Let's have a look.

Persisting Jobs in the Database

Quartz.NET supports multiple database providers, including SQL Server, MySQL, PostgreSQL, and SQLite.

First, you need to add a Nuget package to serialize job's data:

bash
dotnet add package Quartz.Serialization.Json

You need to register the database provider in your DI container:

csharp
builder.Services.AddQuartz(static options => { q.UsePersistentStore(c => { c.RetryInterval = TimeSpan.FromMinutes(2); c.UseProperties = true; c.PerformSchemaValidation = true; c.UseNewtonsoftJsonSerializer(); c.UsePostgres(postgres => { postgres.ConnectionString = configuration.GetConnectionString("Postgres")!; postgres.TablePrefix = $"{DbConsts.SchemaName}.qrtz_"; postgres.UseDriverDelegate<PostgreSQLDelegate>(); }); }); });

RetryInterval is the time to wait before scanning the database for new jobs.

Now we need to create a database schema for Quartz.NET. We can use EF Core Migrations to simplify the process.

Using EF Core Migrations to Create Database Schema for Quartz.NET

Quartz.NET provides a database schema for each database provider. You can use EF Core migrations to create the database schema for Quartz.NET.

First, you need to add one of the following packages depending on your database type:

bash
dotnet add package AppAny.Quartz.EntityFrameworkCore.Migrations.PostgreSQL dotnet add package AppAny.Quartz.EntityFrameworkCore.Migrations.MySql dotnet add package AppAny.Quartz.EntityFrameworkCore.Migrations.SQLite dotnet add package AppAny.Quartz.EntityFrameworkCore.Migrations.SqlServer

These are community packages. I have used the PostgreSQL one in production without any issues.

Next, you need to add migrations to your DbContext:

csharp
public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : DbContext(options) { protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // Adds Quartz.NET PostgreSQL schema to EntityFrameworkCore modelBuilder.AddQuartz(options => options.UsePostgreSql()); } }

Quartz tables are created when you execute your migrations, for example, you can run them on Application startup:

csharp
using (var scope = app.Services.CreateScope()) { var dbContext = scope.ServiceProvider.GetRequiredService<ReportDbContext>(); await dbContext.Database.MigrateAsync(); }

Note: for production this might not be the most suitable option for running migrations

Now, you can run your application and see that jobs are persisted in the database.

Screenshot_1

Screenshot_2

Summary

Quartz.NET is a powerful library for scheduling jobs in .NET applications. It provides a simple API for creating and managing jobs and triggers. You can use it to schedule jobs that run at specific times or intervals.

Quartz.NET also supports persisting jobs in the database. This allows you to schedule jobs that run even if the application is restarted.

EF Core migrations are a great way to create the database schema for Quartz.NET.

Hope you find this newsletter useful. See you next time.

You can download source code for this newsletter for free

Download source code

Improve Your .NET and Architecture Skills

Join my community of 8,700+ developers and architects.

Each week you will get 1 practical tip with best practices and real-world examples.

Learn how to craft better software with source code available for my newsletter.

Join 8,700+ developers already reading
No spam. Unsubscribe any time.