OptiPowerTools.Hangfire
1.0.0
dotnet add package OptiPowerTools.Hangfire --version 1.0.0
NuGet\Install-Package OptiPowerTools.Hangfire -Version 1.0.0
<PackageReference Include="OptiPowerTools.Hangfire" Version="1.0.0" />
<PackageVersion Include="OptiPowerTools.Hangfire" Version="1.0.0" />
<PackageReference Include="OptiPowerTools.Hangfire" />
paket add OptiPowerTools.Hangfire --version 1.0.0
#r "nuget: OptiPowerTools.Hangfire, 1.0.0"
#:package OptiPowerTools.Hangfire@1.0.0
#addin nuget:?package=OptiPowerTools.Hangfire&version=1.0.0
#tool nuget:?package=OptiPowerTools.Hangfire&version=1.0.0
OptiPowerTools.Hangfire
A one-liner bootstrap for adding Hangfire background job processing to Optimizely CMS 12.
This package was inspired by community feedback on the blog post Adding Hangfire to Optimizely CMS 12, which walked through the manual steps of integrating Hangfire with Optimizely. The recurring request for a ready-made, drop-in solution led to this library — turning what was a multi-step manual setup into a simple, drop-in integration.
Features
- Single extension method to register Hangfire with SQL Server storage and background server
- Hangfire Dashboard with Optimizely role-based authorization out of the box
- CMS menu integration — dashboard appears in the Optimizely navigation bar with configurable placement
- Hangfire.Console support for rich job output
- Configurable via options pattern or appsettings.json
- Custom dashboard authorization — bring your own
IDashboardAuthorizationFilteror disable auth entirely for development - Toggle individual features on/off (
EnableDashboard,EnableConsole,EnableCmsMenu) - Built-in job filters for concurrency control (
MutualExclusion,WaitForOtherJobs) and lifecycle management (ExpireOnSuccess,RetainOnSuccess) - Targets net6.0, net8.0, net9.0, net10.0
Quick Start
// In Program.cs or Startup.cs
services.AddOptiPowerToolHangfire(options =>
{
options.ConnectionString = Configuration.GetConnectionString("HangfireConnection");
});
// In the middleware pipeline (after UseAuthentication/UseAuthorization)
app.UseOptiPowerToolHangfire();
Connection string can point to the same database as Optimizely or to a separate one.
That's it. This registers Hangfire with SQL Server storage, starts the background server, enables the dashboard with role-based auth, and adds a menu item to the CMS navigation.
Configuration
All options except ConnectionString have sensible defaults. Configure via code, appsettings.json, or both (code overrides config).
Code configuration
Minimal configuration
// Connection string is read from appsettings.json ("OptiPowerTools:Hangfire:ConnectionString")
services.AddOptiPowerToolHangfire();
app.UseOptiPowerToolHangfire();
Full configuration
services.AddOptiPowerToolHangfire(options =>
{
// Required
options.ConnectionString = "Server=.;Database=MyDb;Trusted_Connection=True;";
// Optional — all values below are the defaults
options.DashboardPath = "/episerver/backoffice/Plugins/hangfire";
options.DashboardTitle = "OptiPowerTools Hangfire Dashboard";
options.AuthorizedRoles = ["Administrators", "CmsAdmins", "WebAdmins"];
options.SchemaName = "hangfire";
options.EnableDashboard = true;
options.EnableConsole = true;
options.EnableCmsMenu = true;
options.EnableStandardAuthorization = true;
// Menu placement — default is CmsSection (under the CMS nav section)
options.MenuPlacement = CmsMenuPlacement.CmsSection;
options.MenuPath = null; // Override the auto-derived menu path
options.MenuSortIndex = null; // Override the auto-derived sort index
options.CustomSectionName = "OptiPowerTools"; // Section name for TopLevel/CustomSection placement
options.CustomMenuItemName = "OptiPowerTools"; // Display name for the menu item
options.CmsShellPath = "/HangfireCms/Index"; // URL path for the CMS shell iframe page
// Storage maintenance
options.JobExpirationCheckInterval = TimeSpan.FromMinutes(15);
});
appsettings.json
{
"OptiPowerTools": {
"Hangfire": {
"ConnectionString": "Server=.;Database=MyDb;Trusted_Connection=True;",
"DashboardPath": "/episerver/backoffice/Plugins/hangfire",
"DashboardTitle": "OptiPowerTools Hangfire Dashboard",
"AuthorizedRoles": ["Administrators", "CmsAdmins", "WebAdmins"],
"SchemaName": "hangfire",
"EnableDashboard": true,
"EnableConsole": true,
"EnableCmsMenu": true,
"EnableStandardAuthorization": true,
"MenuPlacement": "CmsSection",
"MenuPath": null,
"MenuSortIndex": null,
"CustomSectionName": "OptiPowerTools",
"CustomMenuItemName": "OptiPowerTools",
"CmsShellPath": "/HangfireCms/Index",
"JobExpirationCheckInterval": "00:15:00"
}
}
}
Options reference
| Option | Type | Default | Description |
|---|---|---|---|
ConnectionString |
string |
"" |
Required. SQL Server connection string for Hangfire storage. |
DashboardPath |
string |
"/episerver/backoffice/Plugins/hangfire" |
URL path where the Hangfire dashboard is served. |
DashboardTitle |
string |
"OptiPowerTools Hangfire Dashboard" |
Title shown in the dashboard header. |
AuthorizedRoles |
string[] |
["Administrators", "CmsAdmins", "WebAdmins"] |
Optimizely roles allowed to access the dashboard. |
SchemaName |
string |
"hangfire" |
SQL Server schema for Hangfire tables. |
EnableDashboard |
bool |
true |
Serve the Hangfire dashboard UI. Set to false for worker-only nodes. |
EnableConsole |
bool |
true |
Enable Hangfire.Console for rich console output in jobs. |
EnableCmsMenu |
bool |
true |
Add a Hangfire menu item to the Optimizely CMS navigation. |
EnableStandardAuthorization |
bool |
true |
Use the built-in Optimizely role-based authorization filter for the dashboard. When false and no custom filter is provided, the dashboard allows unrestricted access. |
MenuPlacement |
CmsMenuPlacement |
CmsSection |
Where the menu item appears: CmsSection, TopLevel, or CustomSection. See Menu Placement. |
MenuPath |
string? |
null |
Overrides the auto-derived menu path. Takes precedence over MenuPlacement path logic. |
MenuSortIndex |
int? |
null |
Overrides the auto-derived sort index for the menu item (or section in CustomSection mode). |
CustomSectionName |
string |
"OptiPowerTools" |
Display name for the section group when MenuPlacement is TopLevel or CustomSection. |
CustomMenuItemName |
string |
"OptiPowerTools" |
Display name for the Hangfire menu item in the CMS navigation. Falls back to DashboardTitle when empty. |
CmsShellPath |
string |
"/HangfireCms/Index" |
URL path where the CMS shell page (iframe wrapper) is served. The CMS menu item links to this path. |
JobExpirationCheckInterval |
TimeSpan |
00:15:00 |
How often the expiration manager checks for and removes expired jobs. |
Dashboard Authorization
By default, the Hangfire dashboard is protected by the built-in Optimizely role-based authorization filter, which restricts access to users in the AuthorizedRoles (Administrators, CmsAdmins, WebAdmins). You can customize this behavior in three ways:
Standard authorization (default)
No changes needed. The dashboard uses OptimizelyDashboardAuthorizationFilter which checks the user's CMS roles.
Custom authorization filter
Provide your own IDashboardAuthorizationFilter implementation using the generic overload. The custom filter takes full precedence over the standard filter, regardless of the EnableStandardAuthorization setting.
services.AddOptiPowerToolHangfire<MyCustomAuthFilter>(options =>
{
options.ConnectionString = Configuration.GetConnectionString("HangfireConnection");
});
public class MyCustomAuthFilter : IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
{
var httpContext = context.GetHttpContext();
return httpContext.User.Identity?.IsAuthenticated == true;
}
}
Free access (no authorization)
Disable the standard authorization filter without providing a custom one. This allows unrestricted access to the dashboard — useful for development environments.
services.AddOptiPowerToolHangfire(options =>
{
options.ConnectionString = Configuration.GetConnectionString("HangfireConnection");
options.EnableStandardAuthorization = false;
});
Warning: Do not disable authorization in production. The Hangfire dashboard exposes job data, retry controls, and server information.
Menu Placement
By default, the Hangfire menu item appears under the CMS section in the Optimizely navigation bar. You can change this with MenuPlacement:
CmsSection (default)
Nests the menu item under the existing CMS section. This is the current behavior and requires no configuration changes.
TopLevel
Places the menu item directly in the global navigation bar as a top-level entry, alongside CMS, Commerce, etc.
{
"OptiPowerTools": {
"Hangfire": {
"MenuPlacement": "TopLevel"
}
}
}
CustomSection
Creates a new collapsible section group and nests the Hangfire item underneath it. The section name is controlled by CustomSectionName.
{
"OptiPowerTools": {
"Hangfire": {
"MenuPlacement": "CustomSection",
"CustomSectionName": "Background Jobs"
}
}
}
Overriding path and sort index
For any placement mode, you can override the menu path and sort index:
{
"OptiPowerTools": {
"Hangfire": {
"MenuPlacement": "CmsSection",
"MenuPath": "/global/admin/hangfire",
"MenuSortIndex": 500
}
}
}
Job Filters
The package includes CMS-agnostic Hangfire job filters for concurrency control, available via the OptiPowerTools.Hangfire.Tools.Filters namespace.
MutualExclusion
Prevents concurrent execution of jobs sharing the same resource group. Uses Hangfire distributed locks for reliable, race-condition-free mutual exclusion.
using OptiPowerTools.Hangfire.Tools.Filters;
[MutualExclusion("data-pipeline")]
public class DataImportJob
{
public void Execute() { /* ... */ }
}
[MutualExclusion("data-pipeline")]
public class DataExportJob
{
public void Execute() { /* ... */ }
}
When DataImportJob is running, DataExportJob is automatically rescheduled (and vice versa). The worker thread is freed immediately — no blocking. The delay before retry is configurable:
[MutualExclusion("data-pipeline", RetryDelaySeconds = 30)]
WaitForOtherJobs
Prevents a job from executing while specific other job types are processing. This is one-directional — only the decorated job needs the attribute.
using OptiPowerTools.Hangfire.Tools.Filters;
[WaitForOtherJobs(typeof(DataImportJob))]
public class ReportGeneratorJob
{
public void Execute() { /* ... */ }
}
ReportGeneratorJob will be rescheduled if DataImportJob is currently processing. DataImportJob does not need any attribute and runs normally.
// Wait for multiple job types, with custom retry delay
[WaitForOtherJobs(typeof(DataImportJob), typeof(DataExportJob), RetryDelaySeconds = 30)]
Note:
WaitForOtherJobsuses the Hangfire monitoring API and has a small race-condition window. For guaranteed mutual exclusion, useMutualExclusioninstead.
ExpireOnSuccess
Reduces the retention period for succeeded jobs. By default, Hangfire keeps succeeded jobs for 24 hours. This filter overrides the expiration timeout so short-lived fire-and-forget jobs are cleaned up faster.
using OptiPowerTools.Hangfire.Tools.Filters;
// Job data expires 60 seconds after success (instead of 24 hours)
[ExpireOnSuccess(60)]
public class NotificationJob
{
public void Execute() { /* ... */ }
}
// Job data expires immediately after success
[ExpireOnSuccess]
public class HealthCheckJob
{
public void Execute() { /* ... */ }
}
The expirationSeconds parameter defaults to 0 (minimal retention). Only succeeded jobs are affected — failed or deleted jobs keep their default Hangfire expiration.
RetainOnSuccess
Extends the retention period for succeeded jobs beyond Hangfire's default of 24 hours. Use this for infrequent jobs (weekly reports, monthly audits) where you want execution details to remain visible in the dashboard until the next run.
using OptiPowerTools.Hangfire.Tools.Filters;
// Keep succeeded job data for 180 days
[RetainOnSuccess(180)]
public class MonthlyAuditJob
{
public void Execute() { /* ... */ }
}
// Keep succeeded job data for 90 days (default)
[RetainOnSuccess]
public class WeeklyReportJob
{
public void Execute() { /* ... */ }
}
The retentionDays parameter defaults to 90. Only succeeded jobs are affected — failed or deleted jobs keep their default Hangfire expiration. For the inverse (reducing retention), see ExpireOnSuccess.
Combining with DisableConcurrentExecution
Neither filter prevents the same job type from running concurrently with itself. Combine with Hangfire's built-in attribute if needed:
[DisableConcurrentExecution(timeoutInSeconds: 60)]
[MutualExclusion("data-pipeline")]
public class DataImportJob { /* ... */ }
Removing this package
This package is a thin configuration wrapper — it does not modify Hangfire internals or change the way Hangfire stores data. If your project outgrows it and you need full control, simply remove the package and configure Hangfire manually. Your existing database, jobs, and history will continue to work without any migration or data changes.
Development
The solution includes a .Web project that references the Optimizely Foundation site via a git submodule for manual testing.
Prerequisites
- .NET 6.0, 8.0, 9.0, or 10.0 SDK
- Docker (for SQL Server)
- Git with submodule support
Getting started
Clone the repository with submodules:
git clone --recursive https://github.com/szolkowski/OptiPowerTools.Hangfire.gitIf you already cloned without
--recursive, initialize the submodule:git submodule update --init --recursiveIf you don't have Foundation DB configured, follow its README and add connection strings to
src/OptiPowerTools.Hangfire.Web/appsettings.jsonorsrc/OptiPowerTools.Hangfire.Web/appsettings.Development.json.Build and run:
dotnet build dotnet run --project src/OptiPowerTools.Hangfire.Web
The site starts at https://localhost:5001 or http://localhost:5000. Once running:
| URL | Description |
|---|---|
/ |
Foundation home page |
/util/login |
CMS admin login |
/episerver/cms |
CMS editorial UI |
/HangfireCms/Index |
Hangfire dashboard (embedded in CMS shell) |
/episerver/backoffice/Plugins/hangfire |
Hangfire dashboard (standalone) |
Running tests
dotnet test
Tests run against net6.0, net8.0, net9.0, and net10.0.
Project structure
| Project | Purpose |
|---|---|
src/OptiPowerTools.Hangfire |
The NuGet library package (net6.0, net8.0, net9.0, net10.0) |
src/OptiPowerTools.Hangfire.Tools |
CMS-agnostic job filters and utilities (bundled into the main NuGet package) |
src/OptiPowerTools.Hangfire.Web |
Dev site for manual testing (net8.0, references Foundation submodule) |
tests/OptiPowerTools.Hangfire.Tests |
Unit tests for main library — xUnit + NSubstitute |
tests/OptiPowerTools.Hangfire.Tools.Tests |
Unit tests for Tools library — xUnit + NSubstitute |
sub/foundation |
Git submodule — episerver/Foundation |
Troubleshooting
BinaryFormatter serialization ... have been removed— The project must targetnet8.0. Foundation's Commerce modules requireBinaryFormatter.
License
MIT. See LICENSE for details.
Contributing
Contributions are welcome! See CONTRIBUTING.md for guidelines.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. 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
- EPiServer.CMS.AspNetCore (>= 12.0.3 && < 13.0.0)
- EPiServer.CMS.UI.Core (>= 12.0.3 && < 13.0.0)
- Hangfire.AspNetCore (>= 1.8.23)
- Hangfire.Console (>= 1.4.3)
- Hangfire.Core (>= 1.8.23)
- Hangfire.SqlServer (>= 1.8.23)
-
net6.0
- EPiServer.CMS.AspNetCore (>= 12.0.3 && < 13.0.0)
- EPiServer.CMS.UI.Core (>= 12.0.3 && < 13.0.0)
- Hangfire.AspNetCore (>= 1.8.23)
- Hangfire.Console (>= 1.4.3)
- Hangfire.Core (>= 1.8.23)
- Hangfire.SqlServer (>= 1.8.23)
-
net8.0
- EPiServer.CMS.AspNetCore (>= 12.0.3 && < 13.0.0)
- EPiServer.CMS.UI.Core (>= 12.0.3 && < 13.0.0)
- Hangfire.AspNetCore (>= 1.8.23)
- Hangfire.Console (>= 1.4.3)
- Hangfire.Core (>= 1.8.23)
- Hangfire.SqlServer (>= 1.8.23)
-
net9.0
- EPiServer.CMS.AspNetCore (>= 12.0.3 && < 13.0.0)
- EPiServer.CMS.UI.Core (>= 12.0.3 && < 13.0.0)
- Hangfire.AspNetCore (>= 1.8.23)
- Hangfire.Console (>= 1.4.3)
- Hangfire.Core (>= 1.8.23)
- Hangfire.SqlServer (>= 1.8.23)
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 | 80 | 3/27/2026 |
| 1.0.0-beta | 71 | 3/27/2026 |