Complete Guide to Quartz.NET Job Scheduling in .NET 8
If you’re new to background processing in .NET 8, you’ve likely encountered the “Where do I run my periodic tasks?” dilemma. Whether you’re automating report generation, cleaning up stale data, or orchestrating a data pipeline, you need a robust, flexible scheduler that works seamlessly with the modern .NET runtime. Quartz.NET is the de‑facto standard for advanced job scheduling in the .NET ecosystem, and with .NET 8’s minimal APIs, hosted services, and built‑in DI, setting up Quartz has never been easier.
In this complete guide, we walk you through everything you need to get a Quartz.NET scheduler up and running in a .NET 8 application. By the end of this post you’ll be able to:
- Install and configure Quartz.NET via NuGet
- Create a simple job that writes to the console
- Configure various trigger types – Simple, Cron, and Daily
- Leverage .NET 8 features such as minimal APIs and background services
- Understand best practices for dependency injection and graceful shutdown
Let’s dive in!
Table of Contents
- Prerequisites
- Step 1: Install Quartz.NET
- Step 2: Create a .NET 8 Minimal API Project
- Step 3: Configure Quartz in
Program.cs - Step 4: Define Your First Job
- Step 5: Add Triggers – Simple, Cron, and Daily
- Step 6: Running the Scheduler
- Step 7: Advanced Topics
- Best Practices & Tips
- Related Posts
- Conclusion
Prerequisites
- .NET 8 SDK installed (download from the .NET website)
- Visual Studio 17 + or your favorite IDE (VS Code, Rider, etc.)
- Basic understanding of C# and asynchronous programming
- Git account (optional, but recommended for version control)
Tip: If you’re still on an earlier .NET version, consider upgrading to .NET 8 for its performance improvements and minimal API support – Quartz.NET fully supports .NET 8 out of the box.
Step 1: Install Quartz.NET
The easiest way to add Quartz.NET to your project is via the NuGet package manager. For .NET 8 minimal APIs, we recommend the Quartz.Extensions.Hosting package because it integrates tightly with the IHostedService pattern.
dotnet add package Quartz.Extensions.Hosting
You’ll also need the core Quartz package, which the extensions package brings in as a transitive dependency. To verify the installation, check your .csproj file:
<ItemGroup>
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.4.2" />
</ItemGroup>
Why
Quartz.Extensions.Hosting?
The hosting package simplifies adding Quartz to ASP.NET Core and minimal API projects, automatically registersIScheduleras a singleton, and exposes the scheduler as a hosted background service.
Step 2: Create a .NET 8 Minimal API Project
Let’s scaffold a minimal API to host our scheduler. In a terminal, run:
dotnet new web -n QuartzDemo -f net8.0
cd QuartzDemo
Open the newly created Program.cs. It already contains a simple GET / endpoint. We’ll repurpose it to add our Quartz configuration.
Step 3: Configure Quartz in Program.cs
Quartz.NET can be configured via code or appsettings.json. For clarity, we’ll use code‑first configuration in this guide.
using Quartz;
using Quartz.Impl;
using Quartz.Spi;
using Quartz.Extensions.Hosting;
var builder = WebApplication.CreateBuilder(args);
// Add Quartz.NET services
builder.Services.AddQuartz(q =>
{
// Use simple scheduler factory (in-memory storage for demo)
q.UseSimpleTypeLoader();
q.UseInMemoryStore();
q.UseDefaultThreadPool(tp =>
{
tp.MaxConcurrency = 5; // Limit concurrent job execution
});
// Register job types
q.AddJob<MyJob>(opts => opts.WithIdentity("myJob", "demoGroup"));
});
builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
var app = builder.Build();
app.MapGet("/", () => "Quartz.NET Scheduler running in .NET 8!");
// Run the app
app.Run();
Explanation
UseInMemoryStore()keeps job data in RAM, suitable for demonstration and dev. For production, switch to a persistent store (SQL Server, PostgreSQL, etc.).AddJob<MyJob>registers the job type and assigns it an identity.AddQuartzHostedServiceregisters Quartz as a hosted service, ensuring it starts with the app and stops gracefully.
Step 4: Define Your First Job
A Quartz job implements IJob. It can be a simple class:
using Quartz;
using System.Text;
public class MyJob : IJob
{
public Task Execute(IJobExecutionContext context)
{
var sb = new StringBuilder();
sb.AppendLine($"Job executed at {DateTime.UtcNow:O}");
sb.AppendLine($"Trigger: {context.Trigger.Key}");
sb.AppendLine($"Job Data Map: {context.JobDetail.JobDataMap}");
// Simulate work
Console.WriteLine(sb.ToString());
return Task.CompletedTask;
}
}
Key Points
IJobmethods areasync‑compatible; returnTaskorTask<T>.- Use
contextto access metadata (trigger key, job data map, scheduler). - Keep jobs lightweight; heavy logic should be delegated to services injected via DI.
Step 5: Add Triggers – Simple, Cron, and Daily
A trigger defines when a job should fire. Quartz offers several trigger types. In .NET 8, we can add them programmatically or via configuration. Below we’ll showcase three popular patterns.
5.1 Simple Trigger
Triggers a job a specific number of times at fixed intervals.
var simpleTrigger = TriggerBuilder.Create()
.WithIdentity("simpleTrigger", "demoGroup")
.ForJob("myJob", "demoGroup") // Link to job key
.StartNow()
.WithSimpleSchedule(x => x
.WithIntervalInSeconds(10)
.RepeatForever())
.Build();
await scheduler.ScheduleJob(simpleTrigger);
Result: MyJob runs every 10 seconds indefinitely.
5.2 Cron Trigger
Cron triggers use standard cron expressions. .NET 8’s System.TimeZoneInfo improvements help with timezone‑aware schedules.
var cronTrigger = TriggerBuilder.Create()
.WithIdentity("cronTrigger", "demoGroup")
.ForJob("myJob", "demoGroup")
.StartNow()
.WithCronSchedule("0 30 2 * * ?", // Every day at 02:30 UTC
x => x.InTimeZone(TimeZoneInfo.Utc))
.Build();
await scheduler.ScheduleJob(cronTrigger);
Tip: Use the Cron expression tester to validate expressions before deploying.
5.3 Daily Trigger (Time Window)
If you need a job to fire during a specific daily window, combine a cron expression with a DailyTimeIntervalTrigger.
var dailyTrigger = TriggerBuilder.Create()
.WithIdentity("dailyTrigger", "demoGroup")
.ForJob("myJob", "demoGroup")
.StartNow()
.WithDailyTimeIntervalSchedule(x => x
.OnEveryDay()
.StartingDailyAt(TimeOfDay.HourAndMinuteOfDay(6, 0)) // 06:00
.EndingDailyAt(TimeOfDay.HourAndMinuteOfDay(18, 0))) // 18:00
.Build();
await scheduler.ScheduleJob(dailyTrigger);
Result: The job runs every minute between 06:00 and 18:00 UTC.
Step 6: Running the Scheduler
Once you’ve defined jobs and triggers, start the scheduler:
var scheduler = await schedulerFactory.GetScheduler();
await scheduler.Start();
If you’re using Quartz.Extensions.Hosting, this is handled automatically. In a console app, you would call scheduler.Start() manually.
Graceful Shutdown
In .NET 8, the hosted service’sWaitForJobsToCompleteflag ensures the application waits for any in‑flight jobs before shutting down. For background services that rely on DI, register them normally viabuilder.Services.AddSingleton<...>().
Step 7: Advanced Topics
Beyond the basics, Quartz.NET supports a wealth of advanced features:
| Feature | What It Does | Why It Matters |
|---|---|---|
| Persisted Job Store | UseSqlServer() / UsePostgreSql() |
Durable scheduling for production |
| Job Misfire Handling | WithMisfireHandlingInstructionFireNow() |
Prevents missed executions |
| Clustered Scheduler | UseCluster() |
Scale across multiple instances |
| Job Chaining | JobBuilder.WithJobData(...) |
Run dependent jobs in sequence |
| Stateful (Disallow Concurrent Execution) | [DisallowConcurrentExecution] attribute |
Avoid race conditions |
Pro‑Tip: In .NET 8, you can now host Quartz alongside
BackgroundService‑derived classes. This means you can inject domain services into jobs, use MediatR, or even run CQRS commands—all while keeping your job logic decoupled.
Best Practices & Tips
| Practice | How to Apply |
|---|---|
| Use Persistent Stores | Replace UseInMemoryStore() with UseSqlServer() or UseMongoDb() for real‑world durability. |
| Keep Jobs Idempotent | Quartz may re‑trigger jobs during cluster rebalancing; design your job logic to handle duplicates safely. |
| Leverage DI | Inject services into your job’s constructor via a custom IJobFactory. |
Set MaxConcurrency |
Tune the thread pool based on expected load to avoid resource exhaustion. |
| Configure Time Zones | Explicitly set the timezone in WithCronSchedule or WithDailyTimeIntervalSchedule to avoid daylight‑saving headaches. |
| Monitor Scheduler Health | Expose a health endpoint (e.g., /health/quartz) that checks scheduler.IsStarted and scheduler.InStandbyMode. |
| Graceful Shutdown | Set WaitForJobsToComplete = true so that the host waits for jobs to finish before stopping. |
Conclusion
Setting up a Quartz.NET scheduler in a .NET 8 application is straightforward, thanks to the Quartz.Extensions.Hosting package and minimal API patterns. By installing the package, registering jobs and triggers, and leveraging .NET 8’s built‑in DI and timezone handling, you can focus on the business logic that truly matters.
Whether you’re automating routine tasks, orchestrating complex workflows, or building a scalable microservice, Quartz.NET offers the flexibility and reliability you need. Give it a try in your next .NET 8 project and watch your background jobs run smoothly and predictably.