Getting Started
ZeroAlloc.Scheduling is a background job scheduler for .NET 8 and .NET 10. You decorate a class with [Job], and the Roslyn source generator emits the executor, DI registration, and optional recurring startup for you at build time — no reflection, no convention scanning, no IServiceCollection.Scan.
Installation
dotnet add package ZeroAlloc.Scheduling
dotnet add package ZeroAlloc.Scheduling.InMemory
The generator runs as an analyzer:
<PackageReference Include="ZeroAlloc.Scheduling.Generator" Version="*"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
Your First Job
Step 1 — Define the job
Implement IJob and decorate with [Job].
using ZeroAlloc.Scheduling;
[Job]
public sealed class SendWelcomeEmailJob : IJob
{
public required string To { get; init; }
public async ValueTask ExecuteAsync(JobContext ctx, CancellationToken ct)
{
// ctx.JobId, ctx.Attempt, ctx.ScheduledAt are available
Console.WriteLine($"Sending welcome email to {To} (attempt {ctx.Attempt})");
await Task.Delay(100, ct); // simulate work
}
}
Step 2 — Register
The generator emits AddSendWelcomeEmailJob(). Call it alongside AddScheduling and your chosen backend.
builder.Services
.AddScheduling()
.AddSchedulingInMemory()
.AddSendWelcomeEmailJob();
Step 3 — Enqueue
Inject IScheduler and enqueue.
public class UserService(IScheduler scheduler)
{
public async Task RegisterAsync(string email, CancellationToken ct)
{
// ... create user ...
await scheduler.EnqueueAsync(new SendWelcomeEmailJob { To = email }, ct);
}
}
Step 4 — Build and run
dotnet run
The background worker polls the store every 5 seconds (configurable), claims pending jobs, and executes them. On failure it retries with exponential backoff up to DefaultMaxAttempts (default: 3), then dead-letters.
Recurring Jobs
Add Every or Cron to the attribute. The generator also emits an IHostedService that seeds the schedule on startup.
[Job(Every = Every.Hour)]
public sealed class PurgeExpiredSessionsJob : IJob
{
public async ValueTask ExecuteAsync(JobContext ctx, CancellationToken ct) { ... }
}
// Or with a custom cron expression:
[Job(Cron = "0 9 * * 1-5")] // weekdays at 09:00 UTC
public sealed class DailyDigestJob : IJob { ... }
Register the same way — AddPurgeExpiredSessionsJob() registers both the executor and the startup service.
Dashboard
Serve the built-in HTML dashboard with one line:
app.MapJobsDashboard("/jobs");
Open /jobs/ in your browser to see pending, running, succeeded, failed, and dead-lettered jobs with live auto-refresh.
Configuration
builder.Services.AddScheduling(opt =>
{
opt.PollingInterval = TimeSpan.FromSeconds(5); // default
opt.BatchSize = 20; // jobs per poll
opt.RetryBaseDelay = TimeSpan.FromSeconds(2); // exponential base
opt.DefaultMaxAttempts = 3; // global retry limit
});
Next Steps
| Guide | What you'll learn |
|---|---|
| Source Generator | All [Job] options, Every enum, generated code |
| Backends | Switch from InMemory to EF Core or Redis |
| Dashboard | Dashboard options, Blazor component |
| Mediator Bridge | Route jobs through ZeroAlloc.Mediator |
| Performance | Tuning batch size, polling interval, concurrency |