Four lessons I learned after working with Hangfire
Tags: csharp, dotnet, tutorial, adventofcode
Published: 2022-12-19 18:32:44

_I originally published an extended version of this post on [my blog](https://canro91.github.io/2022/12/13/LessonsOnHangfireAndOrmLite/). It's part of my personal [C# Advent of Code](https://canro91.github.io/AdventOfCode2022)._

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](https://canro91.github.io/2022/12/06/BackgroundServicesAndLiteHangfire/).

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](https://github.com/HangfireIO/Hangfire/issues/1991) and [issue #1967](https://github.com/HangfireIO/Hangfire/issues/1967).

This was the fix I found in those two issues:

```csharp
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](https://docs.hangfire.io/en/latest/configuration/using-dashboard.html). Arrrggg!

To make it work in other non-local environments, we need an authorization filter. Like this,

```csharp
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,

```csharp
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](https://github.com/HangfireIO/Hangfire.InMemory/blob/master/src/Hangfire.InMemory/InMemoryMonitoringApi.cs#L259).

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,

```csharp
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](https://github.com/HangfireIO/Hangfire/blob/master/src/Hangfire.Core/DisableConcurrentExecutionAttribute.cs). We can change the resource being locked to avoid executing jobs with the same parameters. For example, to run only one job per entity.

```csharp
[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](https://imcsarag.gumroad.com) to download my ebooks and check my courses._

_Happy coding!_