Dosaic.Plugins.Jobs.Hangfire
1.2.31
dotnet add package Dosaic.Plugins.Jobs.Hangfire --version 1.2.31
NuGet\Install-Package Dosaic.Plugins.Jobs.Hangfire -Version 1.2.31
<PackageReference Include="Dosaic.Plugins.Jobs.Hangfire" Version="1.2.31" />
<PackageVersion Include="Dosaic.Plugins.Jobs.Hangfire" Version="1.2.31" />
<PackageReference Include="Dosaic.Plugins.Jobs.Hangfire" />
paket add Dosaic.Plugins.Jobs.Hangfire --version 1.2.31
#r "nuget: Dosaic.Plugins.Jobs.Hangfire, 1.2.31"
#:package Dosaic.Plugins.Jobs.Hangfire@1.2.31
#addin nuget:?package=Dosaic.Plugins.Jobs.Hangfire&version=1.2.31
#tool nuget:?package=Dosaic.Plugins.Jobs.Hangfire&version=1.2.31
Dosaic.Plugins.Jobs.Hangfire
Dosaic.Plugins.Jobs.Hangfire is a plugin that allows Dosaic-based services to schedule and manage background jobs using Hangfire. It supports recurring (cron) jobs, fire-and-forget jobs, delayed jobs, PostgreSQL or in-memory storage, a built-in dashboard, OpenTelemetry tracing, Prometheus metrics, and feature-flag-based job execution control.
Installation
dotnet add package Dosaic.Plugins.Jobs.Hangfire
or add as a package reference to your .csproj:
<PackageReference Include="Dosaic.Plugins.Jobs.Hangfire" Version="" />
Configuration
Configure appsettings.yml (or appsettings.json) with the hangfire section:
hangfire:
# PostgreSQL storage (ignored when inMemory: true)
host: localhost
port: 5432
database: postgres
user: postgres
password: postgres
# Use in-memory storage instead of PostgreSQL (useful for development)
inMemory: true
# Hostname from which the Hangfire dashboard is accessible.
# Leave empty to disable dashboard access entirely.
allowedDashboardHost: localhost
# Enable Microsoft Feature Management integration to toggle jobs via config
enableJobsByFeatureManagementConfig: false
# Additional queues to listen on (the "default" queue is always included)
queues:
- default
- critical
# Optional tuning
pollingIntervalInMs: 5000 # defaults to Hangfire built-in value
workerCount: 10 # defaults to Hangfire built-in value
invisibilityTimeoutInMinutes: 30
maxJobArgumentsSizeToRenderInBytes: 4096 # max bytes of job args displayed in dashboard
Configuration class reference
[Configuration("hangfire")]
public class HangfireConfiguration
{
public string Host { get; set; }
public int Port { get; set; }
public string Database { get; set; }
public string User { get; set; }
public string Password { get; set; }
public bool InMemory { get; set; }
public string AllowedDashboardHost { get; set; }
public bool EnableJobsByFeatureManagementConfig { get; set; }
public int? PollingIntervalInMs { get; set; }
public int? WorkerCount { get; set; }
public string[] Queues { get; set; } = [EnqueuedState.DefaultQueue];
public int InvisibilityTimeoutInMinutes { get; set; } = 30;
public int MaxJobArgumentsSizeToRenderInBytes { get; set; } = 4096;
public string ConnectionString => $"Host={Host};Port={Port};Database={Database};Username={User};Password={Password};";
}
Usage
Defining jobs
Simple async job (no parameters)
Extend AsyncJob and implement ExecuteJobAsync:
public class SendDailyReportJob : AsyncJob
{
private readonly IReportService _reportService;
public SendDailyReportJob(ILogger<SendDailyReportJob> logger, IReportService reportService)
: base(logger)
{
_reportService = reportService;
}
protected override async Task<object> ExecuteJobAsync(CancellationToken cancellationToken)
{
await _reportService.SendAsync(cancellationToken);
return "done";
}
}
Parameterized async job
Extend ParameterizedAsyncJob<T> when the job requires input:
public class ProcessOrderJob : ParameterizedAsyncJob<int>
{
private readonly IOrderService _orderService;
public ProcessOrderJob(ILogger<ProcessOrderJob> logger, IOrderService orderService)
: base(logger)
{
_orderService = orderService;
}
protected override async Task<object> ExecuteJobAsync(int orderId, CancellationToken cancellationToken)
{
var result = await _orderService.ProcessAsync(orderId, cancellationToken);
Logger.LogInformation("Processed order {OrderId}", orderId);
return result;
}
}
Note: Job input parameters and results are serialized to JSON and displayed in the Hangfire dashboard. Avoid passing sensitive data as job parameters.
Registering recurring jobs
Option 1 — attribute-based auto-registration
Annotate the job class with [RecurringJob]. The plugin discovers and registers it automatically at startup — no boilerplate required:
[RecurringJob("0 0 * * *")] // every day at midnight UTC
public class SendDailyReportJob : AsyncJob { ... }
[RecurringJob("0 * * * *", "critical")] // every hour, on the "critical" queue
public class CriticalHourlyJob : AsyncJob { ... }
The [RecurringJob] attribute accepts a standard cron expression and an optional queue name.
Option 2 — programmatic registration via ConfigureJobs
Register jobs in your plugin or host configuration using the IJobManager API:
public class MyPlugin : IPluginServiceConfiguration
{
public void ConfigureServices(IServiceCollection serviceCollection)
{
serviceCollection.ConfigureJobs((jobs, _) =>
{
// simple recurring job
jobs.RegisterRecurring<SendDailyReportJob>("0 0 * * *");
// parameterized recurring job (passes the value at registration time)
jobs.RegisterRecurring<ProcessOrderJob, int>(42, Cron.Daily());
// recurring job on a specific queue with a name suffix (useful for multiple instances)
jobs.RegisterRecurring<CriticalHourlyJob>("0 * * * *", queue: "critical", jobSuffix: "v2");
});
}
}
Fire-and-forget and delayed jobs via IJobManager
Inject IJobManager to enqueue or schedule jobs programmatically at runtime:
public class OrderController
{
private readonly IJobManager _jobs;
public OrderController(IJobManager jobs) => _jobs = jobs;
public void PlaceOrder(int orderId)
{
// fire-and-forget
_jobs.Enqueue<ProcessOrderJob, int>(orderId);
// fire-and-forget on a specific queue
_jobs.Enqueue<ProcessOrderJob, int>(orderId, queue: "critical");
// simple job (no parameters)
_jobs.Enqueue<SendDailyReportJob>();
// delayed execution
_jobs.Schedule<SendDailyReportJob>(TimeSpan.FromHours(1));
_jobs.Schedule<ProcessOrderJob, int>(orderId, TimeSpan.FromMinutes(5));
}
}
Querying job state via IJobManager
IJobManager exposes monitoring APIs to inspect the current state of the job store:
// all recurring jobs
IList<RecurringJobDto> recurring = jobManager.GetRecurringJobs();
// recurring jobs for a specific type, with optional predicate
IList<RecurringJobDto> myJobs = jobManager.GetRecurringJobs<ProcessOrderJob>();
// enqueued, processing, failed, fetched — all support type-filtered overloads
IList<EnqueuedJobDto> enqueued = jobManager.GetEnqueuedJobs<ProcessOrderJob>();
IList<ProcessingJobDto> processing = jobManager.GetProcessingJobs();
IList<FailedJobDto> failed = jobManager.GetFailedJobs();
// unified view across all states with optional predicate
IList<JobEntity> all = jobManager.GetJobs(e => e.Type == JobType.Failed);
// delete a recurring or background job
jobManager.DeleteRecurring("ProcessOrder");
jobManager.Delete(backgroundJobId);
Dashboard
The Hangfire dashboard is mounted at /hangfire. Access is restricted to the host configured in allowedDashboardHost. If the value is empty or not set, access is denied for all hosts.
Attributes
[RecurringJob(cronPattern, queue)]
Marks a class for automatic recurring job registration at startup. Accepts a cron pattern and an optional queue name.
[RecurringJob("*/5 * * * *", "default")]
public class PollExternalApiJob : AsyncJob { ... }
[JobTimeout(timeout, TimeUnit)]
Cancels the job after the specified duration. The cancellation token passed to ExecuteJobAsync is cancelled automatically.
[JobTimeout(30, TimeUnit.Seconds)]
public class QuickJob : AsyncJob { ... }
Supported TimeUnit values: Milliseconds, Seconds, Minutes, Hours, Days.
[JobTimeZone(TimeZoneInfo)]
Specifies the time zone used for cron schedule evaluation of recurring jobs (default: UTC).
[JobTimeZone(/* TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin") */)]
public class LocalTimeCronJob : AsyncJob { ... }
[UniquePerQueueAttribute(queue)]
Prevents duplicate job executions. If a job with the same type and arguments is already queued, the new one is deleted instead of enqueued.
[UniquePerQueueAttribute("default")]
public class ImportDataJob : AsyncJob { ... }
Optional properties:
CheckScheduledJobs— also check scheduled (delayed) jobs (default:false)CheckRunningJobs— also check currently processing jobs (default:false)
[JobCleanupExpirationTimeAttribute(days)]
Controls how many days job results are retained in the storage backend before deletion.
[JobCleanupExpirationTimeAttribute(14)]
public class ArchiveJob : AsyncJob { ... }
Filters
LogJobExecutionFilter (always active)
Automatically logs a structured message at the start and finish of every job execution using the job's own logger type.
EnabledByFeatureFilter (opt-in)
Gates job execution on a feature flag using the Microsoft Feature Management system. Enable via configuration:
hangfire:
enableJobsByFeatureManagementConfig: true
featureManagement:
SendDailyReportJob: true # job class name is the feature flag name
ProcessOrderJob: false # this job will be skipped
Works with both file-based feature management and the Dosaic Unleash plugin for dynamic runtime feature flags. Since the flag is resolved before each job execution, changes take effect at runtime with a delay based on how frequently the feature management source is refreshed.
Custom storage / server configuration (IHangfireConfigurator)
Implement IHangfireConfigurator (a IPluginConfigurator) to plug in a custom Hangfire storage backend or to configure the background server options:
public class MyHangfireConfigurator : IHangfireConfigurator
{
// Set to true if your Configure() call registers a storage backend,
// so the plugin skips its default PostgreSQL storage setup.
public bool IncludesStorage => true;
public void Configure(IGlobalConfiguration config)
{
config.UseRedisStorage("localhost:6379");
}
public void ConfigureServer(BackgroundJobServerOptions options)
{
options.WorkerCount = 5;
}
}
All IHangfireConfigurator implementations are discovered automatically by the Dosaic plugin system.
Observability
Health check
A Hangfire readiness health check is registered automatically and is accessible via the standard Dosaic health endpoints (/health/readiness). It verifies that at least one Hangfire server is running.
OpenTelemetry tracing
Hangfire jobs are automatically instrumented with OpenTelemetry tracing via OpenTelemetry.Instrumentation.Hangfire.
In addition, every AsyncJob / ParameterizedAsyncJob<T> body runs inside a wrapper span emitted on the shared Dosaic ActivitySource (Tracing.SourceName):
- Span name —
GetType().FullNameof the concrete job - Status — set to
Okon success,Error(with the exception attached) on failure - Log scope —
job.typeis also pushed onto theILoggerscope for the duration of the job
To enrich the span with business identifiers (e.g. tenant id, message id), override EnrichActivity:
public class ProcessOrderJob : ParameterizedAsyncJob<OrderId>
{
protected override void EnrichActivity(Activity activity, OrderId value)
{
activity?.SetTag("order.id", value.ToString());
}
protected override Task<object> ExecuteJobAsync(OrderId value, CancellationToken ct) { ... }
}
AsyncJob has the same hook with the single-argument EnrichActivity(Activity activity) signature. Default implementation is a no-op.
PostgreSQL polling-noise filter
Hangfire's PostgreSQL job-storage polls the hangfire.* schema continuously, which produces a high volume of low-value SQL spans on whatever database instrumentation is enabled (Npgsql, EF Core, etc.). HangFirePlugin registers HangfireSqlNoiseProcessor as the first OpenTelemetry processor in the pipeline; it inspects db.statement / db.query.text / db.statement.text plus the activity display/operation name and clears ActivityTraceFlags.Recorded on any span referencing the hangfire schema, so the OTLP batch exporter drops them before send. No configuration knob — the filter is on whenever the Hangfire plugin is active.
Prometheus metrics
A background service (HangfireStatisticsMetricsReporter) collects Hangfire statistics every 60 seconds and publishes them as OpenTelemetry gauges:
| Metric | Description |
|---|---|
hangfire_job_count_Succeeded |
Number of succeeded jobs |
hangfire_job_count_Failed |
Number of failed jobs |
hangfire_job_count_Scheduled |
Number of scheduled (delayed) jobs |
hangfire_job_count_Processing |
Number of currently processing jobs |
hangfire_job_count_Enqueued |
Number of enqueued jobs |
hangfire_job_count_Deleted |
Number of deleted jobs |
hangfire_job_count_Recurring |
Number of registered recurring jobs |
hangfire_job_count_Servers |
Number of active Hangfire servers |
hangfire_job_count_Queues |
Number of active queues |
hangfire_job_count_RetryJobs |
Number of jobs currently awaiting retry |
Job naming convention
The job ID used by Hangfire is derived from the class name by stripping the Job and Async suffixes. For example:
SendDailyReportJob→SendDailyReportProcessOrderAsyncJob→ProcessOrder
When registering the same job type multiple times with jobSuffix, the suffix is appended: ProcessOrder_v2.
Further reading
| 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
- AspNetCore.HealthChecks.Hangfire (>= 9.0.0)
- Chronos.Abstractions (>= 2.0.24)
- Dosaic.Hosting.Abstractions (>= 1.2.31)
- HangFire (>= 1.8.23)
- Hangfire.MemoryStorage (>= 1.8.1.2)
- Hangfire.PostgreSql (>= 1.21.1)
- Microsoft.Extensions.Caching.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Caching.Memory (>= 10.0.7)
- Microsoft.Extensions.Configuration (>= 10.0.7)
- Microsoft.Extensions.DependencyInjection (>= 10.0.7)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Options (>= 10.0.7)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.7)
- Microsoft.FeatureManagement (>= 4.5.0)
- Newtonsoft.Json (>= 13.0.4)
- OpenTelemetry (>= 1.15.3)
- OpenTelemetry.Extensions.Hosting (>= 1.15.3)
- OpenTelemetry.Instrumentation.Hangfire (>= 1.15.0-beta.1)
- Vogen (>= 8.0.5)
- YamlDotNet (>= 17.0.1)
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.2.31 | 44 | 5/28/2026 |
| 1.2.30 | 68 | 5/7/2026 |
| 1.2.29 | 59 | 5/5/2026 |
| 1.2.28 | 80 | 4/30/2026 |
| 1.2.27 | 66 | 4/29/2026 |
| 1.2.26 | 56 | 4/29/2026 |
| 1.2.25 | 75 | 4/27/2026 |
| 1.2.24 | 69 | 4/21/2026 |
| 1.2.23 | 78 | 4/14/2026 |
| 1.2.22 | 66 | 4/10/2026 |
| 1.2.21 | 62 | 4/10/2026 |
| 1.2.20 | 70 | 4/10/2026 |
| 1.2.19 | 63 | 4/9/2026 |
| 1.2.18 | 82 | 4/2/2026 |
| 1.2.17 | 65 | 4/1/2026 |
| 1.2.16 | 70 | 4/1/2026 |
| 1.2.15 | 71 | 3/31/2026 |
| 1.2.14 | 69 | 3/30/2026 |
| 1.2.13 | 65 | 3/26/2026 |
| 1.2.12 | 82 | 3/24/2026 |