DEV Community

Cover image for How to replace BackgroundServices with a lite Hangfire
Cesar Aguirre
Cesar Aguirre

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

How to replace BackgroundServices with a lite Hangfire

I originally published an extended version of this post on my blog. It's part of my personal C# Advent of Code.

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:

1. Register Hangfire

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

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

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

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

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
  • Attempts is the number of retry attempts. By default, Hangfire retries jobs 10 times. Source

Car factory

Audi, San José Chiapa, Puebla, Mexico. Photo by carlos aranda on Unsplash

2. Write "Producer" and "Consumer" jobs

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

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

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,

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

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.

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

Happy coding!

Top comments (0)