Hangfire 1.8 powers every background job in fullstackhero. The Jobs building block wraps it with four kit-specific filters: FshJobFilter preserves tenant + user context across enqueue → execute, FshJobActivator resolves jobs from a scoped DI container per execution, HangfireTelemetryFilter opens an OpenTelemetry activity per job, and LogJobFilter logs every state transition.
How it’s wired
builder.AddHeroPlatform(o => o.EnableJobs = true); // turns on AddHeroJobsUseHeroPlatform calls UseHeroJobDashboard for you, mounting the dashboard at HangfireOptions:Route (default /jobs).
Storage provider falls out of DatabaseOptions:Provider:
PostgreSQL→Hangfire.PostgreSqlMSSQL→ SQL Server storage
The kit defaults to 5 workers, 30-second heartbeat, queues default + email, 30-second polling.
Enqueueing a job
Inject IJobService and call Enqueue / Schedule:
public sealed class SendInvoiceEmailCommandHandler(IJobService jobs) : ICommandHandler<SendInvoiceEmailCommand, Unit>{ public ValueTask<Unit> Handle(SendInvoiceEmailCommand cmd, CancellationToken ct) { jobs.Enqueue<SendInvoiceEmailJob>(j => j.RunAsync(cmd.InvoiceId, CancellationToken.None)); return ValueTask.FromResult(Unit.Value); }}Hangfire serializes the call signature; the activator resolves SendInvoiceEmailJob from a fresh DI scope when the job dequeues. The job can inject DbContext, IMailService, ICurrentUser, etc.
Recurring jobs in the kit
| Job (recurring job id) | Module | Cron |
|---|---|---|
MonthlyInvoiceJob (billing-monthly-invoices) | Billing | 5 0 1 * * — 00:05 UTC on the 1st of each month |
PurgeOrphanedFilesJob (files-purge-orphans) | Files | 0 * * * * — hourly |
PurgeDeletedFilesJob (files-purge-deleted) | Files | 30 3 * * * — 03:30 UTC daily |
AuditRetentionJob (auditing-retention) | Auditing | 30 3 * * * default (configurable; the job is a no-op until Auditing:Retention:Enabled is true) |
TenantExpiryScanJob (tenant-expiry-scan) | Multitenancy | 0 2 * * * — 02:00 UTC daily |
The eventing outbox is not a Hangfire job — it’s dispatched by the framework’s OutboxDispatcherHostedService. (The host even ships a cleanup service that removes retired {module}-outbox-dispatcher recurring jobs from older deployments.)
Register your own with IRecurringJobManager:
recurringJobs.AddOrUpdate<MyHourlyJob>( "my-hourly-job", job => job.RunAsync(CancellationToken.None), "0 * * * *", // top of every hour TimeZoneInfo.Utc);Retry policies
Per-job retry via the [AutomaticRetry] attribute on the job method:
// WebhookDispatchJob[AutomaticRetry( Attempts = 4, DelaysInSeconds = new[] { 30, 120, 600, 3600 }, OnAttemptsExceeded = AttemptsExceededAction.Fail)]public async Task DispatchAsync(/* ... */) { /* ... */ }The Webhooks module uses this exact pattern — 5 attempts total (1 initial + 4 retries) with 30 s / 2 min / 10 min / 1 h exponential backoff. The Files purge jobs carry their own tuned attributes ([AutomaticRetry(Attempts = 3, DelaysInSeconds = [30, 120, 600])] for orphans; 2 attempts for deleted-file purges).
Dashboard
UseHeroJobDashboard (called inside UseHeroPlatform) mounts the Hangfire dashboard at HangfireOptions:Route (default /jobs), gated by HangfireCustomBasicAuthenticationFilter.
{ "HangfireOptions": { "UserName": "admin", "Password": "set-via-secrets", // min 12 chars — validated at startup "Route": "/jobs" }}There are no safe defaults: UserName (min 3 chars) and Password (min 12 chars) are [Required] and validated at startup via ValidateDataAnnotations().ValidateOnStart() — the host won’t boot with jobs enabled and empty credentials. Local dev gets admin / Password123! from appsettings.Development.json (the Aspire AppHost injects the same); set real values via env vars or a vault in prod. Always HTTPS in production; consider IP whitelisting in the reverse proxy on top of basic auth.
Gotchas
- Hangfire serializes arguments. Pass primitives (Guid, string, int) — not large object graphs. If you need state, look it up inside the job from the DI-resolved DbContext.
FshJobActivatorcreates a new scope per job. Don’t try to share state via scoped services across job invocations; each gets a clean container. Use Valkey / DB for cross-job state.- Stale-lock cleanup runs ~5s after host startup.
HangfireStaleLockCleanupServicereleases locks from servers that crashed without releasing. The delay avoids interfering with legitimate startup-time job acquisition. - Background jobs and tenant context. Enqueuing happens inside a request scope (so
ICurrentUserworks). Execution happens on a background thread (so it doesn’t).FshJobFilterbridges this — but if you write your own filter or skip the kit’s activator, restore the tenant context manually.
Related
- Jobs building block — the implementation reference.
- Billing module —
MonthlyInvoiceJobrecurring schedule. - Files module — orphan + retention purge jobs.
- Webhooks module —
[AutomaticRetry]with exponential backoff.