NeverendingShift.Hangfire
0.1.0
dotnet add package NeverendingShift.Hangfire --version 0.1.0
NuGet\Install-Package NeverendingShift.Hangfire -Version 0.1.0
<PackageReference Include="NeverendingShift.Hangfire" Version="0.1.0" />
<PackageVersion Include="NeverendingShift.Hangfire" Version="0.1.0" />
<PackageReference Include="NeverendingShift.Hangfire" />
paket add NeverendingShift.Hangfire --version 0.1.0
#r "nuget: NeverendingShift.Hangfire, 0.1.0"
#:package NeverendingShift.Hangfire@0.1.0
#addin nuget:?package=NeverendingShift.Hangfire&version=0.1.0
#tool nuget:?package=NeverendingShift.Hangfire&version=0.1.0
NeverendingShift.Hangfire
Advanced Hangfire extensions providing thread-safe access to PerformingContext throughout your background jobs.
Works with both .NET Framework 4.8 and .NET 5+.
Features
- ✅ Thread-safe
PerformingContextaccess usingAsyncLocal<T> - ✅ Works with .NET Framework 4.6.2 and .NET 5+
- ✅ Simple registration with or without DI
- ✅ Extension methods for common operations
- ✅ Handy attributes so you don't have to figure them out yourself
Installation
dotnet add package NeverendingShift.Hangfire
Or via Package Manager:
Install-Package NeverendingShift.Hangfire
Quick Start
.NET 5+ / .NET Core (with Dependency Injection)
using NeverendingShift.Hangfire;
// Program.cs or Startup.cs
services.AddHangfirePerformingContextAccessor();
services.AddHangfire((sp, config) =>
{
config
.UseSqlServerStorage("YourConnectionString")
.UsePerformingContextAccessor(sp);
});
services.AddHangfireServer();
.NET Framework 4.8 (without DI)
using NeverendingShift.Hangfire;
// Global.asax.cs or App_Start
var accessor = new PerformingContextAccessor();
GlobalConfiguration.Configuration
.UseSqlServerStorage("YourConnectionString")
.UsePerformingContextAccessor(accessor);
// Make accessor available to your jobs (e.g., via a static property or DI container)
JobHelper.PerformingContextAccessor = accessor;
.NET Framework 4.8 with DI Container (e.g., Autofac)
using NeverendingShift.Hangfire;
// Register in your container
builder.RegisterType<PerformingContextAccessor>()
.As<IPerformingContextAccessor>()
.SingleInstance();
builder.RegisterType<PerformingContextAccessorFilter>()
.SingleInstance();
// Configure Hangfire
var accessor = container.Resolve<IPerformingContextAccessor>();
GlobalConfiguration.Configuration
.UseSqlServerStorage("YourConnectionString")
.UsePerformingContextAccessor(accessor);
Usage Examples
Basic Job with Progress Reporting
public class DataProcessingJob
{
private readonly IPerformingContextAccessor _contextAccessor;
public DataProcessingJob(IPerformingContextAccessor contextAccessor)
{
_contextAccessor = contextAccessor;
}
public async Task ProcessAsync(string dataId)
{
// Report progress
_contextAccessor.SetProgress(0, "Starting...");
// Get job information
var jobId = _contextAccessor.GetCurrentJobId();
_contextAccessor.WriteLine($"Processing data {dataId} in job {jobId}");
await Step1();
_contextAccessor.SetProgress(33, "Step 1 complete");
await Step2();
_contextAccessor.SetProgress(66, "Step 2 complete");
await Step3();
_contextAccessor.SetProgress(100, "Complete!");
_contextAccessor.WriteLine("Processing complete", ConsoleTextColor.Green);
}
}
Progress Tracking with Loop
public async Task ProcessRecordsAsync(List<Record> records)
{
var total = records.Count;
for (int i = 0; i < total; i++)
{
await ProcessRecord(records[i]);
var progress = (int)((i + 1) / (double)total * 100);
_contextAccessor.SetProgress(progress, $"Processed {i + 1}/{total}");
}
}
Storing Custom Metadata
public async Task ProcessOrderAsync(int orderId)
{
var startTime = DateTime.UtcNow;
// Store custom parameters that persist with the job
_contextAccessor.SetJobParameter("OrderId", orderId);
_contextAccessor.SetJobParameter("StartTime", startTime);
await ProcessOrder(orderId);
var duration = DateTime.UtcNow - startTime;
_contextAccessor.SetJobParameter("Duration", duration.TotalSeconds);
_contextAccessor.SetJobParameter("Status", "Completed");
}
Conditional logic (Works Inside and Outside Jobs)
public class SharedService
{
private readonly IPerformingContextAccessor _contextAccessor;
public async Task DoWorkAsync()
{
if (_contextAccessor.IsInJobContext())
{
// Running in Hangfire - use Hangfire console
_contextAccessor.Current.WriteLine("Processing work item");
_contextAccessor.SetProgress(50);
}
else
{
// Running outside Hangfire - use regular logging
Console.WriteLine("Processing work item");
}
await ProcessWork();
}
}
Using Context Items for Scoped Data
public async Task ProcessWorkflowAsync()
{
// Store data in context items (scoped to this job execution)
_contextAccessor.SetItem("WorkflowId", Guid.NewGuid());
_contextAccessor.SetItem("StartTime", DateTime.UtcNow);
await Step1(); // Can access items from nested methods
await Step2();
await Step3();
// Retrieve stored data
var workflowId = _contextAccessor.GetItem<Guid>("WorkflowId");
var startTime = _contextAccessor.GetItem<DateTime>("StartTime");
var duration = DateTime.UtcNow - startTime;
_contextAccessor.WriteLine($"Workflow {workflowId} completed in {duration.TotalSeconds:F2}s");
}
private async Task Step1()
{
var workflowId = _contextAccessor.GetItem<Guid>("WorkflowId");
_contextAccessor.WriteLine($"Step 1 for workflow {workflowId}");
await Task.Delay(100);
}
API Reference
IPerformingContextAccessor
public interface IPerformingContextAccessor
{
PerformingContext Current { get; set; }
}
Extension Methods
// Get current job ID
string GetCurrentJobId(this IPerformingContextAccessor accessor)
// Report progress (0-100)
void SetProgress(this IPerformingContextAccessor accessor, int value, string message = null)
// Store custom job parameter
void SetJobParameter(this IPerformingContextAccessor accessor, string name, object value)
// Retrieve job parameter
T GetJobParameter<T>(this IPerformingContextAccessor accessor, string name)
// Check if in job context
bool IsInJobContext(this IPerformingContextAccessor accessor)
// Get job creation time
DateTime? GetJobCreatedAt(this IPerformingContextAccessor accessor)
// Context items (scoped storage)
void SetItem(this IPerformingContextAccessor accessor, object key, object value)
T GetItem<T>(this IPerformingContextAccessor accessor, object key)
IDictionary<object, object> GetItems(this IPerformingContextAccessor accessor)
Framework Compatibility
| Framework | Supported | Notes |
|---|---|---|
| .NET Framework 4.8 | ✅ | Full support, use manual registration |
| .NET Framework 4.7.2 | ✅ | Via netstandard2.0 |
| .NET Framework 4.6.1 | ✅ | Via netstandard2.0 |
| .NET Standard 2.0 | ✅ | Full support |
| .NET 5 | ✅ | Full support with DI extensions |
| .NET 6+ | ✅ | Full support with DI extensions |
Best Practices
1. Always Inject as Dependency
// ✅ Good - use dependency injection
public MyJob(IPerformingContextAccessor contextAccessor) { }
// ❌ Bad - don't create instances directly
var accessor = new PerformingContextAccessor();
2. Check Context Before Use
// ✅ Good - check if in job context
if (_contextAccessor.IsInJobContext())
{
_contextAccessor.WriteLine("Running in Hangfire");
}
// ❌ Bad - assumes context exists
_contextAccessor.Current.WriteLine("This may throw!");
3. Use Extension Methods
// ✅ Good - use extension methods
_contextAccessor.SetProgress(50, "Half done");
var jobId = _contextAccessor.GetCurrentJobId();
// ❌ Less ideal - direct property access
_contextAccessor.Current?.SetJobParameter("Progress", 50);
Thread Safety
The implementation uses AsyncLocal<T> to maintain context across async/await boundaries:
// ✅ Thread-safe PerformingContext access
BackgroundJob.Enqueue(() => SomeMethodAsync());
BackgroundJob.Enqueue(() => SomeMethodAsync());
Performance
- Minimal Overhead: Uses
AsyncLocal<T>with negligible performance impact - No Locking: Thread-safe without locks or synchronization
- Singleton: Registered as singleton to minimize allocations
Troubleshooting
Context is Always Null
Problem: Current property returns null in jobs.
Solutions:
- Ensure you registered the accessor (
.AddHangfirePerformingContextAccessor()or manual registration) - Verify
.UsePerformingContextAccessor()is called in Hangfire configuration - Check that jobs are executed by Hangfire server (not called directly)
Progress Not Updating
Problem: Progress values set but not visible in dashboard.
Solutions:
- Use a storage provider that supports job parameters (SQL Server, PostgreSQL, etc.)
- Ensure values are between 0-100
- Check your Hangfire dashboard version
.NET Framework Compilation Errors
Problem: Extension methods not available in .NET Framework project.
Solution: Ensure your project targets .NET Framework 4.6.1 or higher, which is compatible with netstandard2.0.
Example Projects
Check the samples directory for complete working examples:
- NetCore.Sample - .NET 6+ with DI
- NetFramework48.Sample - .NET Framework 4.8 without DI
- NetFramework48.Autofac.Sample - .NET Framework 4.8 with Autofac
License
This project is licensed under the MIT License - see the LICENSE file for details.
Author
Daniel Barwikowski
GitHub: @dbarwikowski
Support
Made with ❤️ for the Hangfire community
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. 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 was computed. 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 was computed. 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. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 is compatible. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETFramework 4.6.2
- Hangfire.Core (>= 1.7.0 && < 2.0.0)
-
.NETStandard 2.0
- Hangfire.Core (>= 1.7.0 && < 2.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 2.1.0)
-
net8.0
- Hangfire.Core (>= 1.7.0 && < 2.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 2.1.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 |
|---|---|---|
| 0.1.0 | 135 | 3/15/2026 |