How to replace BackgroundServices with a lite Hangfire
Tags: csharp, dotnet, tutorial, adventofcode
Published: 2022-12-14 13:00:00

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

I like ASP.NET Core BackgroundServices. I've used them in one of my client's projects to run recurring operations outside the main ASP.NET Core API site. Even for small one-time operations, I've run them in the same API site.

There's one catch. We have to write our own retrying, multi-threading, and reporting mechanism. BackgroundServices are a lightweight alternative to run background tasks.

These days, a coworker came up with the idea to use a "lite" Hangfire to replace ASP.NET Core BackgroundServices. By "lite," he meant an in-memory, single-thread Hangfire configuration, just like ASP.NET Core BackgroundServices.

Let's create an ASP.NET Core API site and install these NuGet packages:

* Hangfire,
* Hangfire.AspNetCore,
* [Hangfire.MemoryStorage](https://github.com/HangfireIO/Hangfire.InMemory),
* [Hangfire.Console](https://github.com/pieceofsummer/Hangfire.Console) to bring color to our lives

## 1. Register Hangfire

In the Program.cs file, let's register the Hangfire server, dashboard, and recurring jobs. Like this,

```csharp
using Hangfire;
using LiteHangfire.Extensions;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.ConfigureHangfire();
//               👆👆👆

var app = builder.Build();

app.UseAuthorization();
app.MapControllers();

app.UseHangfireDashboard();
app.MapHangfireDashboard();
//  👆👆👆
app.ConfigureRecurringJobs();
//  👆👆👆

app.Run();
```

To make things cleaner, we can use extension methods to keep all Hangfire configurations in a single place. Like this,

```csharp
using Hangfire;
using Hangfire.Console;
using Hangfire.MemoryStorage;
using RecreatingFilterScenario.Jobs;

namespace LiteHangfire.Extensions;

public static class ServiceCollectionExtensions
{
    public static void ConfigureHangfire(this IServiceCollection services)
    {
        services.AddHangfire(configuration =>
        {
            configuration.UseMemoryStorage();
            //            👆👆👆
            // Since we have good memory
            configuration.UseConsole();
            //            👆👆👆

        });
        services.AddHangfireServer(options =>
        {
            options.WorkerCount = 1;
            //      👆👆👆
            // Number of worker threads.
            // By default: min(processor count * 5, 20)
        });

        GlobalJobFilters.Filters.Add(new AutomaticRetryAttribute
        {
            Attempts = 1
            // 👆👆👆
            // Retry count.
            // By default: 10
        });
    }

    public static void ConfigureRecurringJobs(this WebApplication app)
    {
        //var config = app.Services.GetRequiredService<IOptions<MyRecurringJobOptions>>().Value;
        // 👆👆👆
        // To read the cron expression from a config file

        RecurringJob.AddOrUpdate<ProducerRecurringJob>(
            ProducerRecurringJob.JobId,
            x => x.DoSomethingAsync(),
            "0/1 * * * *");
            // 👆👆👆
            // Every minute. Change it to suit your own needs

        RecurringJob.Trigger(ProducerRecurringJob.JobId);
    }
}
```

Notice that we used the `UseMemoryStorage()` method to store jobs in memory instead of in a database and the `UseConsole()` to bring color to our logging messages in the Dashboard.

Then, when we registered the Hangfire server, we used these parameters:

* `WorkerCount` is the number of processing threads. By default, it's the minimum between five times the processor count and 20. [Source](https://github.com/HangfireIO/Hangfire/blob/5b696d4174e13c3dd9489cc6a863d3417c632e31/src/Hangfire.Core/BackgroundJobServer.cs#L171)
* `Attempts` is the number of retry attempts. By default, Hangfire retries jobs 10 times. [Source](https://github.com/HangfireIO/Hangfire/blob/5b696d4174e13c3dd9489cc6a863d3417c632e31/src/Hangfire.Core/AutomaticRetryAttribute.cs#L83)

<figure>
<img src="https://source.unsplash.com/SVUqHTVyn6w/600x400" alt="Car factory" />

<figcaption>Audi, San José Chiapa, Puebla, Mexico. Photo by <a href="https://unsplash.com/@carlosaranda?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">carlos aranda</a> on <a href="https://unsplash.com/s/photos/car-factory?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></figcaption>
</figure>

## 2. Write "Producer" and "Consumer" jobs

The next step was to register a recurring job as a "producer." It looks like this,

```csharp
using Hangfire;
using Hangfire.Console;
using Hangfire.Server;

namespace LiteHangfire.Jobs;

public class ProducerRecurringJob
{
    public const string JobId = nameof(ProducerRecurringJob);

    private readonly IBackgroundJobClient _backgroundJobClient;
    private readonly ILogger<ProducerRecurringJob> _logger;

    public ProducerRecurringJob(IBackgroundJobClient backgroundJobClient,
                                ILogger<ProducerRecurringJob> logger)
    {
        _backgroundJobClient = backgroundJobClient;
        _logger = logger;
    }

    public async Task DoSomethingAsync()
    {
        _logger.LogInformation("Running recurring job at {now}", DateTime.UtcNow);

        // Beep, beep, boop...🤖
        await Task.Delay(1_000);

        // We could read pending jobs from a database, for example
        foreach (var item in Enumerable.Range(0, 5))
        {
            _backgroundJobClient.Enqueue<WorkerJob>(x => x.DoSomeWorkAsync(null));
            // 👆👆👆
        }
    }
}
```

Inside this recurring job, we can read pending jobs from a database and enqueue a new worker job for every pending job available.

And a sample worker job that uses Hangfire.Console looks like this,

```csharp
public class WorkerJob
{
    public async Task DoSomeWorkAsync(PerformContext context)
    {
        context.SetTextColor(ConsoleTextColor.Blue);
        context.WriteLine("Doing some work at {0}", DateTime.UtcNow);

        // Beep, beep, boop...🤖
        await Task.Delay(3_000);
    }
}
```

Notice that we expect a `PerformContext` as a parameter to change the color of the logging message. When we enqueued the worker jobs, we passed `null` as context, then Hangfire uses the right instance when running our jobs. [Source](https://stackoverflow.com/questions/38368153/how-do-i-get-the-current-attempt-number-on-a-background-job-in-hangfire/38387512).

Voilà! That's how to use a lite Hangfire to replace BackgroundServices without adding too much overhead or a new database to store jobs. With the advantage that Hangfire has recurring jobs, retries, and a Dashboard out of the box.

Have you used ASP.NET Core BackgroundServices? Or Hangfire? What do you use to run background jobs? Feel free to leave a comment.

***

_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!_