# Quartz.Net.UI **Repository Path**: 992582781/Quartz.Net.UI ## Basic Information - **Project Name**: Quartz.Net.UI - **Description**: 基于Quartz.NET构建的开箱即用分布式任务调度库。支持Cron定时调度、Hangfire风格API、DAG任务链、Redis分布式锁、内置Dashboard、RESTful API,多数据库适配(SQLServer/MySQL/PostgreSQL/SQLite)。 - **Primary Language**: C# - **License**: LGPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2026-09-13 - **Last Updated**: 2026-09-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Quartz.Net.UI An out-of-the-box distributed task scheduling library for .NET, built on top of [Quartz.NET](https://www.quartz-scheduler.net/). ``` dotnet new webapp -n MyScheduler dotnet add package Quartz.Net.UI ``` ## 🚀 Quick Start (5 Minutes) ### 1. Create a Project ```bash dotnet new webapp -n MyScheduler cd MyScheduler dotnet add package Quartz.Net.UI ``` ### 2. Configure `appsettings.json` ```json { "ConnectionStrings": { "DefaultConnection": "Server=.;Database=QuartzNet;User Id=sa;Password=xxx;TrustServerCertificate=true", "Redis": "127.0.0.1:6379,allowAdmin=true,abortConnect=false" }, "DatabaseProvider": "SqlServer" } ``` > Four database providers are supported: `SqlServer` / `MySql` / `PostgreSql` / `Sqlite` > > Redis is optional for single-node deployments, but recommended for cluster coordination. ### 3. `Program.cs` — 11 Lines, That's It ```csharp using Quartz.Net.Infrastructure; using Quartz.Net.Jobs; var builder = WebApplication.CreateBuilder(args); builder.Services.AddQuartzNet(options => { options.ConnectionString = builder.Configuration.GetConnectionString("DefaultConnection")!; options.RedisConnectionString = builder.Configuration.GetConnectionString("Redis") ?? ""; options.DatabaseProvider = DatabaseProvider.SqlServer; options.AutoCreateTables = true; }); var app = builder.Build(); app.UseQuartzNetDashboard(); // Dashboard route: /quartz // Register a recurring health-check task (every 30 seconds) await RecurringJob.AddOrUpdateAsync("health-check", () => JobHelper.HealthCheck(), "*/30 * * * * ?"); app.Run(); ``` > All Quartz tables (`QRTZ_*`, `SYS_TASK`, `SYS_TASK_RECORD`) are auto-created on first startup. ### 4. Run ```bash dotnet run ``` Open your browser: | URL | Description | |-----|-------------| | `http://localhost:5000/quartz` | Dashboard management UI | | `http://localhost:5000/` | Home page with Try buttons for all APIs | --- ## 📖 Your First Task ### Via Dashboard (No Code Required) 1. Open `http://localhost:5000/quartz` 2. Click **+ New Task** 3. Fill in the form: | Field | Example | Description | |-------|---------|-------------| | Task Name | `HealthCheck` | Unique name within the group | | Group Name | `Monitoring` | Logical grouping | | Cron Expression | `0 */5 * * * ?` | Every 5 minutes | | API URL | `https://your-api.com/health` | Target endpoint | | Method | `GET` | HTTP request method | | Auth Key | `Authorization` | Optional, header key | | Auth Value | `Bearer eyJ...` | Optional, header value | 4. Click **Create** → the task runs on schedule ### Via Code ```csharp // 1. Fire-and-forget (one-shot) await BackgroundJob.EnqueueAsync(s => s.SendEmail("user@test.com", "Hello!")); // 2. Delayed execution await BackgroundJob.ScheduleAsync(s => s.SendReminder(123), TimeSpan.FromMinutes(5)); // 3. Recurring (Cron) await RecurringJob.AddOrUpdateAsync("SyncOrders", s => s.SyncPendingOrdersAsync(), "0 */10 * * * ?"); ``` --- ## 📊 Dashboard Guide | Tab | Features | |-----|----------| | **Overview** | Scheduler status, task statistics (total/running/paused) | | **Scheduled Tasks** | CRUD, **pause/resume/trigger**, execution logs, batch operations | | **Execution History** | All execution records, **replay** (re-run with original params), delete | | **Enqueue** | Fire one-shot HTTP requests on the fly | | **Runtime Monitor** | Currently executing jobs, scheduler metadata | | **DAG** | DAG task chain visualization | ### Key Operations - **Pause** — Stops the Cron trigger, task preserved in DB, can be resumed - **Resume** — Restarts the Cron trigger - **Trigger** — Fire immediately without affecting the Cron schedule - **Replay** — Re-run a past execution with original parameters (DAG parent replay auto-rebuilds child chains) --- ## 🔄 Cron Cheat Sheet | Expression | Meaning | |------------|---------| | `0/30 * * * * ?` | **Every 30 seconds** | | `0 */1 * * * ?` | **Every minute** | | `0 0 */1 * * ?` | **Every hour** (on the hour) | | `0 0 9 * * ?` | **Daily at 9:00** | | `0 0 9,18 * * ?` | **Daily at 9:00 and 18:00** | | `0 30 9 * * 1-5` | **Weekdays at 9:30** | | `0 0 0 1 * ?` | **1st of each month at midnight** | | `0 15 10 ? * MON-FRI` | **Weekdays at 10:15** | | `0 0 8 ? * 2#1` | **First Monday of each month at 8:00** | > ⚠️ Quartz cron requires **6 or 7 fields** (seconds are mandatory), unlike Linux cron which uses 5 fields. --- ## 🎯 Task Type Comparison | | **HttpJob** | **EnqueueJob** | **ReflectionJob (Enqueue)** | **ReflectionJob (Recurring)** | |---|---|---|---|---| | **Trigger** | Cron schedule | Fire-and-forget | Fire-and-forget | Cron schedule | | **What it does** | HTTP API call | HTTP API call | .NET method call | .NET method call | | **Parameters** | DB row (SYS_TASK) | API request body | Expression tree args | Expression tree args | | **Auto-cleanup** | ❌ Persistent | ✅ Self-deleting | ✅ Self-deleting | ❌ Persistent | | **Created via** | Dashboard / API | Dashboard / API | `BackgroundJob.EnqueueAsync()` | `RecurringJob.AddOrUpdateAsync()` | | **Use case** | Scheduled HTTP calls (health checks, sync) | One-shot HTTP (callbacks, notifications) | Run your .NET code (one-shot) | Run your .NET code (scheduled) | ### Decision Flow ``` Need to call an HTTP endpoint? ├── On a schedule? → HttpJob (Dashboard or POST /api/task) └── Just once? → EnqueueJob (Dashboard or POST /api/enqueue) Need to run your own .NET code? ├── On a schedule? → RecurringJob.AddOrUpdateAsync() └── Just once? → BackgroundJob.EnqueueAsync() ``` --- ## 📚 API Reference ### BackgroundJob (Code Scheduling) | Method | Description | |--------|-------------| | `EnqueueAsync(Expression)` | Execute immediately (fire-and-forget) | | `EnqueueAsync(Expression)` | Execute immediately (instance method) | | `EnqueueAsync(Expression, maxRetries, retryDelaySec)` | Execute with retry on failure | | `ScheduleAsync(Expression, TimeSpan)` | Delayed execution | | `ScheduleAsync(Expression, TimeSpan)` | Delayed execution (instance method) | | `ContinueWithAsync(parentId, expression, rule)` | Create DAG parent-child chain | | `DeleteAsync(jobId)` | Delete a job | ### RecurringJob (Scheduled Tasks) | Method | Description | |--------|-------------| | `AddOrUpdateAsync(id, Expression, cron)` | Register or update a recurring job | | `AddOrUpdateAsync(id, Expression, cron)` | Register or update (instance method) | | `RemoveIfExistsAsync(id)` | Remove a recurring job | | `TriggerAsync(id)` | Trigger immediately (manual run) | ### RESTful API | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/task` | List tasks (paginated) | | `GET` | `/api/task/{id}` | Get task by ID | | `POST` | `/api/task` | Create a new scheduled task | | `PUT` | `/api/task/{id}` | Update a task | | `DELETE` | `/api/task/{id}` | Delete a task (cascades to children) | | `POST` | `/api/task/{id}/pause` | Pause | | `POST` | `/api/task/{id}/resume` | Resume | | `POST` | `/api/task/{id}/trigger` | Trigger now | | `GET` | `/api/task/{id}/records` | Execution history (paginated) | | `GET` | `/api/task/{id}/dag` | DAG tree | | `DELETE` | `/api/task/record/{recordId}` | Delete execution record | | `POST` | `/api/task/record/{recordId}/replay` | Replay execution | | `POST` | `/api/enqueue` | One-shot HTTP enqueue | | `GET` | `/api/scheduler` | Scheduler metadata | | `GET` | `/api/triggers` | List all triggers | | `GET` | `/api/fired` | Currently executing jobs | --- ## 🗄️ Database Configuration ### SQL Server ```json { "ConnectionStrings": { "DefaultConnection": "Server=.;Database=QuartzNet;User Id=sa;Password=xxx;TrustServerCertificate=true" }, "DatabaseProvider": "SqlServer" } ``` **Quartz properties** (auto-configured): ```json { "Quartz": { "quartz.jobStore.driverDelegateType": "Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz", "quartz.jobStore.lockHandler.type": "Quartz.Impl.AdoJobStore.UpdateLockRowSemaphore, Quartz", "quartz.jobStore.acquireTriggersWithinLock": "true", "quartz.dataSource.default.provider": "SqlServer" } } ``` ### MySQL ```json { "ConnectionStrings": { "DefaultConnection": "Server=localhost;Database=quartznet;User=root;Password=xxx;Charset=utf8mb4" }, "DatabaseProvider": "MySql" } ``` > A custom `QuartzMySqlDelegate` is built-in to resolve `TINYINT(1)` → `bool` conversion issues. **Quartz properties** (auto-configured): ```json { "Quartz": { "quartz.jobStore.driverDelegateType": "Quartz.Net.Infrastructure.QuartzMySqlDelegate, Quartz.Net", "quartz.jobStore.lockHandler.type": "Quartz.Impl.AdoJobStore.UpdateLockRowSemaphore, Quartz", "quartz.jobStore.acquireTriggersWithinLock": "true", "quartz.dataSource.default.provider": "MySql" } } ``` ### PostgreSQL ```json { "ConnectionStrings": { "DefaultConnection": "Host=localhost;Database=quartznet;Username=postgres;Password=xxx" }, "DatabaseProvider": "PostgreSql" } ``` > Add `Npgsql` NuGet package manually. **Quartz properties** (auto-configured): ```json { "Quartz": { "quartz.jobStore.driverDelegateType": "Quartz.Impl.AdoJobStore.PostgreSQLDelegate, Quartz", "quartz.jobStore.lockHandler.type": "Quartz.Impl.AdoJobStore.UpdateLockRowSemaphore, Quartz", "quartz.jobStore.acquireTriggersWithinLock": "true", "quartz.dataSource.default.provider": "Npgsql" } } ``` ### SQLite ```json { "ConnectionStrings": { "DefaultConnection": "Data Source=quartznet.db" }, "DatabaseProvider": "Sqlite" } ``` > ⚠️ SQLite for development only. Use SQL Server, MySQL, or PostgreSQL for production clusters. **Quartz properties** (auto-configured): ```json { "Quartz": { "quartz.jobStore.driverDelegateType": "Quartz.Impl.AdoJobStore.SQLiteDelegate, Quartz", "quartz.jobStore.lockHandler.type": "Quartz.Impl.AdoJobStore.StdRowLockSemaphore, Quartz", "quartz.jobStore.acquireTriggersWithinLock": "false", "quartz.dataSource.default.provider": "SQLite-Microsoft" } } ``` --- ## 🔗 DAG Task Chains DAG (Directed Acyclic Graph) lets you define parent-child task relationships — child tasks automatically execute after the parent completes. ### Continuation Rules | Rule | Description | |------|-------------| | `Always` | Child fires regardless of parent result | | `OnSuccess` | Child fires only when parent succeeds | | `OnFailure` | Child fires only when parent fails | ### One-Shot DAG Chain ```csharp // Parent success → triggers child var parentId = await BackgroundJob.EnqueueAsync(() => JobHelper.SimpleTask("Parent")); await BackgroundJob.ContinueWithAsync(parentId, () => JobHelper.SimpleTask("Child (OnSuccess)"), "OnSuccess"); // Linear chain: A → B → C (serial) var jobA = await BackgroundJob.EnqueueAsync(() => JobHelper.SimpleTask("A")); var jobB = await BackgroundJob.ContinueWithAsync(jobA, () => JobHelper.SimpleTask("B"), "OnSuccess"); await BackgroundJob.ContinueWithAsync(jobB, () => JobHelper.SimpleTask("C"), "Always"); ``` ### Cron + DAG ```csharp // Every cron execution triggers child tasks automatically await RecurringJob.AddOrUpdateAsync("DagCron", () => JobHelper.SimpleTask("Parent"), "0 */2 * * * ?"); await BackgroundJob.ContinueWithAsync("DagCron", () => JobHelper.SimpleTask("OnSuccess Child"), "OnSuccess"); await BackgroundJob.ContinueWithAsync("DagCron", () => JobHelper.SimpleTask("Always Child"), "Always"); ``` ### Child Task Lifecycle - Cannot be deleted independently (cascaded from parent) - Cannot be paused/resumed/triggered independently (operate on parent) - Replaying a parent record auto-rebuilds the entire DAG subtree --- ## ⚙️ QuartzNetOptions Reference | Property | Type | Default | Description | |----------|------|---------|-------------| | `DatabaseProvider` | `enum` | `SqlServer` | Database type | | `ConnectionString` | `string` | `""` | Business DB connection string | | `RedisConnectionString` | `string` | `""` | Redis connection string (optional) | | `WorkerId` | `ushort` | `1` | Snowflake worker ID (0-63) | | `DashboardPath` | `string?` | `"/quartz"` | Dashboard base path, `null` to disable | | `AutoCreateTables` | `bool` | `true` | Auto-create tables on startup | | `DefaultMaxRetryAttempts` | `int` | `0` | Default retry count | | `DefaultRetryDelaySeconds` | `int` | `60` | Retry interval (seconds) | | `LockTimeoutSeconds` | `int` | `600` | Distributed lock / job timeout | | `QuartzProperties` | `Dictionary?` | `null` | Override Quartz properties | | `HttpClient` | `object` | — | HTTP Client options | Full `appsettings.json` example: ```json { "QuartzNet": { "DatabaseProvider": "SqlServer", "ConnectionString": "Server=.;Database=QuartzNet;User Id=sa;Password=xxx;TrustServerCertificate=true", "RedisConnectionString": "127.0.0.1:6379", "WorkerId": 1, "DashboardPath": "/quartz", "AutoCreateTables": true, "DefaultMaxRetryAttempts": 3, "DefaultRetryDelaySeconds": 60, "LockTimeoutSeconds": 600 }, "Quartz": { "quartz.scheduler.instanceName": "QuartzNetScheduler", "quartz.serializer.type": "json", "quartz.threadPool.threadCount": "10" } } ``` > The `Quartz` section is optional. Core properties (driverDelegateType, lockHandler, dataSource) are auto-configured by `AddQuartzNet` based on your `DatabaseProvider`. --- ## 🏭 Production Checklist | Item | Recommendation | Notes | |------|---------------|-------| | `AutoCreateTables` | `false` | Use migrations for production schema management | | **Redis** | Required | Distributed lock coordination for multi-node | | `WorkerId` | Unique per node | Snowflake ID de-duplication across cluster | | `LockTimeoutSeconds` | ≥ max job duration | Jobs exceeding this timeout are considered hung | | **Dashboard auth** | Must configure | Add authorization (see example below) | | **Thread pool** | `threadCount ≥ 10` | Adjust based on workload | | **Database** | SQL Server / MySQL / PostgreSQL | SQLite for dev/test only | ### Dashboard Authentication ```csharp app.UseQuartzNetDashboard(new DashboardOptions { Authorization = new IDashboardAuthorizationFilter[] { // Option 1: localhost only new LocalhostOnlyFilter(), // Option 2: API key // new ApiKeyFilter("your-secret-key"), // Option 3: custom (JWT / Cookie / OAuth) // new MyCustomAuthFilter() } }); ``` --- ## 🏗️ Architecture ``` ┌────────────────────────────────────────────┐ │ Dashboard (/quartz) │ │ ┌────────┐ ┌──────┐ ┌───────┐ ┌────────┐ │ │ │Overview│ │Tasks │ │Record │ │Enqueue │ │ │ └────────┘ └──────┘ └───────┘ └────────┘ │ └──────────────────┬─────────────────────────┘ │ REST API ▼ ┌────────────────────────────────────────────┐ │ Quartz Scheduler │ │ ┌──────────┐ ┌──────────┐ ┌───────────┐ │ │ │ HttpJob │ │EnqueueJob│ │Reflection │ │ │ │ (Cron) │ │(One-shot)│ │ (Code) │ │ │ └──────────┘ └──────────┘ └───────────┘ │ └──────────────────┬─────────────────────────┘ │ ┌────────────────────────────────────────────┐ │ Redis Distributed Lock │ │ Watchdog auto-renewal / Lua atomic release │ └────────────────────────────────────────────┘ │ ┌────────────────────────────────────────────┐ │ SYS_TASK / SYS_TASK_RECORD │ │ Execution History / Replay / DAG Chains │ └────────────────────────────────────────────┘ ``` ### Key Technical Details - **Redis Distributed Lock** — Lua atomic operations + Watchdog auto-renewal (every expiry/3) to prevent duplicate execution - **Snowflake ID** — Based on Yitter.IdGenerator, globally unique and time-ordered - **Auto Table Creation** — Detects and creates Quartz + business tables on startup - **DAG Chains** — `ChainEdgeStore` stores edges in memory; Cron tasks use non-consuming Peek mode --- ## Dependencies | Package | Version | Purpose | |---------|---------|---------| | Quartz.NET | 3.13.0 | Core scheduling engine | | Quartz.Extensions.Hosting | 3.13.0 | Hosting integration | | Quartz.Serialization.Json | 3.13.0 | JSON serialization | | StackExchange.Redis | 3.1.0 | Distributed locking | | Yitter.IdGenerator | 1.0.14 | Snowflake ID generator | | Newtonsoft.Json | 13.0.3 | JSON serialization | | Microsoft.EntityFrameworkCore | 8.0.0 | ORM | | Pomelo.EntityFrameworkCore.MySql | 8.0.0 | MySQL EF provider | | MySql.Data | 8.0.33 | Quartz MySQL ADO.NET driver | | Npgsql.EntityFrameworkCore.PostgreSQL | 8.0.0 | PostgreSQL EF provider | | Microsoft.EntityFrameworkCore.Sqlite | 8.0.0 | SQLite EF provider | ## License MIT