close up picture of cogs

Hangfire background tasks for .NET – Part 1: The fundamentals

Whether a task takes to long to be processed directly by the webserver, we need to perform something sometime but not necessarily right now or our app needs the ability to scale out: Enabling tasks to be processed in the background is a powerful tool, but rolling your own solution is complicated. Hangfire is a battle tested solution for background tasks which will make your life easier. In this series we’ll take a deep look into how it works and how we can use it.

Hangfire Architecture

The central part of Hangfire is the Storage. There are multiple implementations that can be used, some are officially supported, some are contributions by the community. You can use in-memory, SQL Server, Redis, MongoDB and more services.

The client creates Jobs and saves them into storage. It can be a web application, console app, desktop app or mobile app.

The server(s) read the Jobs from storage and execute them. By default this is done by polling the storage repeatedly but push-based solutions can also be used – even though they are not recommended and often times also not needed – like MSMQ when using SQL Server for example.

Hangfire Dashboard queries your datastore to provide you with information about the Jobs, their status and Servers and some control like re-enqueueing jobs, aborting jobs and more. It’s implemented as OWIN middleware so you can plug it into a lot of apps and even self host it in a console app.

In a small scale application all those roles may be fulfilled by a single machine. For example you can configure your ASP.NET Core MVC application to also start a hangfire server, host the hangfire dashboard, use in-memory storage and use the client from within your application as well. When your application grows you can scale out by adding external machines to act as Hangfire servers or Hangfire dashboard and use a SQL Server instance as storage, tying them together.

NuGet Packages and their content

The Hangfire package is a metapackage to install the following three packages in one go: Hangfire.Core, Hangfire.SqlServer and Hangfire.AspNetCore. If you have an older application which does not implement at least .NET Standard 1.3, the Hangfire.AspNetCore package will not be installed, Hangfire will instead be dependent on Microsoft.Owin.Host.SystemWeb for hosting. If your goal is to extend an existing webapp that uses SQLServer as database with Hangfire, this package is for you.

If you want something else, you need to mix and match the packages according to your individual needs. We’ll dive into the packages a little bit so you can identify the ones you need.

Hangfire.Core, as the name implies, contains the core functionality of Hangfire like the client, the server and the dashboard. It also defines the interfaces used for persisting the data in storage but lacks the implementation of these interfaces.

Hangfire.SqlServer implements the interfaces defined in Hangfire.Core to persist Hangfires data in SQL Server, SQL Server LocalDB and Azure SQL. It is also in charge of creating or upgrading the database schema if needed. Please note that this has a runtime dependency on either Microsoft.Data.SqlClient or System.Data.SqlClient. Use the former whenever possible as there seem to be problems with the second one when using the new recommended settings for Hangfire utilizing a long polling technique.

Hangfire.NetCore provides handy extension methods to add Hangfire to any .NET (Core) application. You can run the processing Server as a hosted service within your app for example.

Hangfire.AspNetCore is used to integrate Hangfire with your ASP.NET (Core) application with useful extension methods.

Jobs

Let’s take a look at the lifecycle of a Hangfire “job” via the following statechart diagram.

A job begins its life in awaiting, scheduled or enqueued state, depending on how the job was created by the client (This will be elaborated further in part 2). It is now sitting there, waiting for its processing to start.

When a Hangfire server comes along and starts processing the job, its state is set to processing.

When an exception occurs it transitions to the failed state and will eventually be set to scheduled again to retry execution at a later point. If no retry attempts are left, it will be left in the failed state and remain there forever until it is deleted by you. If you want Hangfire to delete failed jobs on its own, you can set the AttemptExceededAction accordingly.

When processing was successful, the job will reach succeeded state and will be deleted once the retention period is over.

When you delete a job via the client, or have Hangfire configured to delete failed jobs automatically it will be in deleted state until the retention period is over.

State election is performed by Hangfire itself but can be extended to accommodate custom, user defined states if needed.

Hangfire Client’s public interface

No we’ll learn how to place jobs in storage for Hangfire server to process: Use the static methods provided by the Hangfire.BackgroundJob class for this:

  • Enqueue creates a fire and forget job,
  • Schedule creates a job that is to be executed after some specified time,
  • ContinueJobWith creates a job that is executed after another job finishes (successfully),
  • Reschedule changes the state of an existing job to ScheduledState,
  • Requeue changes the state of an existing job to EnqueuedState and
  • Delete changes the state of an existing job to DeletedState.

These static methods are thread-safe whist some instance methods may not be. To reference a job you use the jobId (string) that Hangfire returned after the job was successfully placed in storage.

When was a job successfully placed in storage? If the method call returned without throwing an exception, you are good. Does the client write anything to the logs? No, only the server uses logging infrastructure. The client will inform you with exceptions and return values about what is going on. Whether or not a job completed successfully is tracked by the Hangfire Server because it’s the thing that processes them.

Join me in part 2 where we’ll create a console app and play around with the client.

Photo by Mark Hindle on Unsplash