ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Quartz.NET 仪表盘:基于 Blazor Server 的调度器可视化与运维面板实战指南

Quartz.NET 仪表盘:基于 Blazor Server 的调度器可视化与运维面板实战指南 任务调度后端【免费下载链接】quartznetQuartz Enterprise Scheduler .NET项目地址https://gitcode.com/gh_mirrors/qu/quartznet点击查看免费下载Quartz.Dashboard 是 Quartz.NET 官方提供的 Blazor 仪表盘组件它直接运行在你的 ASP.NET Core 应用进程内通过进程内 API 客户端读取同一个容器中注册的所有调度器提供任务Job、触发器Trigger、日历Calendar、执行历史与实时事件流的可视化界面。读完本文你将掌握仪表盘的安装与基本接入、自定义路径与反向代理部署、基于策略与角色的生产级授权加固、与现有 Blazor Server 应用的整合方式以及纯 API 项目在 .NET 10 下的静态资源配置要点。适用前提Quartz.Dashboard 要求Quartz 3.16 及以上版本支持目标框架为.NET 8 及更新版本。官方将其标注为“work in progress”仪表盘的 API 面可能在版本之间调整升级时需关注 Release Notes。安装仪表盘构建在Quartz.AspNetCore之上该包会被自动引入因此只需要添加一个包引用dotnet add package Quartz.Dashboard在 Visual Studio 的包管理器控制台中可等价使用Install-Package Quartz.Dashboard从仓库的项目文件 src/Quartz.Dashboard/Quartz.Dashboard.csproj 可以看到该包是一个Microsoft.NET.Sdk.Razor项目直接引用Quartz.AspNetCore并依赖Microsoft.AspNetCore.App框架引用。同时项目显式声明IsTrimmablefalse——Blazor Server 本质上是反射型框架组件的[Parameter]按名称绑定、路由按类型发现组件因此该包刻意不做裁剪适配裁剪或 Native AOT 发布的应用应通过Quartz.HttpClient以远程 HTTP API 方式驱动调度器而不是内嵌仪表盘。基本接入仪表盘的接入分为“注册服务”和“映射端点”两步以非 minimal hosting 的典型配置为例services.AddQuartz(q { // configure jobs and triggers }); services.AddQuartzDashboard(); services.AddQuartzHostedService(options options.WaitForJobsToComplete true);然后映射端点app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseAntiforgery(); app.UseEndpoints(endpoints { endpoints.MapQuartzDashboard(); });仪表盘界面默认位于/quartz。在 minimal hostingWebApplication下Quartz.Dashboard的 README 示例 给出了完整写法注意app.UseAntiforgery()与app.MapStaticAssets()都是必需的WebApplicationBuilder builder WebApplication.CreateBuilder(args); builder.AddQuartz(); builder.Services.AddQuartzHttpApi(); builder.Services.AddQuartzDashboard(); builder.AddQuartzHostedService(options options.WaitForJobsToComplete true); WebApplication app builder.Build(); app.UseAntiforgery(); app.MapStaticAssets(); app.MapQuartzHttpApi().RequireAuthorization(); app.MapQuartzDashboard().RequireAuthorization();其中app.MapStaticAssets()负责提供仪表盘的样式与脚本缺少它时页面会“无样式且无交互地渲染”。README 还特别强调仪表盘与 HTTP API 本身都不添加任何认证逻辑且两者都具备完整的变更能力通过它们调度的任务类型以字符串形式由请求方提供因此RequireAuthorization()不是装饰而是安全底线——若映射时不声明任何授权应用将拒绝启动确实希望匿名开放时才使用AllowAnonymous()。注册背后的实现细节从源码 QuartzDashboardServiceCollectionExtensions.cs 可以看到AddQuartzDashboard()实际完成的工作包括注册QuartzDashboardOptions并带有启动期校验DashboardPath必须以/开头且必须是简单 URL 路径不能包含{、}、?、#、./..段或空段HistoryRetention必须为正HistoryMaxEntriesPerScheduler至少为 1调用AddRazorComponents().AddInteractiveServerComponents()即仪表盘自带一个独立的 Blazor Server 组件根注册 SignalR并配置SchedulerStatus枚举以名称而非数字输出便于浏览器端直接显示 “Standby” 等状态通过TryAdd注册进程内IQuartzApiClientInProcessQuartzApiClient页面统一经由它读取ISchedulerRepository中的调度器若应用先注册了自己的IQuartzApiClient则优先使用应用自己的实现调用AddQuartzExecutionHistory()与AddQuartzSchedulerEvents()——执行历史记录器写入每个调度器的执行/错过记录事件流则供 Live Logs 页读取。在进程内调度器走内存流通过AddQuartzHttpClient注册的远程调度器则走 HTTP API 的事件路由注册QuartzEndpointAuthorizationGuardIHostedService它拒绝启动一个“映射了仪表盘但未做任何授权声明”的应用。自定义路径托管Hosting under a custom path注意下述“完全自包含自定义路径”需要Quartz.Dashboard 3.18.2 之后的版本对应 Issue #3134。在 3.18.2 及更早版本中只有仪表盘页面、链接和 hub 使用自定义路径Blazor 框架脚本、circuit 和静态资源仍停留在应用根路径因此前缀转发的反向代理需要UsePathBase或等价手段。当仪表盘托管自己的 Blazor 根即无参的MapQuartzDashboard()重载时可以通过选项设置自定义基路径services.AddQuartzDashboard(options { options.DashboardPath /my-api/quartz; });设置之后所有内容都在DashboardPath下提供页面、导航链接、SignalR hub、交互式 circuit{DashboardPath}/_blazor、框架脚本{DashboardPath}/_framework/blazor.web.js以及仪表盘静态资源{DashboardPath}/_content/Quartz.Dashboard/*。仪表盘外壳会输出以仪表盘为根的base href。源码层面QuartzDashboardEndpointRouteBuilderExtensions.cs 在检测到自定义路径时会做三件事把QuartzDashboardApp的页面路由从编译期/quartz前缀重写到配置的路径#3093把/_blazorcircuit 端点移动到dashboardPath /_blazor#3134SignalR 分发不依赖路由模式同时把 hub 映射到dashboardPath /hub。此外自定义路径模式下还会在仪表盘路径下镜像一份静态资源端点、_framework/blazor.web.js脚本端点与_framework/opaque-redirect转发端点使“只转发仪表盘前缀”的反向代理也能完整工作。仪表盘自带的路由匹配逻辑见 Routes.razor内置 Blazor Router 只能匹配编译期的/quartz模板所以独立仪表盘自行根据DashboardLink解析当前 URI 并匹配DashboardRouteTable。反向代理场景的两种部署方式方式一代理只转发路径前缀不设置 path base将DashboardPath设置为外部可见路径例如代理原样转发/my-api/*时设置为/my-api/quartz反向代理必须同时转发{DashboardPath}/_blazor和{DashboardPath}/hublive-views hub的 WebSocket 连接服务端 circuit 会通过浏览器使用的同一个外部 URL 连接 live-events hub因此应用自身必须能访问到自己的公网地址Live Logs 视图才能工作。方式二整个应用用UsePathBase()重设基路径此时DashboardPath相对于 path base 生效默认的/quartz在前缀下无需修改即可工作。在 minimal hosting 下必须显式在app.UsePathBase(...)之后调用app.UseRouting()app.UsePathBase(/my-api); app.UseRouting();否则隐式路由步骤会匹配到未剥离前缀的路径导致每个仪表盘路由都返回 404。升级既有自定义路径部署时的注意事项旧版本中 Blazor circuit 停留在站点根使用自定义DashboardPath后它改为在{DashboardPath}/_blazor连接。因此需要更新针对/_blazor的反向代理规则例如 WebSocket upgrade location。自定义DashboardPath不支持与MapQuartzDashboard(blazor)集成到现有 Blazor 应用的重载搭配使用该模式下仪表盘页面路由固定为/quartz若配置了自定义路径应用启动时会抛出带明确描述的异常见 QuartzDashboardEndpointRouteBuilderExtensions.cs 中的校验逻辑。启用历史插件Execution History仪表盘的 History 页展示执行历史。要让历史被记录需要在QuartzOptions中启用 Quartz 的历史插件services.ConfigureQuartzOptions(options { options[quartz.plugin.jobHistory.type] Quartz.Plugin.History.LoggingJobHistoryPlugin, Quartz.Plugins; options[quartz.plugin.triggerHistory.type] Quartz.Plugin.History.LoggingTriggerHistoryPlugin, Quartz.Plugins; });需要说明的是这两个插件来自 Quartz.Plugins 包它们把执行/触发事件写入应用日志仪表盘并不读取它们。仪表盘的 History 页读取的是AddQuartzExecutionHistory()注册的进程内执行历史存储AddQuartzDashboard()会自动调用它。源码 QuartzDashboardServiceCollectionExtensions.cs 中的AddHistory会把仪表盘的两个历史选项HistoryRetention、HistoryMaxEntriesPerScheduler回写到ExecutionHistoryOptions上若应用注册了自定义IDashboardHistoryStore则 Quartz 的执行历史存储会反向适配到它上面。历史保留默认 24 小时、每调度器默认 2000 条执行与 2000 条 misfire最旧优先丢弃由 QuartzDashboardOptions.cs 中的HistoryRetention与HistoryMaxEntriesPerScheduler控制。生产加固Production hardening策略与基于角色的授权为仪表盘访问显式声明一个授权策略services.AddAuthorization(options { options.AddPolicy(QuartzDashboardOps, policy { policy.RequireAuthenticatedUser(); policy.RequireRole(Operations, SchedulerAdmin); }); }); services.AddQuartzDashboard(options { options.AuthorizationPolicy QuartzDashboardOps; });app.UseEndpoints(endpoints { endpoints.MapQuartzDashboard(); });设置了AuthorizationPolicy后该策略一致地作用于仪表盘页面、SignalR hub、Blazor circuit/_blazor和仪表盘静态资源端点整个仪表盘在包括 fail-closedFallbackPolicy的场景下都被统一收口。不设置策略时仪表盘自身不添加任何授权具体行为如下静态资源端点_content/Quartz.Dashboard/*与Blazor circuit/_blazor允许匿名访问因此在 fail-closedFallbackPolicy下仍能正常工作——它们属于公开的包内容仪表盘页面与 SignalR hub没有授权元数据由宿主自身策略管辖。在 fail-closedFallbackPolicy下未认证用户访问/quartz会被重定向到登录页认证用户可看到完整仪表盘。若希望向未认证用户开放仪表盘就不要在仪表盘路径上强制 fail-closedFallbackPolicy或者设置一个未认证用户也能满足的AuthorizationPolicy。从实现上看QuartzDashboardEndpointRouteBuilderExtensions.cs设置策略时hub、全部静态资源端点以及独立模式下组件端点统一RequireAuthorization(policyName)未设置策略时静态资源端点会被标记AllowAnonymous()独立模式下非页面端点如/_blazor也会被标记AllowAnonymous而页面与 hub 保持无元数据状态交由宿主策略管辖——这样既能保住 fail-closed 下的页面样式与 circuit 连通又不会让调度器数据对匿名用户静默暴露。关于 fail-closedFallbackPolicy与MapStaticAssets()的警告宿主app.MapStaticAssets()提供的资源.NET 9/10 默认行为以及框架脚本_framework/blazor.web.js来自宿主/框架自有端点Quartz 无法为其添加授权标注。fail-closedFallbackPolicy会无视仪表盘配置、直接拦截未认证用户对这些资源的访问。若需要在认证之前让它们可达例如为登录页加载样式使用app.MapStaticAssets().AllowAnonymous();静态 Web 资源属于公开内容经典的app.UseStaticFiles()中间件在授权之前执行不受FallbackPolicy约束。参见下文 API-only 项目 中相关的RequiresAspNetWebAssets设置。使用自定义DashboardPath时这条警告不适用于仪表盘自身此时框架脚本和静态资源由仪表盘自有端点在仪表盘路径下提供这些端点携带仪表盘的授权元数据。API Key 或自定义授权检查优先使用 ASP.NET Core 基于策略/Handler 的授权机制这样仪表盘 UI 与 hub 的强制策略保持一致。源码中 QuartzDashboardOptions.cs 还提供了SchedulerAuthorizationPolicy——它按调度器粒度鉴权设置后调度器选择器只展示访问者有权查看的调度器无权调度器的页面直接提示live-events hub 也会拒绝订阅。该策略与 HTTP API 的QuartzHttpApiOptions.SchedulerAuthorizationPolicy使用同一套AuthorizationHandlerTRequirement, SchedulerResource两个面共用一份实现。多调度器与集群部署指南集群化 ADO.NET 任务存储JobStore仪表盘操作就是调度器操作会影响整个集群。应把写操作权限限制在可信的运维角色上。单宿主内多个本地调度器调度器选择器支持多个已注册调度器。请使用清晰的调度器名称与环境化分组。反向代理与 Blazor Server需启用 WebSocket/SignalR 转发托管栈有要求时启用粘性会话sticky sessions。Blazor circuit 连接/_blazor自定义DashboardPath时为{DashboardPath}/_blazor。拆分运维体验为只读观察者运行ReadOnly true的只读仪表盘为操作员单独运行可写实例。ReadOnly选项QuartzDashboardOptions.cs由客户端强制执行而非仅靠页面隐藏按钮因此被拒绝的变更在任何入口都被拒绝。运维数据保留仪表盘历史来自插件属于运维数据长期分析需求应配置插件加外部保留/报表方案。此外QuartzDashboardOptions还提供IsJobTypeAllowedQuartzDashboardOptions.cs通过仪表盘IQuartzApiClient调用时可命名的任务类型谓词默认放行一切类型。它按任务类型名字符串匹配同一类型可能有多种拼写如是否带Version/Culture/PublicKeyToken被拒绝的名字抛出UnauthorizedAccessException——这是对“通过Quartz.Jobs的NativeJob启动进程”这类风险的类型级收窄HTTP API 侧对应的设置是QuartzHttpApiOptions.IsJobTypeAllowed。功能清单调度器概览与摘要卡片任务与触发器列表支持搜索与分页任务详情与触发器详情页当前执行中的任务视图调度器活动的实时事件/日志流暂停、恢复、立即触发、取消调度/删除等操作只读模式下不可用触发器详情的 cron 重排以及任务详情中“带覆盖触发”的操作日历的创建/替换cron 日历、详情与删除操作多调度器选择通过仪表盘选项支持只读模式对应的页面组件全部位于 src/Quartz.Dashboard/Components/PagesDashboard.razor概览、Jobs.razor/Triggers.razor列表、JobDetail.razor/TriggerDetail.razor详情、CurrentlyExecuting.razor执行中、LiveLogs.razor实时日志、History.razor历史、Calendars.razor/CalendarDetail.razor日历、Schedulers.razor与Cluster.razor调度器与集群视图等。当前限制实时视图是近实时的轮询/流式传输并非无损事件存储。没有面向历史分析的持久化 UI插件支撑的历史是运维性、日志导向的。管理功能刻意保持克制类型化编辑器覆盖 cron 日历/触发器与运维覆盖overrides。UX 面向 Quartz API 与调度器操作不是工作流或业务流程的可视化。另外需要注意与文档/源码一致的细节远程调度器通过AddQuartzHttpClient注册到另一进程同样会出现在列表中并可被驱动含历史唯独 Live Logs 页在 HTTP 方式下没有可展示的内容页面会明确提示。集成到现有 Blazor Server 应用若宿主已使用 Blazor Server已调用MapRazorComponentsApp().AddInteractiveServerRenderMode()应使用接受现有RazorComponentsEndpointConventionBuilder的MapQuartzDashboard重载以避免注册第二个/_blazorSignalR 端点导致路由冲突services.AddRazorComponents().AddInteractiveServerComponents(); services.AddQuartzDashboard();app.UseRouting(); app.UseAntiforgery(); var blazor app.MapRazorComponentsApp() .AddInteractiveServerRenderMode(); app.MapQuartzDashboard(blazor);同时必须把仪表盘程序集加入宿主Routes.razor中Router的AdditionalAssemblies否则交互式路由无法解析仪表盘页面Router AppAssemblytypeof(App).Assembly AdditionalAssembliesnew[] { typeof(Quartz.Dashboard.Components.QuartzDashboardApp).Assembly } ... /Router缺少这一步时仪表盘首次请求会服务端渲染成功但 circuit 建立后路由无法匹配/quartz页面会“闪一下”然后被应用的自定义 404 页面替换。仪表盘的页面、布局、CSS 与 JS 互操作通过AddAdditionalAssemblies注册进宿主的 Blazor 端点路由App.razor无需额外添加link或script标签。两条硬性提醒不要在自有MapRazorComponents之外再调用无参MapQuartzDashboard()这会在/_blazor注册两个端点仪表盘的交互页面将无法工作。仪表盘页面不声明渲染模式因此宿主必须使用全局交互式服务端渲染例如App.razor中的Routes rendermodeInteractiveServer /。若使用按页面/组件粒度的交互式渲染页面会以静态 SSR 渲染其操作全部失效。实现层面QuartzDashboardEndpointRouteBuilderExtensions.cs集成重载会调用existingComponents.AddAdditionalAssemblies(typeof(QuartzDashboardApp).Assembly)把仪表盘程序集注册进宿主的 Blazor 路由并复用宿主已有组件构建器授权约定只作用到“组件来自仪表盘程序集”的端点#3066不会波及宿主自身页面。QuartzDashboardConventionBuilder.cs 则把页面与 live-events hub 打包成一个返回对象保证对仪表盘的整体授权声明同时覆盖“有趣的那一半”数据面。API-only 项目无 .razor 文件若宿主项目自身没有.razor文件例如纯 API 项目托管 Quartz并且运行在.NET 10 及以上需要在项目文件中添加PropertyGroup RequiresAspNetWebAssetstrue/RequiresAspNetWebAssets /PropertyGroup该属性让 .NET SDK 将 Blazor 框架脚本_framework/blazor.web.js、blazor.server.js纳入应用的静态 Web 资源。缺少它时/_framework/blazor.web.js返回 HTTP 404——因为自 .NET 10 起这些文件作为静态 Web 资源提供不再内嵌于 ASP.NET Core 程序集。同时必须在请求管线中启用静态文件app.UseRouting(); app.Antiforgery(); app.UseStaticFiles();在 .NET 8 与 .NET 9 上框架脚本通过端点路由提供无需额外配置。从实现上看自定义路径模式下的框架脚本端点会先查WebRootFileProvider再回退到 .NET 8/9 的嵌入式 manifest 提供器最后在 .NET 10 场景下转发到框架自有的根端点见 QuartzDashboardEndpointRouteBuilderExtensions.cs。结语Quartz.Dashboard 为运行在 ASP.NET Core 进程内的 Quartz.NET 调度器提供了开箱即用的可视化运维入口从/quartz的默认接入、自定义路径与反向代理部署到基于策略/角色的整体授权、只读与按调度器粒度鉴权再到与既有 Blazor Server 应用的平滑整合均可通过 QuartzDashboardOptions.cs 中有限的几个选项和两个MapQuartzDashboard重载完成。部署到生产环境前请重点核对仪表盘与 HTTP API 是否显式声明了授权、反向代理是否正确转发_blazor与hub的 WebSocket、集群/多调度器场景下写操作是否收敛到可信角色以及 .NET 10 纯 API 项目的RequiresAspNetWebAssets与静态文件管线配置。赞分享任务调度后端【免费下载链接】quartznetQuartz Enterprise Scheduler .NET项目地址https://gitcode.com/gh_mirrors/qu/quartznet点击查看免费下载相关推荐Azure Arc 仪表盘实战指南基于官方 JSON 模板构建 SQL Server 混合资产可视化监控Azure Arc 仪表盘实战指南基于官方 JSON 模板构建 SQL Server 混合资产可视化监控 在 Azure Arc 大规模接入 SQL Serv示例工程数据库教程后端70免费Blazor组件打造专业仪表盘Radzen Blazor仪表盘与量表组件完全指南70免费Blazor组件打造专业仪表盘Radzen Blazor仪表盘与量表组件完全指南 Radzen Blazor是一套包含70多个免费原生BlazorJuggle数据可视化图表仪表盘实战指南Juggle数据可视化图表仪表盘实战指南 概述 在当今数据驱动的时代业务监控和数据分析已成为企业运营的核心需求。Juggle作为一款强大的接口编排平台不仅低代码流程编排后端前端上一篇5个简单步骤快速上手Ink/Stitch从SVG到刺绣文件下一篇快速上手Openbay5分钟完成环境配置与项目部署指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进