Hangfire: A Feature-Rich Approach to Task Scheduling in .NET
In the vast landscape of .NET scheduling libraries, Hangfire stands tall as a formidable competitor.
We've previously explored the powerful Quartz library in our last article:
Today we're shifting our focus to Hangfire, a highly efficient and feature-rich job scheduling solution.
Hangfire is an open-source framework designed to simplify the process of creating, processing and managing background tasks. It offers a range of features that make it a robust and versatile tool for managing different types of tasks.
In this article, we'll look at Hangfire's key components, discuss its benefits and provide a practical example of its use in a .NET Core application.
Presentation of Hangfire
Hangfire is an open-source framework that helps you create, process, and manage your background jobs. There are 4 main parts to Hangfire:
- Dashboard : application to monitor and manage background jobs (view status and history of jobs, and also managed them).
- Server : application that processes background jobs. Each Server is responsible for processing jobs from a queue.
- Storage : each piece of information about the jobs is stored in a database. Hangfire supports multiple storage options, including SQL Server and Redis
- Jobs : refers to a task that needs to be executed. There are many types of jobs: Fire-and-forget jobs, Delayed jobs, Recurring jobs,
Continuations, Batch jobs (Pro version), Batch Continuations (Pro version)
How to setup Hangfire
Let's put some context.
In my current role at the company, we have numerous virtual machine tasks managed by the Windows Task Scheduler. Recognizing the need for a more efficient task management system with improved monitoring capabilities, I began searching for a suitable solution.
However, I faced a significant constraint: I couldn't utilize the same server for the dashboard and certain jobs. This restriction led to the distinction between "Internal" and "External" jobs, each with its unique requirements and constraints.
To address this challenge, I decided to explore Hangfire as a potential solution, given its robust background job processing and monitoring features.
The entire article project is documented on a GitHub repository :
I advise you to open it alongside your reading to make a direct link between the examples given and the actual case. To use it, you'll need a local installation of Docker.
The project is divided into 3 parts:
- GoatHangfire.Dashboard to supervize jobs
- GoatHangfire.InternalJob to start jobs on local server
- GoatHangfire.ExternalJob to start jobs on external server
You can run the project directly with docker-compose or DotNet Aspire (which would be the subject of a future article)
Create a Hangfire Job
To set up a scheduling system, the first step is to define the tasks that will be triggered. To do this, simply integrate the Hangfire.Core
library (or I'll refer to the example GitHub project)
dotnet add package Hangfire.Core
Let's start by building a recurring task, i.e. a job that will be executed periodically. To define it at Hangfire level, we need to inject the IRecurringJobManager
interface and add it to its internal list.
public class InternalGoatJob
{
private readonly IRecurringJobManager _recurringJobs;
private readonly IGoatService _goatService;
public InternalGoatJob(IRecurringJobManager recurringJobs, IGoatService goatService)
{
_recurringJobs = recurringJobs;
_goatService = goatService;
}
public Task ExecuteRecurringJobAsync(string cronExpression, CancellationToken stoppingToken)
{
_recurringJobs.AddOrUpdate<IGoatService>("internal-goat-recurring-job",
x => x.RecurringExecuteAsync(stoppingToken),
cronExpression);
return Task.CompletedTask;
}
}
The periodicity of a recurring task is defined using a CRON expression, either as a character string (e.g.: 0 * * * * *), or more easily using the Cron class (e.g.: Cron.Minutely
for a trigger every minute).
For more occasional tasks, you can use the IBackgroundJobClient
interface, which stacks requests and executes them in the background.
public class InternalGoatJob
{
private readonly IBackgroundJobClient _backgroundJobs;
public InternalGoatJob(IBackgroundJobClient backgroundJobs...)
{
...
_backgroundJobs = backgroundJobs;
}
public void ExecuteQueueJob()
{
_backgroundJobs.Enqueue(() => _goatService.QueueExecute());
}
}
In order to manage the jobs, we need to create a service that will be used by the Hangfire Dashboard.
Manage jobs with Hangfire Dashboard
What I appreciate about Hangfire is the Hangfire Dashboard. It's a built-in, web-based user interface that allows you to monitor, manage, and debug your background jobs. If you don't set any authorization, the dashboard is accessible only on localhost.
dotnet add package Hangfire
By default, the Hangfire Dashboard is accessible at "/hangfire". You can change this by passing a different route to
app.UseHangfireDashboard("/dashboard", new DashboardOptions {
Authorization = new[] { new AuthorizationAlwaysTrueFilter() }
});
The first argument to UseHangfireDashboard
is the route where the dashboard will be accessible. In this case, it's "/dashboard".
The second argument is an instance of DashboardOptions
. This is where you can specify options for the dashboard. In this case, an AuthorizationAlwaysTrueFilter
is used, which means the dashboard will be accessible without any authorization.
You can also set up SSO authentication for the Hangfire Dashboard by using the AuthorizationSsoFilter
class.
Authorization
You can manage access authorizations to the dashboard by creating a custom implementation of the IDashboardAuthorizationFilter
interface.
public class AuthorizationAlwaysTrueFilter : IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
{
return true;
}
}
Connection to Hangfire
Hangfire can easily be connected to external database to save all jobs information.
dotnet add package Hangfire.SqlServer
The connection string can bedefined inside a appsettings.json
file
"ConnectionStrings": {
"HangfireDbConnection": "Server=127.0.0.1,1633;Database=hangfire;User=sa;Password=Your_password123;Trusted_Connection=false;Encrypt=false"
}
and be use inside the Hangfire declaration configuration as following:
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// AddHangfire: This method adds Hangfire services to the services container. It takes a configuration action where you can configure various aspects of Hangfire.
builder.Services.AddHangfire(configuration => configuration
// SetDataCompatibilityLevel(CompatibilityLevel.Version_180): This sets the compatibility level for serialized data.
// Version 180 is the latest and it's recommended to use the latest version unless you need to support older Hangfire servers.
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
// This adds a filter that automatically retries failed jobs.
// The Attempts property is set to 0, which means jobs won't be retried if they fail.
.UseFilter(new AutomaticRetryAttribute { Attempts = 0, })
// This changes the type serializer to use simple assembly names.
// This can help avoid issues when moving jobs between different environments.
.UseSimpleAssemblyNameTypeSerializer()
// This applies recommended JSON serializer settings.
// It's generally a good idea to use this unless you have specific serialization needs that it doesn't meet.
.UseRecommendedSerializerSettings()
// This adds a log provider that outputs colored logs to the console.
.UseColouredConsoleLogProvider()
// This configures Hangfire to use SQL Server for storage.
// The connection string is retrieved from the application's configuration with the key "HangfireDb
.UseSqlServerStorage(builder.Configuration.GetConnectionString("HangfireDbConnection")));
Before showing the user interface, and in order to achieve the aim of this article, let's take a quick look at how to use an external instance of a Hangfire server.
Start jobs on external Hangfire server
It is also possible to start jobs on a Hangfire server other than the one available with the dashboard. This is useful when you need a separate server to manage scheduling.
To achieve this, the class declaring the jobs must inherit from BackgroundService
, and operate in the same way as internal jobs.
public class ExternalGoatJob : BackgroundService
{
...
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
_recurringJobs.AddOrUpdate("ExternalGoatJob", () => _goatService.ExecuteAsync(stoppingToken), Cron.Minutely);
return Task.CompletedTask;
}
}
All that remains is to declare the service in program.cs
.
builder.Services.AddHostedService<ExternalGoatJob>();
builder.Services.AddHangfire(configuration => configuration);
# You can also configure the settings in the same way as for the dashboard
The external job can be run directly on your server.
Accessing the Dashboard
Once the application is running, you can access the Hangfire Dashboard at https://localhost:32793/dashboard.
The dashboard contains a series of screens showing the list of jobs in progress, the history of jobs that have been triggered and information on those that are currently failing.
The Recurring Jobs tab contains all declared jobs, including the external and internal recurring jobs in the example.
As you can see, for external jobs, we have less information than internal jobs with the message Could not resolve assembly
. This is due to the fact that the job is not referenced in the dashboard project.
Conclusion
In wrapping up our exploration of Hangfire, it's clear that this library offers a comprehensive solution for background processing in .NET Core applications.
The Hangfire Dashboard is a particularly useful tool for monitoring and managing jobs, providing a clear and detailed overview of all your background tasks.
It's really easy to set up and use, and the ability to create different types of jobs makes it a versatile tool for handling a wide range of tasks.
Depending on the number of jobs and the frequency at which they are processed, Hangfire can consume significant system resources, especially if it's running in the same process as your web application.
To go further:
Have a goat day π