The Jobs block wires Hangfire 1.8 into the kit. The interesting parts are the tenant-aware job filter (preserves multi-tenant context across enqueue → execute), the DI-aware activator (jobs get scoped service resolution), the OpenTelemetry filter (activities for every job), the basic-auth dashboard at /hangfire, and a stale-lock cleanup service for after-crash recovery.
What it ships
Extensions
AddHeroJobs(services)— registers the Hangfire server with 5 workers, 30-second heartbeat, queuesdefault+email, and 30-second polling. ReadsDatabaseOptions:Providerto select Postgres (Hangfire.PostgreSql) or SQL Server storage. RegistersFshJobActivator,FshJobFilter,LogJobFilter,HangfireTelemetryFilter, plusHangfireStaleLockCleanupService.UseHeroJobDashboard(app, configuration)— wires the Hangfire dashboard at the route configured inHangfireOptions:Route(default/hangfire) behind a basic-auth filter usingHangfireOptions:UserName+HangfireOptions:Password.
Service interface
IJobService— convenience facade over Hangfire’s staticBackgroundJobAPI.EnqueueAsync,ScheduleAsync,AddOrUpdateRecurringJob, etc.HangfireService : IJobService— implementation.
Filters
FshJobActivator : IJobActivator— resolves job classes from a freshIServiceScopeper job, so every job gets a clean scoped DbContext, ICurrentUser, etc.FshJobFilter— captures tenant + user at enqueue, restores them inside the activator’s scope before the job runs.LogJobFilter— logs every state transition (Enqueued, Processing, Succeeded, Failed) throughILogger.HangfireTelemetryFilter— opens an OpenTelemetry activity per job withjob.name,job.queue,job.idtags.HangfireCustomBasicAuthenticationFilter— gates the dashboard via basic auth.
Hosted service
HangfireStaleLockCleanupService— runs a few seconds after host startup, queries the storage for orphaned locks (held by a server that crashed), and releases them.
Options
HangfireOptions—UserName,Password,Route.
How modules consume Jobs
Inject IJobService to enqueue:
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); }}Or register a recurring job during module startup:
public void ConfigureServices(IHostApplicationBuilder builder){ builder.Services.AddSingleton<IConfigureOptions<RecurringJobsConfig>>(/* … */);}
// somewhere in MapMiddlewares or a hosted servicerecurringJobManager.AddOrUpdate<MonthlyInvoiceJob>( "billing-monthly-invoices", job => job.RunAsync(CancellationToken.None), "5 0 1 * *"); // 00:05 UTC on the 1st of each monthThe Billing module ships MonthlyInvoiceJob; the Files module ships PurgeOrphanedFilesJob (hourly) and PurgeDeletedFilesJob (daily); the Auditing module ships AuditRetentionJob (opt-in).
Configuration
{ "HangfireOptions": { "UserName": "admin", "Password": "set-via-secrets", "Route": "/hangfire" }}The storage provider falls out of DatabaseOptions:Provider — PostgreSQL routes to Hangfire.PostgreSql; MSSQL routes to the SQL Server storage.
How to extend
Add a new queue
Hangfire dispatches by queue name. Register your job with [Queue("priority")] and add the queue name to the server’s queue array in AddHeroJobs:
services.AddHangfireServer(opts => opts.Queues = new[] { "default", "email", "priority" });Decorate jobs with a custom filter
Implement IServerFilter and register it in DI; AddHangfireServer will pick it up. The kit’s filters are templates — read FshJobFilter.cs for the lifecycle hooks (OnPerforming, OnPerformed).
Use Hangfire’s [AutomaticRetry] attribute
For per-job retry policies:
[AutomaticRetry(Attempts = 4, OnAttemptsExceeded = AttemptsExceededAction.Fail, DelaysInSeconds = new[] { 30, 120, 600, 3600 })]public sealed class WebhookDispatchJob { /* ... */ }The Webhooks module uses this exact pattern.
Gotchas
- Hangfire serializes arguments when enqueuing. 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 hold scoped state across jobs; each invocation gets a fresh DbContext.- Dashboard auth is basic. Use HTTPS in production. Consider IP whitelisting in your reverse proxy in addition.
- Stale-lock cleanup runs after startup. It defers ~10 seconds so it doesn’t interfere with legitimate startup-time job acquisition; you’ll see the cleanup log entries a few seconds after the host is fully up.
Critical files
src/BuildingBlocks/Jobs/Extensions.cssrc/BuildingBlocks/Jobs/Services/HangfireService.cssrc/BuildingBlocks/Jobs/Filters/FshJobFilter.cssrc/BuildingBlocks/Jobs/Filters/FshJobActivator.cssrc/BuildingBlocks/Jobs/HangfireStaleLockCleanupService.cs
Related
- Web —
FshPlatformOptions.EnableJobstoggle. - Billing module —
MonthlyInvoiceJobrecurring schedule. - Files module — orphan + retention purge jobs.
- Webhooks module —
[AutomaticRetry]with exponential backoff.