I originally published an extended version of this post on my blog. It's part of my personal C# Advent of Code.
These days I finished another project with one of my clients. I worked to connect a Property Management System with a third-party Point of Sales. I used Hangfire to replace ASP.NET BackgroundServices.
Today I want to share four lessons I learned while working with Hangfire.
1. Hangfire lazy-loads configurations
Hangfire lazy loads configurations. We have to retrieve services from the ASP.NET dependencies container instead of using static alternatives.
I faced this issue after trying to run Hangfire in non-development environments without registering the Hangfire dashboard. This was the exception message I got: "JobStorage.Current property value has not been initialized." When registering the Dashboard, Hangfire loads some of those configurations. That's why "it worked on my machine."
These two issues in Hangfire GitHub repo helped me to find this out: issue #1991 and issue #1967.
This was the fix I found in those two issues:
using Hangfire;
using MyCoolProjectWithHangfire.Jobs;
using Microsoft.Extensions.Options;
namespace MyCoolProjectWithHangfire;
public static class WebApplicationExtensions
{
public static void ConfigureRecurringJobs(this WebApplication app)
{
// Before, using the static version π
//
// RecurringJob.AddOrUpdate<MyCoolJob>(
// MyCoolJob.JobId,
// x => x.DoSomethingAsync());
// RecurringJob.Trigger(MyCoolJob.JobId);
// After π€©
//
var recurringJobManager = app.Services.GetRequiredService<IRecurringJobManager>();
// πππ
recurringJobManager.AddOrUpdate<MyCoolJob>(
MyCoolJob.JobId,
x => x.DoSomethingAsync());
recurringJobManager.Trigger(MyCoolJob.JobId);
}
}
2. Hangfire Dashboard in non-Local environments
By default, Hangfire only shows the Dashboard for local requests. A coworker pointed that out. It's in plain sight in the Hanfire Dashboard documentation. Arrrggg!
To make it work in other non-local environments, we need an authorization filter. Like this,
public class AllowAnyoneAuthorizationFilter : IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
{
// Everyone is more than welcome...πππ
return true;
}
}
And we add it when registering the Dashboard into the dependencies container. Like this,
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new [] { new AllowAnyoneAuthorizationFilter() }
});
3. InMemory-Hangfire SucceededJobs method
For the In-Memory Hangfire implementation, the SucceededJobs()
method from the monitoring API returns jobs from most recent to oldest. There's no need for pagination. Look at the Reverse()
method in the SucceededJobs source code.
I had to find out why an ASP.NET health check was only working for the first time. It turned out that the code was paginating the successful jobs, always looking for the oldest successful jobs. Like this,
public class HangfireSucceededJobsHealthCheck : IHealthCheck
{
private const int CheckLastJobsCount = 10;
private readonly TimeSpan _period;
public HangfireSucceededJobsHealthCheck(TimeSpan period)
{
_period = period;
}
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
var isHealthy = true;
var monitoringApi = JobStorage.Current.GetMonitoringApi();
// Before π
//
// It used pagination to bring the oldest 10 jobs
//
// var succeededCount = (int)monitoringApi.SucceededListCount();
// var succeededJobs = monitoringApi.SucceededJobs(succeededCount - CheckLastJobsCount, CheckLastJobsCount);
// πππ
// After π€©
//
// SucceededJobs returns jobs from newest to oldest
var succeededJobs = monitoringApi.SucceededJobs(0, CheckLastJobsCount);
// π
var successJobsCount = succeededJobs.Count(x => x.Value.SucceededAt.HasValue
&& x.Value.SucceededAt > DateTime.UtcNow - period);
var result = successJobsCount > 0
? HealthCheckResult.Healthy("Yay! We have succeeded jobs.")
: new HealthCheckResult(
context.Registration.FailureStatus, "Nein! We don't have succeeded jobs.");
return Task.FromResult(result);
}
}
4. Prevent Concurrent execution of Hangfire jobs
Hangfire has an attribute to prevent the concurrent execution of the same job: DisableConcurrentExecutionAttribute
. Source. We can change the resource being locked to avoid executing jobs with the same parameters. For example, to run only one job per entity.
[DisableConcurrentExecutionAttribute(timeoutInSeconds: 60)]
// πππ
public class MyCoolJob
{
public async Task DoSomethingAsync()
{
// Beep, beep, boop...π€
}
}
VoilΓ ! That's what I learned from this project. This gave me the idea to stop to reflect on what I learned from every project I work on. I really enjoyed figuring out the issue with the health check. It made me read the source code of the In-memory storage for Hangfire.
Hey, there! I'm Cesar, a software engineer and lifelong learner. Visit my Gumroad page to download my ebooks and check my courses.
Happy coding!