A runnable reference application that demonstrates every feature of SW.Scheduler in one place.
The app intentionally ships two parallel UI approaches side-by-side:
| Approach | What it is | When to use |
|---|---|---|
Built-in Admin UI (/scheduler-management) |
Server-rendered HTMX + Pico.css dashboard from SW.Scheduler.Viewer |
Zero-effort dashboard — install the NuGet and wire two lines |
REST API (/api/scheduler, /api/dashboard) |
JSON endpoints exposed via SchedulerController and DashboardController |
Foundation for building your own SPA, mobile app, or custom admin UI |
Both approaches are fully wired and running concurrently so you can compare them directly and choose the one that fits your project.
cd SampleApplication
dotnet runThe app starts on https://localhost:5001 (or the port shown in the console).
| URL | Description |
|---|---|
https://localhost:5001/scheduler-management |
Built-in HTMX admin UI |
https://localhost:5001/swagger |
Swagger UI for all REST endpoints |
Five sample customers (Alice, Bob, Carol, Dan, Eva) are seeded automatically on first run.
The sample uses an in-memory Quartz store and an in-memory EF Core database so it runs with zero infrastructure — no PostgreSQL, no SQL Server, no Redis needed.
<!-- SampleApplication.csproj -->
<ProjectReference Include="..\SW.Scheduler\SW.Scheduler.csproj" />
<ProjectReference Include="..\SW.Scheduler.EfCore\SW.Scheduler.EfCore.csproj" />
<ProjectReference Include="..\SW.Scheduler.Sdk\SW.Scheduler.Sdk.csproj" />
<ProjectReference Include="..\SW.Scheduler.PgSql\SW.Scheduler.PgSql.csproj" />
<ProjectReference Include="..\SW.Scheduler.Viewer\SW.Scheduler.Viewer.csproj" />In a real application you would reference only the provider you need (
PgSql,SqlServer, orMySql) andSW.Scheduler.EfCore. Job-definition projects reference onlySW.Scheduler.Sdk.
SampleApplication/
├── Jobs/
│ ├── CountCustomersJob.cs — simple job, [Schedule] attribute, [RetryConfig]
│ ├── GenerateReportJob.cs — simple job, [Schedule] attribute, no retry
│ ├── SendCustomerEmailsJob.cs — parameterized job (SendEmailsParams)
│ └── NotifyCustomerJob.cs — parameterized job (NotifyCustomerParams)
│
├── Controllers/
│ ├── SchedulerController.cs — REST API: schedule / reschedule / pause / resume / unschedule
│ ├── DashboardController.cs — REST API: execution history queries (IScheduleReader)
│ └── CustomersController.cs — CRUD for the sample Customer entity
│
├── Data/
│ └── AppDbContext.cs — EF Core DbContext (in-memory + ApplyScheduling())
│
├── Program.cs — service registration + pipeline wiring
└── appsettings.json
Provided by SW.Scheduler.Viewer. No extra code — just register and mount.
URL: http://localhost:5000/scheduler-management
Pages:
- Dashboard — running jobs count, recent executions, failure rate, success rate
- Running — live view of in-flight executions across all nodes
- History — filterable execution log (by job group and success/failure)
- Detail — per-execution breakdown including duration, error, and job parameters (context)
// Register — before builder.Build()
builder.Services.AddSchedulerMonitoring<AppDbContext>(); // must come first
builder.Services.AddSchedulerViewer(opts =>
{
opts.Title = "Sample App Scheduler";
opts.PathPrefix = "/scheduler-management";
// No auth guard here — open for development.
// In production set AuthorizeAsync:
// opts.AuthorizeAsync = ctx => Task.FromResult(ctx.User.IsInRole("Admin"));
});
// Mount — after builder.Build()
app.UseSchedulerViewer(); // auth guard middleware
app.MapSchedulerViewer(); // MVC routes under /scheduler-managementThe viewer delegates auth entirely to your application. Set the AuthorizeAsync delegate to any logic that fits your stack:
// ASP.NET Core Identity role
opts.AuthorizeAsync = ctx =>
Task.FromResult(ctx.User.Identity?.IsAuthenticated == true
&& ctx.User.IsInRole("Admin"));
// API key header
opts.AuthorizeAsync = ctx =>
{
var key = ctx.Request.Headers["X-Scheduler-Key"].FirstOrDefault();
return Task.FromResult(key == "my-secret");
};
// Cookie
opts.AuthorizeAsync = ctx =>
Task.FromResult(ctx.Request.Cookies["scheduler_auth"] == "my-token");
// ASP.NET Core policy
opts.AuthorizeAsync = async ctx =>
{
var svc = ctx.RequestServices.GetRequiredService<IAuthorizationService>();
var result = await svc.AuthorizeAsync(ctx.User, "SchedulerAdminPolicy");
return result.Succeeded;
};
⚠️ AuthorizeAsync = null(the default) allows all requests. Only use this in development.
The REST controllers show how you would drive scheduling from your own frontend (React, Vue, Angular, mobile) or any HTTP client. They are a direct complement to the built-in viewer: the viewer reads, the API writes.
Wraps IScheduleRepository. Every scheduling operation available in the library has a corresponding endpoint.
| Method | Endpoint | Action |
|---|---|---|
GET |
/api/scheduler/jobs |
List all discovered job types |
POST |
/api/scheduler/count-customers/schedule |
Set cron for CountCustomersJob |
POST |
/api/scheduler/count-customers/reschedule |
Change cron |
POST |
/api/scheduler/count-customers/pause |
Pause trigger |
POST |
/api/scheduler/count-customers/resume |
Resume trigger |
DELETE |
/api/scheduler/count-customers |
Remove trigger |
POST |
/api/scheduler/generate-report/schedule |
Set cron for GenerateReportJob |
POST |
/api/scheduler/generate-report/reschedule |
Change cron |
POST |
/api/scheduler/generate-report/pause |
Pause |
POST |
/api/scheduler/generate-report/resume |
Resume |
DELETE |
/api/scheduler/generate-report |
Remove |
| Method | Endpoint | Action |
|---|---|---|
POST |
/api/scheduler/send-emails/schedule |
Create a recurring email campaign |
POST |
/api/scheduler/send-emails/schedule-once |
Run an email batch once |
POST |
/api/scheduler/send-emails/{scheduleKey}/reschedule |
Change campaign cron |
POST |
/api/scheduler/send-emails/{scheduleKey}/pause |
Pause campaign |
POST |
/api/scheduler/send-emails/{scheduleKey}/resume |
Resume campaign |
DELETE |
/api/scheduler/send-emails/{scheduleKey} |
Cancel campaign |
POST |
/api/scheduler/notify-customer/schedule |
Schedule recurring notification |
POST |
/api/scheduler/notify-customer/schedule-once |
Send one-off notification |
POST |
/api/scheduler/notify-customer/{scheduleKey}/reschedule |
Change notification cron |
POST |
/api/scheduler/notify-customer/{scheduleKey}/pause |
Pause |
POST |
/api/scheduler/notify-customer/{scheduleKey}/resume |
Resume |
DELETE |
/api/scheduler/notify-customer/{scheduleKey} |
Cancel |
# Schedule CountCustomersJob every 30 seconds
curl -X POST https://localhost:5001/api/scheduler/count-customers/schedule \
-H "Content-Type: application/json" \
-d '{"cronExpression":"0/30 * * * * ?"}'
# Create an email campaign that runs every day at 9 AM
curl -X POST https://localhost:5001/api/scheduler/send-emails/schedule \
-H "Content-Type: application/json" \
-d '{
"scheduleKey": "acme-weekly",
"cronExpression": "0 0 9 * * ?",
"subject": "Weekly Update",
"body": "Hello from ACME!",
"filterByDomain": "acme.com"
}'
# Send a one-off notification to customer #1 right now
curl -X POST https://localhost:5001/api/scheduler/notify-customer/schedule-once \
-H "Content-Type: application/json" \
-d '{
"customerId": 1,
"channel": "push",
"message": "Your order has shipped!"
}'
# Pause the campaign
curl -X POST https://localhost:5001/api/scheduler/send-emails/acme-weekly/pause
# Cancel the campaign
curl -X DELETE https://localhost:5001/api/scheduler/send-emails/acme-weeklyWraps IScheduleReader. Use these endpoints to populate your own dashboard or monitoring page.
| Method | Endpoint | Action |
|---|---|---|
GET |
/api/dashboard/running |
All currently running executions |
GET |
/api/dashboard/count-customers/last |
Last execution of CountCustomersJob |
GET |
/api/dashboard/count-customers/recent?limit=20 |
Recent executions |
GET |
/api/dashboard/count-customers/failed?since=2026-01-01 |
Failed executions since date |
GET |
/api/dashboard/generate-report/last |
Last execution of GenerateReportJob |
GET |
/api/dashboard/generate-report/recent |
Recent executions |
GET |
/api/dashboard/generate-report/failed |
Failed executions |
GET |
/api/dashboard/send-emails/{scheduleKey}/last |
Last execution of a campaign |
GET |
/api/dashboard/send-emails/{scheduleKey}/recent |
Recent campaign executions |
GET |
/api/dashboard/send-emails/{scheduleKey}/failed |
Failed campaign executions |
GET |
/api/dashboard/notify-customer/{scheduleKey}/last |
Last notification execution |
GET |
/api/dashboard/notify-customer/{scheduleKey}/recent |
Recent notification executions |
GET |
/api/dashboard/notify-customer/{scheduleKey}/failed |
Failed notification executions |
[Schedule("0 * * * * ?", Description = "Count customers every minute")]
[ScheduleConfig(AllowConcurrentExecution = false, MisfireInstructions = MisfireInstructions.FireOnce)]
[RetryConfig(MaxRetries = 3, RetryAfterMinutes = 2)]
public class CountCustomersJob : IScheduledJob
{
public async Task Execute() { /* counts customers, logs total */ }
}- Starts automatically at startup via
[Schedule] - Can be overridden at runtime via
POST /api/scheduler/count-customers/schedule - Retries up to 3 times with a 2-minute gap on failure
[Schedule("0 0 6 * * ?", Description = "Daily customer report at 6 AM")]
[ScheduleConfig(AllowConcurrentExecution = false, MisfireInstructions = MisfireInstructions.Skip)]
public class GenerateReportJob : IScheduledJob
{
public async Task Execute() { /* logs all customer names */ }
}- Starts at 6 AM daily; misfired triggers are skipped (no catch-up)
- No
[RetryConfig]— a failed report is simply logged and dropped
[ScheduleConfig(AllowConcurrentExecution = true, MisfireInstructions = MisfireInstructions.Skip)]
[RetryConfig(MaxRetries = 5, RetryAfterMinutes = 10)]
public class SendCustomerEmailsJob : IScheduledJob<SendEmailsParams>
{
public async Task Execute(SendEmailsParams p) { /* sends emails */ }
}
public record SendEmailsParams(string Subject, string Body, string? FilterByDomain = null);- Runtime-only — no
[Schedule]attribute - Multiple campaigns (different
scheduleKeyvalues) run independently and concurrently - Each
scheduleKeyis a separate Quartz job with its own data map and retry counter
[ScheduleConfig(AllowConcurrentExecution = false, MisfireInstructions = MisfireInstructions.Skip)]
public class NotifyCustomerJob : IScheduledJob<NotifyCustomerParams>
{
public async Task Execute(NotifyCustomerParams p) { /* sends SMS/push/webhook */ }
}
public record NotifyCustomerParams(int CustomerId, string Channel, string Message);- Runtime-only
AllowConcurrentExecution = falseper schedule key ensures the same customer isn't notified in parallel by the same recurring schedule
The sample uses an in-memory store by default. To use PostgreSQL, replace the AddScheduler block in Program.cs:
// Before (in-memory):
builder.Services.AddScheduler(options => { ... }, typeof(Program).Assembly);
// After (PostgreSQL):
builder.Services.AddPgSqlScheduler(
connectionString: builder.Configuration.GetConnectionString("Postgres")!,
schema: "quartz",
configureOptions: options => { ... },
assemblies: typeof(Program).Assembly);And update AppDbContext.OnModelCreating:
// Before:
modelBuilder.ApplyScheduling();
// After:
modelBuilder.UseSchedulerPostgreSql("quartz");Then run migrations:
dotnet ef migrations add AddScheduler
dotnet ef database updateThe same pattern applies for SQL Server (UseSchedulerSqlServer) and MySQL (UseSchedulerMySql).