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:
- Hangfire,
- Hangfire.AspNetCore,
- Hangfire.MemoryStorage,
- 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,
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,
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 -
Attempts
is the number of retry attempts. By default, Hangfire retries jobs 10 times. Source
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));
// ๐๐๐
}
}
}
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);
}
}
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.
Starting out or already on the software engineering journey? Join my free 7-day email course to refactor your coding career and save years and thousands of dollars' worth of career mistakes.
Happy coding!