SolTechnology.Core.Hangfire
1.0.0
dotnet add package SolTechnology.Core.Hangfire --version 1.0.0
NuGet\Install-Package SolTechnology.Core.Hangfire -Version 1.0.0
<PackageReference Include="SolTechnology.Core.Hangfire" Version="1.0.0" />
<PackageVersion Include="SolTechnology.Core.Hangfire" Version="1.0.0" />
<PackageReference Include="SolTechnology.Core.Hangfire" />
paket add SolTechnology.Core.Hangfire --version 1.0.0
#r "nuget: SolTechnology.Core.Hangfire, 1.0.0"
#:package SolTechnology.Core.Hangfire@1.0.0
#addin nuget:?package=SolTechnology.Core.Hangfire&version=1.0.0
#tool nuget:?package=SolTechnology.Core.Hangfire&version=1.0.0
SolTechnology.Core.Hangfire
Hangfire-backed persistent event dispatch and recurring jobs for SolTechnology.Core.CQRS.
Events survive process restarts via Hangfire storage; recurring jobs run on cron schedules
with typed, DI-resolved handlers. Intentionally minimal public surface —
new knobs require an ADR.
Features
- Persistent events —
IEventinstances are enqueued as Hangfire background jobs. A crash between publish and dispatch does not lose the event. - Recurring jobs —
IJobimplementations run on cron schedules, registered with a one-liner and resolved from DI at execution time. - Plugin model — opt in with
AddPersistentEvents()afterAddCQRS(). Does not replace in-memory dispatch unless explicitly installed.
Registration
The plugin offers two independent features — use either or both:
// Persistent events only (requires AddCQRS first)
builder.Services.AddCQRS(assemblies: typeof(Program).Assembly);
builder.Services.AddPersistentEvents();
// Recurring jobs only (no AddCQRS dependency)
builder.Services.AddRecurringJob<MyDailyJob>(Cron.Daily);
// Both together
builder.Services.AddCQRS(assemblies: typeof(Program).Assembly);
builder.Services.AddPersistentEvents();
builder.Services.AddRecurringJob<MyDailyJob>(Cron.Daily);
| Method | Requires AddCQRS() |
What it does |
|---|---|---|
AddPersistentEvents() |
✅ yes | Replaces in-memory event publisher with Hangfire-backed durable dispatch |
AddRecurringJob<TJob>(cron) |
❌ no | Registers a typed job on a cron schedule via Hangfire |
Configuration
builder.Services.AddPersistentEvents(o => o.QueueName = "events");
| Option | Default | Description |
|---|---|---|
QueueName |
"default" |
Hangfire queue used for event dispatch jobs. |
Usage — Persistent Events
Define an event and its handler(s) in your CQRS layer:
public class CitySearched : IEvent
{
public City City { get; set; }
}
public class SaveCitySearchHandler(ICityDomainService cityDomainService) : IEventHandler<CitySearched>
{
public async Task Handle(CitySearched @event, CancellationToken cancellationToken)
{
await cityDomainService.Save(@event.City);
}
}
Publish from any handler via IMediator:
mediator.Publish(new CitySearched { City = result });
With AddPersistentEvents() installed, the event is enqueued as a Hangfire job and
dispatched in a fresh DI scope when the server picks it up.
Usage — Recurring Jobs
public class FetchTrafficJob(ITrafficClient client) : IJob
{
public async Task Execute(CancellationToken cancellationToken)
{
await client.FetchLatest(cancellationToken);
}
}
// Registration (Program.cs)
builder.Services.AddRecurringJob<FetchTrafficJob>("0 */6 * * *"); // every 6 hours
The job id is stable (typeof(TJob).Name) — re-registration on deploy updates rather
than duplicates the schedule.
App-Side Requirements
The plugin depends on Hangfire.Core only. The app must provide:
A DI-aware Hangfire
JobActivator— supplied byHangfire.AspNetCore(viaAddHangfireServer()) orGlobalConfiguration.UseActivator(...). Without it, Hangfire cannot resolve the plugin's internal dispatcher/runner types.Type-aware job-argument serialisation —
IEventis serialised through an interface-typed parameter. Configure:builder.Services.AddHangfire(config => config .UseSqlServerStorage(connectionString) .UseRecommendedSerializerSettings() // TypeNameHandling.Auto .UseSimpleAssemblyNameTypeSerializer());Without this, the event cannot be deserialised when the job runs.
Hangfire server —
builder.Services.AddHangfireServer()so jobs are actually processed. Without it, jobs are persisted but never fire.
The
Newtonsoft.Json13.0.4 pin (CVE-2024-21907) is satisfied transitively by the plugin's own package reference.
Retry & Resilience
Persistent events dispatch once ([AutomaticRetry(Attempts = 0)]). A failed handler
surfaces as a Failed job in the Hangfire dashboard — visible and manually re-queueable.
Resilience is a handler-level concern (e.g. Polly inside the handler). The plugin does not add automatic retries.
Persistence buys durability (a job enqueued before a crash runs after restart) and at-least-once delivery (crash-recovery or manual re-queue). Handlers must be idempotent.
Rate Limiting
Out of scope for this Hangfire.Core-only plugin. Concurrency is bounded app-side via
AddHangfireServer(o => o.WorkerCount = N) and named queues. True jobs-per-interval
rate limiting requires Hangfire.Throttling or infrastructure-level controls.
Event Payload Guideline
Persisted events may be re-dispatched (at-least-once). Prefer small, immutable payloads
carrying IDs — reload current state inside the handler. CitySearched carries its full
payload as a deliberate exception (freshly fetched city not yet in the store, no id to
reload by).
Public API Surface
Intentionally minimal:
| Symbol | Kind |
|---|---|
AddPersistentEvents() |
Extension method |
AddRecurringJob<TJob>(cron) |
Extension method |
IJob |
Interface |
PersistentEventsOptions |
Options class |
IEvent, IEventHandler<T>, IEventPublisher, IEventDispatcher live in
SolTechnology.Core.CQRS — see CQRS docs.
Filters
The plugin ships three Hangfire job filters. Register them globally via
UseSolTechnologyFilters() in the app's AddHangfire callback:
builder.Services.AddHangfire((sp, config) => config
.UseRecommendedSerializerSettings()
.UseSimpleAssemblyNameTypeSerializer()
.UseSqlServerStorage(connectionString)
.UseSolTechnologyFilters(sp)); // registers correlation + smart-retry filters
Correlation-id propagation
Preserves the X-Correlation-Id across the enqueue→execute boundary. On enqueue
the current correlation id is saved as a job parameter; on execute it is restored
into ICorrelationIdService and pushed as a log scope — so logs from background
handlers appear under the same correlation as the original request.
Smart retry (Result-aware)
Bridges the Result pattern with Hangfire's exception-only retry model. If a job
returns Result.Fail(new Error { Recoverable = true }), the filter forces the job
into FailedState so Hangfire retries it. Non-recoverable failures are left as
succeeded — no pointless retries, no silent swallowing.
Prevent overlap
Cancels a job execution if another instance with the same method + arguments is already scheduled or processing. Prevents pile-up when a recurring job is mid-retry and the next cron trigger fires. Apply as an attribute on a job method:
[PreventOverlapJobFilter]
public async Task RunAsync(CancellationToken cancellationToken) { ... }
Or register for a specific recurring job:
builder.Services.AddRecurringJob<MyJob>(Cron.Hourly, preventOverlap: true);
Testing
In component/integration tests, do not call AddPersistentEvents(). The default
in-memory IEventPublisher from AddCQRS() dispatches events in-process (fire-and-forget)
— no Hangfire infrastructure needed, no test doubles required.
// Test fixture — just use AddCQRS(), no publisher swap needed
builder.Services.AddCQRS(assemblies: typeof(Program).Assembly);
// Events dispatch in-memory; handlers run in background Task.Run with a fresh scope.
If your tests assert on handler side-effects, the in-memory publisher's Task.Run dispatch
is fast enough for most scenarios. For strict synchronous guarantees, assert with a short
poll/retry — not a custom publisher.
Recommended defaults
Retry backoff
SmartRetryJobFilter applies this 10-attempt schedule by default:
10s → 30s → 1m → 5m → 15m → 30m → 1h → 2h → 4h → 8h
After the 10th failure, the job is moved to the Failed state (visible in the dashboard).
Override per-job with [SmartRetry(attempts: 5)] if you need fewer attempts.
Worker count
Hangfire defaults to Environment.ProcessorCount * 2 workers. This is a good starting
point for IO-bound handlers (events, notifications). Do not override unless you have
measured contention — more workers on CPU-bound work causes context-switch overhead.
Database migration
Hangfire requires its own schema tables (HangfireJob, HangfireState, HangfireServer,
etc.). The storage provider creates them automatically on first use via
SqlServerStorage.Install(...), which is idempotent — safe to call on every startup.
// Program.cs — call before UseHangfireServer/Dashboard
var connectionString = builder.Configuration.GetConnectionString("Hangfire");
Hangfire.SqlServer.SqlServerObjectsInstaller.Install(
new System.Data.SqlClient.SqlConnection(connectionString));
Why no framework helper? The storage (connection string, database, schema) is owned by the application, not by
SolTechnology.Core.Hangfire. The framework registers jobs and event dispatch; the application owns infrastructure. This keeps the library independent of any specific ADO.NET provider or migration tool.
See Also
- ADR-009 — Persistent events and recurring jobs
- CQRS — commands, queries, events (in-memory default)
- Cron / Scheduler (deprecated)
Decision note: Delay-queue vs Hangfire scheduled jobs
TL;DR: use Hangfire scheduled jobs. Don't build a bespoke delay-queue.
The pattern: "hold a message until time T, then process it." Examples: send a reminder email in 24h, retry a failed webhook in 5 minutes, expire a draft after 7 days.
Hangfire already does this:
// Fire-and-forget in 5 minutes
BackgroundJob.Schedule(() => handler.ProcessAsync(payload), TimeSpan.FromMinutes(5));
// Or via the typed IJob + cron for recurring
services.AddRecurringJob<ExpireDraftsJob>("0 */6 * * *"); // every 6 hours
When NOT to use Hangfire: if you need sub-second delay precision or millions of pending messages (Hangfire polls SQL storage — latency floor ~15s, throughput ceiling ~100/s per server). In that case, use Azure Service Bus scheduled messages or a Redis sorted set.
Why not build a library primitive:
- Hangfire gives you dashboard visibility, retry, dead-letter, correlation (via
CorrelationIdJobFilter), and persistence for free. - A bespoke delay-queue duplicates that stack without the observability.
- See also: FI-002 — Priority background jobs.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Hangfire.Core (>= 1.8.23)
- Microsoft.Extensions.Configuration (>= 10.0.9)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.9)
- Microsoft.Extensions.DependencyInjection (>= 10.0.9)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Options (>= 10.0.9)
- Newtonsoft.Json (>= 13.0.4)
- SolTechnology.Core (>= 1.0.0)
- SolTechnology.Core.CQRS (>= 1.0.0)
- SolTechnology.Core.Logging (>= 1.2.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 61 | 7/6/2026 |