DEV Community

Cover image for Four lessons I learned after working with Hangfire
Cesar Aguirre
Cesar Aguirre

Posted on • Updated on • Originally published at canro91.github.io

Four lessons I learned after working with Hangfire

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 internal project while working with one of my clients. I worked to connect a Property Management System with a third-party Point of Sales. I had to work with Hangfire. I used it to replace ASP.NET BackgroundServices. Today I want to share some of the technical things I learned along the way.

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

And we add it when registering the Dashboard into the dependencies container. Like this,

app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
    Authorization = new [] { new AllowAnyoneAuthorizationFilter() }
});
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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...
    }
}
Enter fullscreen mode Exit fullscreen mode

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. To support my work, visit my Gumroad page to download my ebooks, check my courses, or buy me a coffee.

Happy coding!

Latest comments (0)