资讯动态

Quartz.NET + .NET Aspire AppHost 实战:声明式编排、持久化 Postgres 作业库与密码固定策略

发布时间:2026/10/8 19:18:31 来源:尧图企业网站定制
任务调度后端【免费下载链接】quartznetQuartz Enterprise Scheduler .NET项目地址https://gitcode.com/gh_mirrors/qu/quartznet点击查看免费下载导读src/Quartz.Examples.Aspire.AppHost是 Quartz.NET 仓库中展示「如何让 Quartz 调度器跑在 .NET Aspire 编排环境下」的最小可运行示例一个 AppHost 只声明了 PostgreSQL 服务器、它上面的quartz数据库和调度 worker 三个资源一行dotnet run即可拉起整套环境并在 dashboard 上观察调度器的运行。读完本文你将掌握 Aspire AppHost 的项目结构、WithReference/WaitFor/WithHttpHealthCheck的资源接线方式理解「持久化容器 自动生成密码」这一经典坑位的成因与修复并学会运行后如何清理遗留的容器与数据卷。两个项目、一条命令示例的全貌这个示例由两个项目组成位于仓库的src/下AppHostQuartz.Examples.Aspire.AppHostAspire 的编排层orchestrator。它声明这个示例需要的两个资源——一台带数据库的 PostgreSQL 服务器、以及一个对着该数据库做调度的 worker——除此之外什么都不做。WorkerQuartz.Examples.Aspire.Worker真正承载 Quartz 调度器的应用注册持久化作业库、调度一个每 10 秒触发一次的HeartbeatJob并暴露/health端点供 AppHost 轮询。两者的分工非常清晰AppHost 只回答「环境长什么样」有哪些数据库、哪些应用、谁依赖谁Worker 只回答「调度器怎么配」用哪个连接、建什么表、跑什么作业。这也是 .NET Aspire 的核心心智模型——声明式编排与业务实现解耦。AppHost 项目的 csproj 只有两个包引用和一句关键的项目引用ItemGroup PackageReference IncludeAspire.Hosting.AppHost / PackageReference IncludeAspire.Hosting.PostgreSQL / /ItemGroup ItemGroup ProjectReference Include..\Quartz.Examples.Aspire.Worker\Quartz.Examples.Aspire.Worker.csproj / /ItemGroup注意它没有引用任何 Quartz 包——Quartz 只存在于 worker 一侧AppHost 对调度器一无所知它只需要知道「有一个名为 worker 的应用要用 quartz 这个数据库」就够了。这正是 Aspire 的松耦合设计编排层不关心业务代码只负责把连接字符串和启动顺序交付给下游。运行它一行 dotnet run不需要 aspire CLIAppHost 的 README 给出的启动方式只有一条命令dotnet run --project src/Quartz.Examples.Aspire.AppHost前提必须有一个正在运行的容器运行时——Docker Desktop 或 Podman——因为 AppHost 会启动一个 PostgreSQL 容器。关键点在于aspireCLI 和 Aspire workload 都不需要。原因是 csproj 中使用了 SDK 元素形式引用了Aspire.AppHost.SdkSdk NameAspire.AppHost.Sdk Version$(AspireVersion) /Aspire.AppHost.Sdk会把 dashboard 和 Developer Control PlaneDCP作为包载荷package payload带进来所以普通的dotnet run就足够把整个编排环境跑起来。csproj 中的注释还解释了为什么这里刻意不设置SkipAddAspireDefaultReferences设为true可以去掉那约 57 MB解压后半 GB的运行时载荷但代价是 AppHost 无法再dotnet run——a sample nobody can run is half a sample没人能运行的示例只是半个示例所以示例项目故意保持默认值。另外值得注意的工程细节SDK 版本来自$(AspireVersion)属性定义在 Directory.Packages.props这之所以可行是因为这里用的是Sdk元素形式MSBuild 会把它作为项目主体的一部分求值如果写成Project SdkAspire.AppHost.Sdk/$(AspireVersion)的属性形式MSBuild 会在任何属性存在之前解析它从而去找一个按未展开字符串命名的包。AppHost 项目还压制了ASPIRE010诊断——它提示 Aspire CLI bundle 未在使用所指向的功能aspire run、aspire publish、bundle 自带的 dashboard 和 DCP全是「运行」相关的能力而本仓库没有任何 CI 环节真正运行 AppHost因此该警告在三条流水线上都只是噪音。启动之后你能看到什么命令运行后控制台会打印一个 dashboard URL。打开它你会看到postgres资源一个 PostgreSQL 服务器容器quartz数据库挂在 postgres 服务器上的数据库资源workerAppHost 持续轮询它的/health端点来判定健康状态。worker 的日志会显示一个heartbeat 作业每 10 秒触发一次日志行格式为 Heartbeat fired at {FireTime}, next at {NextFireTime}见 HeartbeatJob.cs而数据库侧最终会出现12 张qrtz_*表它们不是 AppHost 建的而是 worker首次启动时自己创建的——因为它的启动配置运行在Development环境见 launchSettings.json 中的ASPNETCORE_ENVIRONMENTDevelopment。这套「开发环境自动建表」的行为由Quartz.Aspire的AddQuartzPersistentStore根据IHostApplicationBuilder.Environment决定Development下取SchemaProvisioning.CreateIfMissing只在缺失时创建、绝不改动或删除任何已有对象非开发环境则什么都不说让未配置的存储保持默认的Validate行为——生产账号通常既没有也不应该拥有执行 DDL 的权限。完整的决策逻辑见 QuartzAspireHostApplicationBuilderExtensions.cs。Worker 侧接线把 AppHost 的声明翻译成调度器配置要真正理解 AppHost 在做什么需要看 worker 是如何消费这些资源的。worker 的 Program.cs 完整展示了这条链路builder.AddNpgsqlDataSource(quartz); builder.AddQuartzPersistentStore(quartz); builder.AddQuartz(q q.ScheduleJobHeartbeatJob(trigger trigger .WithIdentity(heartbeat) .StartNow() .WithSimpleSchedule(x x.WithInterval(TimeSpan.FromSeconds(10)).RepeatForever()))); builder.AddQuartzHostedService(options options.WaitForJobsToComplete true); WebApplication app builder.Build(); app.MapHealthChecks(/health); app.Run();这里有几个值得展开的要点1. 连接字符串的名字即身份。AppHost 中postgres.AddDatabase(quartz)给出的名字就是 worker 侧AddQuartzPersistentStore(quartz)索要的名字。WithReference(quartzDb)会把这个连接字符串以ConnectionStrings__quartz环境变量的形式注入 worker默认配置构建器再把它读回为ConnectionStrings:quartz。名字对得上一切自动接好。2. 唯一需要注意的注册顺序。builder.AddNpgsqlDataSource(quartz)必须放在AddQuartzPersistentStore之前——因为后者决定「连接从哪里来」时是照着当下已注册的服务集合来探测的若能找到按quartz键注册的 keyedDbDataSource就用它否则找容器里唯一的非 keyed 数据源都没有才退回连接字符串。一个事后才注册的数据源是AddQuartzPersistentStore永远看不到的。而AddQuartzPersistentStore与AddQuartz的顺序则无所谓——存储是通过ConfigureAllQuartzSchedulers贡献的属于增量式、与顺序无关的注册见 QuartzAspireHostApplicationBuilderExtensions.cs 的注释说明。3. 为什么 worker 用的是 Web SDK。这个项目用的是Microsoft.NET.Sdk.Web而不是Microsoft.NET.Sdk.Worker原因在 csproj 注释里写得很明白WithHttpHealthCheck需要某个东西在/health上应答而Microsoft.NET.Sdk.Worker没有 HTTP 服务器AppHost 就无从轮询。换成 Web SDK 只是这个改变的全部——应用仍然只托管调度器不托管任何其他 HTTP 业务。对应的worker 的httpendpoint 来自其 launchSettings.json这也是其他示例的 worker 项目没有这个文件的原因WithHttpHealthCheck在资源没有 http/https endpoint 时会在 AppHost 构建期直接抛异常。4. 健康检查。AddQuartzPersistentStore会自动把调度器的健康检查注册到IHealthChecksBuilder上对应AddQuartzHealthChecks()app.MapHealthChecks(/health)只是把它映射到端点。AppHost 侧则通过WithHttpHealthCheck(/health)轮询它。对应的 AppHost 侧完整接线在 Program.csIDistributedApplicationBuilder builder DistributedApplication.CreateBuilder(args); IResourceBuilderPostgresServerResource postgres builder.AddPostgres(postgres, password: postgresPassword) .WithDataVolume() .WithLifetime(ContainerLifetime.Persistent); IResourceBuilderPostgresDatabaseResource quartzDb postgres.AddDatabase(quartz); builder.AddProjectProjects.Quartz_Examples_Aspire_Worker(worker) .WithReference(quartzDb) .WaitFor(quartzDb) .WithHttpHealthCheck(/health); builder.Build().Run();为什么密码被固定一个持久化存储的经典陷阱这是 AppHost README 最核心、也最值得单独成节的内容。为什么示例要把 Postgres 密码硬编码固定下来答案藏在两行配置的组合效应里builder.AddPostgres(postgres, password: postgresPassword) .WithDataVolume() .WithLifetime(ContainerLifetime.Persistent);.WithDataVolume()和.WithLifetime(ContainerLifetime.Persistent)的意图是让容器及其数据跨多次运行存活——这正是持久化作业库的意义一个行数据每次重启就消失的「持久化」作业库本质上只是「零件更多一点的内存存储」README 原话a persistent job store whose rows disappear is an in-memory store with more moving parts。但问题随之而来如果让 Aspire 自己生成 Postgres 密码除非初始化了 user secrets否则它每次启动都会生成一个新密码而那个存活下来的数据卷里数据库还是用旧密码初始化的。于是第二次运行就陷入死锁worker 拿新密码连不上旧数据库WaitFor(quartzDb)永远不会完成worker 永远起不来——而且没有任何地方明确说出原因。AppHost 虽然会打印一条警告但警告和症状看起来毫不相干Resource postgres has a persistent lifetime but the AppHost project does not have user secrets configured. Generated parameter values (such as passwords) may change on each restart, causing persistent containers to be recreated.资源 postgres 具有持久化生命周期但 AppHost 项目未配置 user secrets。生成的参数值如密码可能每次重启都变化导致持久化容器被重建。修复方式在 Program.cs 中用一个带值的参数把密码钉死IResourceBuilderParameterResource postgresPassword builder.AddParameter(postgres-password, quartz-examples-local-only, secret: true);AddParameter加一个显式值就是 Aspire 里「固定一个参数」的惯用法idiom。第二次运行就能正常连上旧卷了。但 README 特别强调这个值出现在源码里对本机上的容器没问题却不是部署该有的做法。生产环境的正确姿势是把值拿掉builder.AddParameter(postgres-password, secret: true);只声明参数、不给值然后从user secrets、环境变量或密钥库key vault提供真实密码。AppHost 那条警告警告的正是「什么也没提供」的情形——它是在提醒你把凭证来源安排好而不是鼓励你把它写进源码。运行后的清理容器和数据卷是刻意留下的这个示例故意在运行结束后把容器和数据卷留在机器上——这正是「持久化作业库」的演示目的。你可以用以下命令查看它们docker ps -a docker volume ls会发现一个postgres-suffix容器和一个形如quartz.examples.aspire.apphost-hash-postgres-data的数据卷。手动删除它们docker rm -f postgres-suffix docker volume rm quartz.examples.aspire.apphost-hash-postgres-data如果之后想重跑示例通常保留数据卷反而是符合演示意图的——再次运行会直接复用旧数据库让「持久化」名副其实。延伸Quartz.Aspire一个调用背后的能力面AppHost 本身不引用 Quartz但它的存在意义是喂给Quartz.Aspire集成。理解 AppHost 的 README最好顺带知道AddQuartzPersistentStore(quartz)这一个调用究竟替你做完了什么——这正是 QuartzAspireSettings 和 QuartzAspireHostApplicationBuilderExtensions.cs 所定义的能力说明数据库方言选择根据连接字符串形状推断驱动Npgsql / SqlServer / MySqlConnector / SQLite / Oracle推断遵循「宁拒勿猜」原则零匹配或双匹配都抛SchedulerConfigException并点名QuartzAspireSettings.Provider见 ConnectionStringProviderInference.cs也可显式设置Provider连接来源选择依次探测keyedDbDataSource按连接名→ 容器内唯一非 keyed 数据源 → 连接字符串SQL Server 除外因为它没有DbDataSource永远走连接字符串路径Schema 供给开发环境CreateIfMissing、其余环境保持默认Validate可通过settings.SchemaProvisioning显式覆盖三值CreateIfMissing/Validate/None集群settings.Clustered true同时打开UseClustering()和GenerateInstanceId补上副本集缺的那一半集群配置健康检查自动注册调度器健康检查settings.DisableHealthChecks可关可观测性订阅 Quartz 的ActivitySource与Meter均为Quartz到现有 OpenTelemetry 管道不添加 exporter多调度器settings.SchedulerName指定这个存储属于哪个调度器对应AddQuartz(name, …)的名字表前缀settings.TablePrefix覆盖非默认QRTZ_前缀的表名设置按「共享节 → 连接专属节」两级绑定先读Aspire:Quartz再用Aspire:Quartz:connectionName覆盖连接字符串随后最后是configureSettings委托——每一步都比上一步更具体。这个示例也印证了一个重要事实Quartz.Aspire 并不依赖 AppHost 存在。它不引用任何Aspire.*包IHostApplicationBuilder就是它的全部契约——它只需要一个叫quartz的连接字符串。所以 worker 完全可以脱离 AppHost 单独运行只要在appsettings.json里给任意一个 PostgreSQL 放上连接字符串即可见 worker 的 README{ ConnectionStrings: { quartz: Hostlocalhost;Databasequartz;Usernamepostgres;Passwordpostgres } }dotnet run --project src/Quartz.Examples.Aspire.WorkerAppHost 带来的增量是容器、连接字符串注入、启动顺序和 dashboard去掉它其余能力provider 推断、Aspire:Quartz:*绑定、健康检查、遥测、开发环境建表原样保留。小结Quartz.Examples.Aspire.AppHost是理解「Quartz 在 Aspire 下如何落地」的最小标本AppHost 用三行声明把 Postgres、数据库和 worker 接成一张依赖图worker 用一个AddQuartzPersistentStore调用把 Aspire 的连接名翻译成持久化作业库。它同时是一份极佳的故障案例教材——.WithDataVolume()与自动生成密码的组合会让第二次运行陷入「worker 永不启动且无人解释」的死局而AddParameter(postgres-password, …, secret: true)的固定参数惯用法给出了优雅修复。这个项目的完整背景包括 AppHost 接线逐行注释与 worker 配置细节可继续阅读 AppHost 的 README、worker 的 README 以及仓库文档中对应的 Aspire 运行指南生产环境的 12 张qrtz_*建表脚本则位于 database/tables/tables_postgres.sql。赞分享任务调度后端【免费下载链接】quartznetQuartz Enterprise Scheduler .NET项目地址https://gitcode.com/gh_mirrors/qu/quartznet点击查看免费下载相关推荐OpenTTD 的 Eints 翻译系统修改 english.txt 基础语言的规则与翻译同步机制OpenTTD 的 Eints 翻译系统修改 english.txt 基础语言的规则与翻译同步机制 OpenTTD 的全部界面文本由 Web 翻译系统 Ein任务调度后端GraalVM Crema 运行时类加载下的 Micronaut 服务加载验证test-suite-crema-graalvm 测试套件解析GraalVM Crema 运行时类加载下的 Micronaut 服务加载验证test suite crema graalvm 测试套件解析 Micronau后端微服务Web框架OpenCore Simplify几分钟生成可用的黑苹果EFIOpenCore Simplify几分钟生成可用的黑苹果EFI OpenCore Simplify 读取硬件报告后自动下载 OpenCore、匹配 Kext开发工具CLI上一篇PyTorch Lightning 完全指南用 LightningModule 与 Lightning Fabric 规模化训练任意 AI 模型下一篇cpp-httplib HTTPS 服务端实战从自签名证书到 SSLServer 客户端验证创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑