mcpField
D
dotnet-quartz-background-jobs

dotnet-quartz-background-jobs

github.com/ronnythedev开发7762 次安装15 周前更新
代码仓库

关于 dotnet-quartz-background-jobs

面向 .NET 开发者的 Quartz.NET 后台作业技能,提供作业定义、注册与调度模板,并涵盖周期、Cron 及运行注意事项。

主要功能

  1. 01间隔与 Cron 调度模板
  2. 02作业注册与依赖注入
  3. 03JobDataMap 数据传递
  4. 04重试与动态调度示例
  5. 05Cron 表达式参考

适用场景

  1. 01为应用添加定时后台任务
  2. 02按 Cron 规则生成报告
  3. 03传递作业参数或配置重试
  4. 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));
    }
}

关键规则

  1. 使用 [DisallowConcurrentExecution] - 防止任务运行重叠
  2. 正确处理异常 - 记录日志并确定重试策略
  3. 使用 CancellationToken - 从 context.CancellationToken 获取
  4. 保持任务专注 - 每个任务只负责一项职责
  5. 在生产环境中使用持久化存储 - 重启后任务仍会保留
  6. 时区很重要 - 为 Cron 触发器指定时区
  7. 监控任务执行情况 - 记录开始时间、结束时间和持续时长
  8. 不要阻塞线程 - 使用 async/await
  9. 注入作用域服务 - 每次执行都会创建新的作用域
  10. 单独测试任务逻辑 - 将逻辑提取到可测试的服务中

应避免的反模式

// ❌ 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