关于 dotnet-quartz-background-jobs
面向 .NET 开发者的 Quartz.NET 后台作业技能,提供作业定义、注册与调度模板,并涵盖周期、Cron 及运行注意事项。
主要功能
- 01间隔与 Cron 调度模板
- 02作业注册与依赖注入
- 03JobDataMap 数据传递
- 04重试与动态调度示例
- 05Cron 表达式参考
适用场景
- 01为应用添加定时后台任务
- 02按 Cron 规则生成报告
- 03传递作业参数或配置重试
- 04运行时动态安排作业
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ ronnythedev/ dotnet-clean-architecture-skills --skill dotnet-quartz-background-jobs安装后在 Claude Code 中输入/dotnet-quartz-background-jobs确认已加载。
详细介绍
后台任务生成器(Quartz)
概述
Quartz.NET 是一个功能齐全的任务调度库:
- 任务调度 - 在指定时间或按固定间隔运行任务
- Cron 表达式 - 复杂的调度模式
- 持久化 - 应用程序重启后任务仍会保留
- 依赖注入 - 全面支持 DI
- 集群 - 分布式任务执行
快速参考
| 组件 | 用途 |
|---|---|
IJob | 需要实现的任务接口 |
IConfigureOptions<QuartzOptions> | 任务注册 |
JobKey | 唯一任务标识符 |
TriggerBuilder | 定义任务运行时间 |
CronScheduleBuilder | 基于 Cron 的调度 |
SimpleScheduleBuilder | 基于间隔的调度 |
任务结构
/Infrastructure/
├── BackgroundJobs/
│ ├── {JobName}Job.cs
│ ├── {JobName}JobSetup.cs
│ └── ...
└── DependencyInjection.cs
模板:简单间隔任务
// src/{name}.infrastructure/BackgroundJobs/ProcessPendingOrdersJob.cs
using Microsoft.Extensions.Logging;
using Quartz;
namespace {name}.infrastructure.backgroundjobs;
/// <summary>
/// Processes pending orders every 5 minutes
/// </summary>
[DisallowConcurrentExecution] // Prevent overlapping executions
public sealed class ProcessPendingOrdersJob : IJob
{
private readonly IOrderRepository _orderRepository;
private readonly IOrderProcessor _orderProcessor;
private readonly ILogger<ProcessPendingOrdersJob> _logger;
public ProcessPendingOrdersJob(
IOrderRepository orderRepository,
IOrderProcessor orderProcessor,
ILogger<ProcessPendingOrdersJob> logger)
{
_orderRepository = orderRepository;
_orderProcessor = orderProcessor;
_logger = logger;
}
public async Task Execute(IJobExecutionContext context)
{
_logger.LogInformation("Starting pending orders processing...");
try
{
var pendingOrders = await _orderRepository
.GetPendingOrdersAsync(context.CancellationToken);
_logger.LogInformation(
"Found {Count} pending orders to process",
pendingOrders.Count);
foreach (var order in pendingOrders)
{
try
{
await _orderProcessor.ProcessAsync(order, context.CancellationToken);
_logger.LogInformation(
"Processed order {OrderId}",
order.Id);
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Failed to process order {OrderId}",
order.Id);
}
}
_logger.LogInformation("Completed pending orders processing");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in pending orders processing job");
throw; // Quartz will handle retry based on configuration
}
}
}
模板:任务设置(IConfigureOptions)
// src/{name}.infrastructure/BackgroundJobs/ProcessPendingOrdersJobSetup.cs
using Microsoft.Extensions.Options;
using Quartz;
namespace {name}.infrastructure.backgroundjobs;
internal sealed class ProcessPendingOrdersJobSetup
: IConfigureOptions<QuartzOptions>
{
public void Configure(QuartzOptions options)
{
var jobKey = JobKey.Create(nameof(ProcessPendingOrdersJob));
options
.AddJob<ProcessPendingOrdersJob>(jobBuilder =>
jobBuilder
.WithIdentity(jobKey)
.WithDescription("Processes pending orders"))
.AddTrigger(triggerBuilder =>
triggerBuilder
.ForJob(jobKey)
.WithIdentity($"{nameof(ProcessPendingOrdersJob)}-trigger")
.WithSimpleSchedule(schedule =>
schedule
.WithIntervalInMinutes(5)
.RepeatForever())
.StartNow());
}
}
模板:Cron 调度任务
// src/{name}.infrastructure/BackgroundJobs/DailyReportJob.cs
using Microsoft.Extensions.Logging;
using Quartz;
namespace {name}.infrastructure.backgroundjobs;
/// <summary>
/// Generates daily reports at 6:00 AM every day
/// </summary>
[DisallowConcurrentExecution]
public sealed class DailyReportJob : IJob
{
private readonly IReportService _reportService;
private readonly IEmailService _emailService;
private readonly ILogger<DailyReportJob> _logger;
public DailyReportJob(
IReportService reportService,
IEmailService emailService,
ILogger<DailyReportJob> logger)
{
_reportService = reportService;
_emailService = emailService;
_logger = logger;
}
public async Task Execute(IJobExecutionContext context)
{
_logger.LogInformation("Starting daily report generation...");
var reportDate = DateTime.UtcNow.Date.AddDays(-1);
var report = await _reportService.GenerateDailyReportAsync(
reportDate,
context.CancellationToken);
await _emailService.SendReportAsync(
report,
context.CancellationToken);
_logger.LogInformation(
"Daily report for {Date} sent successfully",
reportDate.ToShortDateString());
}
}
// src/{name}.infrastructure/BackgroundJobs/DailyReportJobSetup.cs
using Microsoft.Extensions.Options;
using Quartz;
namespace {name}.infrastructure.backgroundjobs;
internal sealed class DailyReportJobSetup : IConfigureOptions<QuartzOptions>
{
public void Configure(QuartzOptions options)
{
var jobKey = JobKey.Create(nameof(DailyReportJob));
options
.AddJob<DailyReportJob>(jobBuilder =>
jobBuilder
.WithIdentity(jobKey)
.WithDescription("Daily report generation"))
.AddTrigger(triggerBuilder =>
triggerBuilder
.ForJob(jobKey)
.WithIdentity($"{nameof(DailyReportJob)}-trigger")
.WithCronSchedule(
"0 0 6 * * ?", // 6:00 AM every day
builder => builder.InTimeZone(TimeZoneInfo.Utc))
.WithDescription("Fires at 6:00 AM UTC daily"));
}
}
Cron 表达式参考
| 表达式 | 描述 |
|---|---|
0 0 * * * ? | 每小时的第 0 分钟 |
0 0/15 * * * ? | 每 15 分钟 |
0 0 6 * * ? | 每天上午 6:00 |
0 0 6 ? * MON-FRI | 工作日上午 6:00 |
0 0 0 1 * ? | 每月 1 日午夜 |
0 0 0 L * ? | 每月最后一天午夜 |
0 0 12 ? * SUN | 每周日中午 |
格式:seconds minutes hours day-of-month month day-of-week [year]
| 字段 | 值 |
|---|---|
| 秒 | 0-59 |
| 分钟 | 0-59 |
| 小时 | 0-23 |
| 月份中的日期 | 1-31、L(最后一天)、W(工作日) |
| 月份 | 1-12 或 JAN-DEC |
| 星期 | 1-7 或 SUN-SAT、L(最后一个) |
| 年份 | 可选,1970-2099 |
特殊字符:
*- 所有值?- 不指定具体值(月份中的日期/星期)-- 范围(例如,MON-FRI),- 列表(例如,MON,WED,FRI)/- 增量(例如,0/15= 每 15 个单位)L- 最后(例如,每月最后一天)W- 最接近的工作日#- 第 N 个星期几(例如,2#3= 第三个星期一)
模板:使用数据映射的任务
// src/{name}.infrastructure/BackgroundJobs/SendScheduledEmailJob.cs
using Microsoft.Extensions.Logging;
using Quartz;
namespace {name}.infrastructure.backgroundjobs;
/// <summary>
/// Sends a scheduled email using data from JobDataMap
/// </summary>
public sealed class SendScheduledEmailJob : IJob
{
public const string EmailIdKey = "EmailId";
public const string RecipientKey = "Recipient";
private readonly IEmailService _emailService;
private readonly ILogger<SendScheduledEmailJob> _logger;
public SendScheduledEmailJob(
IEmailService emailService,
ILogger<SendScheduledEmailJob> logger)
{
_emailService = emailService;
_logger = logger;
}
public async Task Execute(IJobExecutionContext context)
{
// Get data from job data map
var dataMap = context.MergedJobDataMap;
var emailId = dataMap.GetGuid(EmailIdKey);
var recipient = dataMap.GetString(RecipientKey);
_logger.LogInformation(
"Sending scheduled email {EmailId} to {Recipient}",
emailId,
recipient);
await _emailService.SendScheduledEmailAsync(
emailId,
context.CancellationToken);
}
}
// Scheduling the job with data
public class EmailScheduler
{
private readonly ISchedulerFactory _schedulerFactory;
public async Task ScheduleEmailAsync(
Guid emailId,
string recipient,
DateTime sendAt)
{
var scheduler = await _schedulerFactory.GetScheduler();
var jobKey = new JobKey($"email-{emailId}", "scheduled-emails");
var job = JobBuilder.Create<SendScheduledEmailJob>()
.WithIdentity(jobKey)
.UsingJobData(SendScheduledEmailJob.EmailIdKey, emailId.ToString())
.UsingJobData(SendScheduledEmailJob.RecipientKey, recipient)
.Build();
var trigger = TriggerBuilder.Create()
.WithIdentity($"email-{emailId}-trigger", "scheduled-emails")
.StartAt(sendAt)
.Build();
await scheduler.ScheduleJob(job, trigger);
}
}
模板:带重试逻辑的任务
// src/{name}.infrastructure/BackgroundJobs/SyncExternalDataJob.cs
using Microsoft.Extensions.Logging;
using Quartz;
namespace {name}.infrastructure.backgroundjobs;
/// <summary>
/// Syncs data from external API with retry support
/// </summary>
[DisallowConcurrentExecution]
[PersistJobDataAfterExecution] // Persist data map changes
public sealed class SyncExternalDataJob : IJob
{
private const int MaxRetries = 3;
private const string RetryCountKey = "RetryCount";
private readonly IExternalApiClient _apiClient;
private readonly IDataSyncService _syncService;
private readonly ILogger<SyncExternalDataJob> _logger;
public SyncExternalDataJob(
IExternalApiClient apiClient,
IDataSyncService syncService,
ILogger<SyncExternalDataJob> logger)
{
_apiClient = apiClient;
_syncService = syncService;
_logger = logger;
}
public async Task Execute(IJobExecutionContext context)
{
var retryCount = context.MergedJobDataMap.GetInt(RetryCountKey);
try
{
_logger.LogInformation(
"Starting external data sync (attempt {Attempt})",
retryCount + 1);
var data = await _apiClient.FetchDataAsync(context.CancellationToken);
await _syncService.SyncAsync(data, context.CancellationToken);
// Reset retry count on success
context.JobDetail.JobDataMap.Put(RetryCountKey, 0);
_logger.LogInformation("External data sync completed successfully");
}
catch (Exception ex)
{
_logger.LogError(
ex,
"External data sync failed (attempt {Attempt} of {MaxRetries})",
retryCount + 1,
MaxRetries);
if (retryCount < MaxRetries - 1)
{
// Increment retry count
context.JobDetail.JobDataMap.Put(RetryCountKey, retryCount + 1);
// Throw to trigger Quartz retry
throw new JobExecutionException(ex, refireImmediately: false);
}
else
{
// Max retries reached, log and don't retry
_logger.LogCritical(
"External data sync failed after {MaxRetries} attempts. Manual intervention required.",
MaxRetries);
context.JobDetail.JobDataMap.Put(RetryCountKey, 0);
}
}
}
}
模板:Quartz 注册
// src/{name}.infrastructure/DependencyInjection.cs
using Quartz;
using Microsoft.Extensions.DependencyInjection;
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration configuration)
{
// ... other registrations
AddBackgroundJobs(services, configuration);
return services;
}
private static void AddBackgroundJobs(
IServiceCollection services,
IConfiguration configuration)
{
services.AddQuartz(configure =>
{
// ═══════════════════════════════════════════════════════════════
// IN-MEMORY STORE (Development)
// ═══════════════════════════════════════════════════════════════
configure.UseInMemoryStore();
// ═══════════════════════════════════════════════════════════════
// PERSISTENT STORE (Production - uncomment for production)
// ═══════════════════════════════════════════════════════════════
// configure.UsePersistentStore(store =>
// {
// store.UsePostgres(configuration.GetConnectionString("Database")!);
// store.UseJsonSerializer();
// store.PerformSchemaValidation = true;
// });
// ═══════════════════════════════════════════════════════════════
// CLUSTERING (Multi-instance - uncomment for distributed)
// ═══════════════════════════════════════════════════════════════
// configure.UsePersistentStore(store =>
// {
// store.UsePostgres(configuration.GetConnectionString("Database")!);
// store.UseJsonSerializer();
// store.UseClustering(cluster =>
// {
// cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(20);
// cluster.CheckinInterval = TimeSpan.FromSeconds(10);
// });
// });
});
// Register hosted service
services.AddQuartzHostedService(options =>
{
options.WaitForJobsToComplete = true;
options.AwaitApplicationStarted = true;
});
// Register job setups
services.ConfigureOptions<ProcessPendingOrdersJobSetup>();
services.ConfigureOptions<DailyReportJobSetup>();
services.ConfigureOptions<SyncExternalDataJobSetup>();
}
}
模板:任务调度器服务
// src/{name}.infrastructure/BackgroundJobs/JobSchedulerService.cs
using Quartz;
namespace {name}.infrastructure.backgroundjobs;
/// <summary>
/// Service for dynamically scheduling jobs at runtime
/// </summary>
public interface IJobSchedulerService
{
Task ScheduleJobAsync<TJob>(DateTime runAt, JobDataMap? data = null)
where TJob : IJob;
Task ScheduleJobAsync<TJob>(TimeSpan delay, JobDataMap? data = null)
where TJob : IJob;
Task CancelJobAsync(string jobName, string groupName);
Task<bool> IsJobScheduledAsync(string jobName, string groupName);
}
internal sealed class JobSchedulerService : IJobSchedulerService
{
private readonly ISchedulerFactory _schedulerFactory;
public JobSchedulerService(ISchedulerFactory schedulerFactory)
{
_schedulerFactory = schedulerFactory;
}
public async Task ScheduleJobAsync<TJob>(DateTime runAt, JobDataMap? data = null)
where TJob : IJob
{
var scheduler = await _schedulerFactory.GetScheduler();
var jobName = $"{typeof(TJob).Name}-{Guid.NewGuid()}";
var jobBuilder = JobBuilder.Create<TJob>()
.WithIdentity(jobName, "dynamic-jobs");
if (data is not null)
{
jobBuilder.UsingJobData(data);
}
var job = jobBuilder.Build();
var trigger = TriggerBuilder.Create()
.WithIdentity($"{jobName}-trigger", "dynamic-jobs")
.StartAt(runAt)
.Build();
await scheduler.ScheduleJob(job, trigger);
}
public async Task ScheduleJobAsync<TJob>(TimeSpan delay, JobDataMap? data = null)
where TJob : IJob
{
await ScheduleJobAsync<TJob>(DateTime.UtcNow.Add(delay), data);
}
public async Task CancelJobAsync(string jobName, string groupName)
{
var scheduler = await _schedulerFactory.GetScheduler();
await scheduler.DeleteJob(new JobKey(jobName, groupName));
}
public async Task<bool> IsJobScheduledAsync(string jobName, string groupName)
{
var scheduler = await _schedulerFactory.GetScheduler();
return await scheduler.CheckExists(new JobKey(jobName, groupName));
}
}
关键规则
- 使用 [DisallowConcurrentExecution] - 防止任务运行重叠
- 正确处理异常 - 记录日志并确定重试策略
- 使用 CancellationToken - 从
context.CancellationToken获取 - 保持任务专注 - 每个任务只负责一项职责
- 在生产环境中使用持久化存储 - 重启后任务仍会保留
- 时区很重要 - 为 Cron 触发器指定时区
- 监控任务执行情况 - 记录开始时间、结束时间和持续时长
- 不要阻塞线程 - 使用 async/await
- 注入作用域服务 - 每次执行都会创建新的作用域
- 单独测试任务逻辑 - 将逻辑提取到可测试的服务中
应避免的反模式
// ❌ WRONG: Long-running synchronous code
public Task Execute(IJobExecutionContext context)
{
Thread.Sleep(60000); // Don't block!
return Task.CompletedTask;
}
// ✅ CORRECT: Async operations
public async Task Execute(IJobExecutionContext context)
{
await Task.Delay(60000, context.CancellationToken);
}
// ❌ WRONG: Swallowing exceptions silently
public async Task Execute(IJobExecutionContext context)
{
try { await DoWork(); }
catch { } // Silent failure, no logging!
}
// ✅ CORRECT: Log and handle exceptions
public async Task Execute(IJobExecutionContext context)
{
try { await DoWork(); }
catch (Exception ex)
{
_logger.LogError(ex, "Job failed");
throw; // Let Quartz handle retry
}
}
// ❌ WRONG: Ignoring cancellation
public async Task Execute(IJobExecutionContext context)
{
foreach (var item in items)
{
await ProcessItem(item); // Ignores shutdown signal
}
}
// ✅ CORRECT: Respect cancellation
public async Task Execute(IJobExecutionContext context)
{
foreach (var item in items)
{
context.CancellationToken.ThrowIfCancellationRequested();
await ProcessItem(item, context.CancellationToken);
}
}
相关技能
dotnet-outbox-pattern- Outbox 处理器任务email-service- 定时邮件任务dotnet-clean-architecture- 基础设施层设置
评论
支持 Markdown