One common scenario when developing applications is the need to perform background processing than can either be run only once (like sending an email) or scheduled to be run multiple times within a given interval (like doing database housekeeping), usually defined by a cron expression.
When I need to implement such requirements my first choice for the last few years as always been Hangfire.
It integrates seamlessly with ASP.NET Core applications, it has a simple but very powerful dashboard to monitor and manually trigger recurring jobs and is open source and completely free for commercial use.
In this article I’m going to explain how to configure both Hangfire server and dashboard into an ASP.NET Core application and recurrently send requests to HTTP endpoints, which will be configured based on application settings without requiring a service restart to detect changes.
Why use Hangfire to trigger HTTP endpoints?
An extremely common approach I use when implementing schedulers that may run millions of jobs per day is to use Hangfire to trigger HTTP endpoints, offloading work to external APIs instead of in process.
By design, Hangfire is implemented to be easily distributed on different machines by orchestrating the job execution using a persistent data storage shared across all servers.
The problem comes when some jobs need more processing power than others or millions of executions per day. You’ll have to maintain multiple nodes looking at different queues, code changes must be properly distributed to the relevant nodes, the database will probably become a bottleneck - even if using some message bus or in-memory cache.
To attenuate this problem I usually prefer to keep Hangfire instances as simple as possible, by recurrently doing simple HTTP requests that can be configured either by settings files or database providers (like exemplified in this article I recently made). This allows to offload the heavy work to external APIs that can be more easily scaled, tested, distributed and monitored and ensuring you’ll need much fewer Hangfire instances to run millions of jobs per day while using the dashboard to keep track of job executions.
Solution setup
In this example, which I have available on GitHub, I’m going to setup an ASP.NET Core application with .NET 8 to host both Hangfire server and dashboard. For simplicity, the job storage will be in-memory but feel free to use any other of the supported ones (like SQL Server).
Start by creating an empty ASP.NET Core project and register the following NuGet packages:
Hangfire.AspNetCore
- depends onHangfire.NetCore
, which is used to run the jobs server onMicrosoft.Extensions.Hosting
as a hosted service, but also brings a Web dashboard that can be used to manually trigger recurring jobs or check execution status;Hangfire.InMemory
- in-memory job storage but you can swap for any other of your preference, likeHangfire.SqlServer
;
Open the Program.cs
file, register both the server and dashboard services into the dependency injection container and configure the dashboard to listen on base address:
1 | var builder = WebApplication.CreateBuilder(args); |
If you run the application, the Hangfire dashboard will be shown with a single server running but without recurring jobs.
As you can see, in just a few minutes you have Hangfire up and ready do run. As stated previously, this is one of the reasons I always use Hangfire for scheduling jobs, the simplicity of it!
HTTP job options
Now that we have both the server and dashboard running, let’s start by defining the recurring HTTP options that we’ll read from the application settings and dynamically configure endpoints to be recurrently triggered.
Let’s start by defining the options for requesting a single endpoint. To keep things simple, I’ll assume the need for an HTTP address, method, headers and a request timeout. It will also need a cron expression, so we can define the recurrency interval, and a flag to enable it so you can manage per environment which jobs are enabled.
1 | public class HttpJobEndpointOptions |
We could now load a collection of HttpJobEndpointOptions
from the application settings but let’s try to organize the endpoints into categories and also give a name for each. This will be handy later to have a more readable job name when looking at the Hangfire dashboard.
1 | public class HttpJobOptions |
Open the appsettings.json
file and configure an IsAlive
job inside a Core
category that’ll run every 5 minutes. This job will be used later for testing, so assume an HTTP request GET /api/is-alive
to itself:
1 | { |
Open the Program.cs
file and configure the HttpJobOptions
from the HttpJobs
section. Let’s also add the testing endpoint to be triggered later:
1 | var builder = WebApplication.CreateBuilder(args); |
HTTP job runner
Now that we have defined the job options, let’s create the class that will be recurrently executed by the Hangfire server.
The core idea of this class is to be responsible to do a single HTTP request by receiving both the category and endpoint names as a parameter, looking up for the configuration in the HttpJobOptions
instance and doing the expected HTTP request. It will also receive a PerformContext
instance which is a special (but optional) class provided by Hangfire to give context to your methods.
1 | public class HttpJobRunner( |
Register into the DI container the HTTP client services (because we are using the IHttpClientFactory
) and also this class as transient.
1 | var builder = WebApplication.CreateBuilder(args); |
HTTP hosted service
It is time to let Hangfire know that we have endpoints that must be requested on a recurrent basis.
To configure the class HttpJobRunner
as a recurring job, we can use either the static method RecurringJob.AddOrUpdate
or the equivalent methods provided by IRecurringJobManager
.
A manual registration of the HTTP:Core:IsAlive
job we defined in the application settings would be as follows:
1 | // using static class RecurringJob |
As you can see, we pass an expression to the AddOrUpdate
method that will be used by Hangfire to build dynamic code to call the RunAsync
method of our HttpJobRunner
class, including the parameters we specify. As stated before, the PerformContext
parameter is special and despite we are passing null when defining the expression, Hangfire will actually create a context each time the method is run.
With this simple yet effective approach, Hangfire doesn’t impose any interface or abstract class that must be implemented, making our life much easier.
This registration could be done at application startup but since we want do detect changes to the application settings without restarting the server, let’s create a hosted service that will be running in the background and either add or remove scheduler jobs. As a note, since Hangfire doesn’t provide a way to get currently registered jobs so we can remove inexistent, we’ll register the hosted service as a singleton and have a collection in-memory to track registered job ids.
1 | public class HttpJobHostedService( |
Register this class into the Dependency Injection container as a hosted service.
1 | var builder = WebApplication.CreateBuilder(args); |
If you now run the application, the Hangfire dashboard will show the HTTP:Core:IsAlive
recurring job to be run every 5 minutes.
Try to change the application settings while the application is running, like changing the cron expression, disabling, removing or creating jobs, so you can see the recurring job list to change accordingly.
If you wait for the jobs to run (or manually trigger) and go to the Jobs
tab, open the Succeeded
status and you’ll see the list of all jobs that have run successfully.
If you open one of the jobs, you’ll see details about it’s execution, like the parameters that were passed, the execution time and, if it was failed, the exception details.
Job filters
The code is looking nice and simple and seems to work very well but there’s still three problems that I think should be solved:
- If you have multiple HTTP endpoints you can’t tell just looking at the list which one was called without checking execution details. In this case every job is called
HttpJobRunner.RunAsync
, independently the endpoint; - Since this is a recurring and stateless job so, when it fails, there’s no point in keeping it in the failed state to be retried, we can move it to the deleted stated that would be automatically housekept by Hangfire;
- If for some reason a job for a given endpoint takes more time to run than its cron interval, multiple executions will overlap and cause requests to happen concurrently, which may not be the expected behavior;
To solve these problems we are going to use Hangfire filters, which are attributes that can be used to annotate methods (or registered globally) to add custom behavior when running a job, working similarly to ASP.NET Core MVC filters.
We are going to use the JobDisplayName
filter to define the job name in the dashboard, the AutomaticRetry
filter to mark the job as deleted when failed, and create a custom filter than will lock job executions per endpoint until the previous one has completed.
Starting by the custom filter, we must extend the JobFilterAttribute
and because it’s changing the behavior of a job execution, it must also implement the IServerFilter
interface. We are going to use both the category and endpoint name parameters to create a unique identifier per endpoint execution and use an Hangfire distributed lock feature to ensure a job for the same endpoint only runs after the lock is disposed by a previous execution.
1 | public class EnableDistributedMutexAttribute : JobFilterAttribute, IServerFilter |
Open the HttpJobRunner
class and annotate the RunAsync
method with a JobDisplayName
, EnableDistributedMutex
and AutomaticRetry
attributes.
1 | public class HttpJobRunner( |
If you now run the application and wait or manually trigger the HTTP:Core:IsAlive
job you’ll see the new display name in the dashboard.
Feel free to also force some exceptions to see the job in the deleted state instead of failed, or delaying the job execution for more time than the defined cron interval to see jobs waiting for the previous ones to complete.
Conclusion
In this article I explained how easy it is to configure the Hangfire server to run recurring jobs in the background of an ASP.NET Core application, and use its dashboard to manage and monitor job executions.
For simplicity we used the in-memory job storage and created the base architecture for jobs that recurrently trigger HTTP endpoints, everything configured using application settings.
As a reminder, this example is available on GitHub so feel free to give it a look.