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 /jobs, 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/jobs) behind a basic-auth filter usingHangfireOptions:UserName+HangfireOptions:Password.
Service interface
IJobService— convenience facade over Hangfire’s staticBackgroundJobAPI:Enqueue/Enqueue<T>(with an optional queue overload),Schedule/Schedule<T>(byTimeSpandelay orDateTimeOffset),Delete,Requeue. All return/accept Hangfire job-id strings. Recurring jobs go through Hangfire’sIRecurringJobManagerdirectly.HangfireService : IJobService— implementation.
Filters
FshJobActivator : JobActivator— resolves job classes from a freshIServiceScopeper job, restoring the captured tenant (via Finbuckle’sIMultiTenantContextSetter) and user id (viaICurrentUserInitializer) before resolution — so the job’s scoped DbContext is built with the right tenant.FshJobFilter : IClientFilter— at enqueue time, stamps the current tenant + user id onto the job as parameters (skipped when there’s no HTTP context, e.g. recurring-job creation).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(required, min 3 chars),Password(required, min 12 chars — both validated at startup, no safe default),Route(default/jobs).
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 — the kit’s pattern is to resolve IRecurringJobManager inside the module’s MapEndpoints (it’s null when jobs are disabled, so guard it):
var jobManager = endpoints.ServiceProvider.GetService<IRecurringJobManager>();if (jobManager is not null){ // Fire at 00:05 UTC on the 1st of every month; the job bills the previous period. jobManager.AddOrUpdate( "billing-monthly-invoices", Job.FromExpression<MonthlyInvoiceJob>(j => j.RunAsync(CancellationToken.None)), "5 0 1 * *", new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });}The Billing module ships MonthlyInvoiceJob (monthly); the Files module ships PurgeOrphanedFilesJob (hourly) and PurgeDeletedFilesJob (daily, 03:30 UTC); the Auditing module ships AuditRetentionJob (cron from AuditRetentionOptions, no-op until enabled).
Configuration
{ "HangfireOptions": { "UserName": "admin", "Password": "set-via-secrets-min-12-chars", "Route": "/jobs" }}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 a few seconds so it doesn’t interfere with legitimate startup-time lock acquisition; it’s best-effort and logs a warning with the count when it actually releases something.
Critical files
src/BuildingBlocks/Jobs/Extensions.cssrc/BuildingBlocks/Jobs/Services/HangfireService.cssrc/BuildingBlocks/Jobs/FshJobFilter.cssrc/BuildingBlocks/Jobs/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.