Hangfire Job Scheduling in ASP.Net Core 3.0

A lot of times, we need to create applications that need some sort of time-based scheduling. Hangfire helps us right there. Hangfire provides a very easy and fluent way to create and manage scheduled jobs.

The disadvantage of creating a solution for scheduling job from scratch are the following:

  • Firstly, it is usually cumbersome and time-consuming to build something like this.
  • Secondly given it is not the core feature of the application, there is very little return on investment.
  • Finally, it is usually error-prone to build something like that from scratch.

Creating schedule jobs to run at a particular interval is always tedious. And also error-prone. It encapsulates all these and provides very easy to use API for configuring jobs.

In this blog, I will walk through creating an ASP.Net application, which will use it to manage recurring jobs.

Basic Hangfire concepts

It has three main components for managing jobs. The first one is Storage, the second one is the Client and the third one is the Server.

Storage

It uses storage for keeping all the job information for processing. Every single detail about the job is saved in the storage in a serialized format. The design for the storage system is extensible. And we can implement an extension for any type of storage medium.

The storage is added into the Hangfire through configuration during startup. And it is a mandatory component that is needed for it to work.

In this example for this blog, I will first use In-Memory storage provider, and then eventually use SQL Server storage provider.

Client

In Hangfire, the client is nothing but the API the consumers use to create background jobs. The client is responsible for storing the jobs into the storage provider configured.

The background jobs should always be performed outside of the main execution context. And it manages this internally without needing for additional configuration.

Server

The server is the component responsible for executing the background jobs. It gets information about the scheduled jobs from the storage and executes them based on the configuration.

The server listens to the storage through multiple threads for new jobs. And once they are available, perform them by deserializing the job information saved in the storage.

The server is represented by the class BackgroundJobServer. And the background job server can be running in any process.

Creating an ASP.Net Core Application

First of all, I will create a new ASP.Net Core web application. For that I will follow the following steps:

  • Open Visual Studio 2019
  • In the menu, select File -> New Project
  • Next, in the pop-up model, select ASP.NET Core Web Application and click Next
  • After that, on the next page of the pop-up, I will provide the name of the project in the Project Name field as Hangfire.Demo and click on the Create
  • Finally, on the next page, I will select the option Empty template, and I will keep other values default (ASP.Net Core 3.1) and click Create

Once the project is ready, I will open the NuGet package manager UI and add the necessary NuGet packages. The three main Nuget packages needed for hangfire are:

  • Hangfire.Core – The core package that supports the core logic of Hangfire
  • Hangfire.AspNetCore – Support for ASP.Net Core Middleware and Middleware for the dashboard user interface
  • Hangfire.MemoryStorage – This package will provide in-memory storage for saving the background jobs

Configuring Hangfire

Once all the NuGet packages are installed, it is time to configure the server. To do that, I will open the Startup.cs file and update the ConfigureServices method of the Startup class.

I will first call the AddHangfire extension method on the IServiceCollection instance to add the Hangfire to the Dependency Injection container.

services.AddHangfire(config =>                 
    config.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
        .UseSimpleAssemblyNameTypeSerializer()
        .UseDefaultTypeSerializer()
        .UseMemoryStorage());

In the above code, the AddHangfire method takes an Action delegate, which passes IGlobalConfiguration of the Hangfire ecosystem to configure the Hangfire.

And here first I will call SetDataCompatibilityLevel method on the IGlobalConfiguration instance passed to set the compatibility to Version_170.

Next, I will call the UseSimpleAssemblyNameTypeSerializer and UseDefaultTypeSerializer one after the other, to set serialization configuration.

Finally, I will call UseMemoryStorage on the IGlobalConfiguration instance to set up an in-memory storage provider.

Next, I will call the extension method AddHangfireServer on the IServiceCollection instance to add the Hangfire server to the dependency injection container. Which we will use later to configure and run jobs.

services.AddHangfireServer();

Update Configure method

Once the basic setup for the dependency injection container is done, now I will add the middleware needed to add the Hangfire dashboard UI. For that, I will call the extension method UseHangfireDashboard to the IApplicationBuilder instance.

app.UseHangfireDashboard();

Finally, just to test a basic Hangfire job, I will use the Enqueue method of the IBackgroundJobClient instance to enqueue a simple Console.WriteLine statement.

To achieve this I will update the Configure method to accept a new parameter IBackgroundJobClient. This will be passed by the dependency injection container, since we added the Hangfire server earlier by calling the AddHangfireServer method.

backgroundJobClient.Enqueue(() => Console.WriteLine("Hello Hangfire job!"));

Complete code change in the Startup is below.

using System;
using Hangfire.MemoryStorage;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Hangfire.Demo
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddHangfire(config =>
                config.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
                .UseSimpleAssemblyNameTypeSerializer()
                .UseDefaultTypeSerializer()
                .UseMemoryStorage());

            services.AddHangfireServer();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(
            IApplicationBuilder app, 
            IWebHostEnvironment env,
            IBackgroundJobClient backgroundJobClient)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Hello World!");
                });
            });

            app.UseHangfireDashboard();
            backgroundJobClient.Enqueue(() => Console.WriteLine("Hello Hangfire job!"));
        }
    }
}

Running the application

Now, the above job will just print Hello Hangfire job! to the console output.

I will run the application to see the output as well as the Hangfire dashboard UI. To access the dashboard UI, we will navigate to the resource /hangfire.

simple hangfire job
Console output
hangfire dashboard
Hangfire Dashboard UI

Above in the dashboard UI, we can see the Console.WriteLine job as completed.

As you can see here, the job itself is saved as Console.WriteLine. It is because Hangfire serializes the method, type and other information along with the state in the storage. And during execution, the server deserializes this information for execution.

Creating a recurring job

Creating a background job as we did above is easy with Hangfire, but it is as easy using an instance of Task class as well. So why go with something like Hangfire and install all these packages into the project?

Well, the main advantage of Hangfire comes in when we use it for creating scheduling jobs. It uses CRON expressions for scheduling.

Let us say we need to create a job that is responsible for printing some text into the console every minute. To do that, let us first create a class that will do just that.

namespace Hangfire.Demo
{
    public interface IPrintJob
    {
        void Print();
    }
}

using System;

namespace Hangfire.Demo
{
    public class PrintJob : IPrintJob
    {
        public void Print()
        {
            Console.WriteLine($"Hanfire recurring job!");
        }
    }
}

Configure Startup class

Once the PrintJob class is created, it is time to configure the Startup class. In the Startup class, the objective is to configure a recurring job to call the Print method every minute.

Firstly, I will add the PrintJob to the dependency injection container in the ConfigureServices method.

services.AddSingleton<IPrintJob, PrintJob>();

Secondly, I will update the Configure method to take two new parameters. The first one is the IRecurringJobManager necessary for creating a recurring job. And the second one is the IServiceProvider to get the IPrintJob instance from the dependency injection container.

Thirdly, I will call the AddOrUpdate on the IRecurringJobManager instance to set up a recurring job.

recurringJobManager.AddOrUpdate(
                "Run every minute",
                () => serviceProvider.GetService<IPrintJob>().Print(),
                "* * * * *"
                );

In the above code, the CRON expression “* * * * *” is an expression to run the job every minute.

The complete change in the Startup code is below:

using System;
using Hangfire.MemoryStorage;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Hangfire.Demo
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddHangfire(config =>
                config.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
                .UseSimpleAssemblyNameTypeSerializer()
                .UseDefaultTypeSerializer()
                .UseMemoryStorage());

            services.AddHangfireServer();

            services.AddSingleton<IPrintJob, PrintJob>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(
            IApplicationBuilder app, 
            IWebHostEnvironment env,
            IBackgroundJobClient backgroundJobClient,
            IRecurringJobManager recurringJobManager,
            IServiceProvider serviceProvider)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Hello World!");
                });
            });

            app.UseHangfireDashboard();
            backgroundJobClient.Enqueue(() => Console.WriteLine("Hello Hanfire job!"));
            recurringJobManager.AddOrUpdate(
                "Run every minute",
                () => serviceProvider.GetService<IPrintJob>().Print(),
                "* * * * *"
                );
        }
    }
}

Now let us run the application and see the result in the console as well as the dashboard.

hangfire recurring job
Recurring job console
recurring jobs dashboard
Hangfire dashboard Succeeded jobs tab
Hangfire dashboard Recurring jobs tab

As we can see, in the Recurring Jobs tab of the dashboard, we can see the recurring jobs scheduled.

SQL Server for Job Storage

Now let us explore, how we can use SQL Server for job storage. For using SQL Server for job storage, first, we will have to create the database.

Firstly, I will open the SQL Server Management Studio and connect to the local database SQL Express server.

Secondly, I will execute the following command in the SQL Server to create a new database named HangfireTest:

CREATE DATABASE [HangfireTest]
GO

Once the database is created, now it is time to update the existing project to use this database as the job storage.

Adding database configuration

Firstly, I will add a new configuration to appsettings.json file with the name Hangfire.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "Hangfire": "Server=.\\sqlexpress;Database=HangfireTest;Integrated Security=SSPI;"
}

Secondly, I will add a new NuGet package Hangfire.SqlServer. For doing that I will open the NuGet package manager and search for Hangfire.SqlServer and install it.

Once that is done, it is time to configure the SQL Server storage in the Startup class. For that, I will change the AddHangfire method to replace memory storage with SQL Server storage.

Also, since the connection is in configuration, I will update the Startup class to add a constructor. And the constructor will expect the IConfiguration to be available through dependency injection.

public Startup(IConfiguration configuration)
{
    Configuration = configuration;
}

public IConfiguration Configuration { get; }

To replace memory storage with SQL Server storage, I will replace UseMemoryStorage extension method with UseSqlServerStorage extension methid. The UseSqlServerStorage takes the connection string as a parameter. And SqlServerStorageOptions as second parameter in another overload.

For this example, I will just pass the connection string for the time being. I will not be passing the SqlServerStorageOptions for the time being.

using System;
using Hangfire.MemoryStorage;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Hangfire.Demo
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddHangfire(config =>
                config.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
                .UseSimpleAssemblyNameTypeSerializer()
                .UseDefaultTypeSerializer()
                .UseSqlServerStorage(Configuration["Hangfire"]));

            services.AddHangfireServer();

            services.AddSingleton<IPrintJob, PrintJob>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(
            IApplicationBuilder app, 
            IWebHostEnvironment env,
            IBackgroundJobClient backgroundJobClient,
            IRecurringJobManager recurringJobManager,
            IServiceProvider serviceProvider)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Hello World!");
                });
            });

            app.UseHangfireDashboard();
            backgroundJobClient.Enqueue(() => Console.WriteLine("Hello Hanfire job!"));
            recurringJobManager.AddOrUpdate(
                "Run every minute",
                () => serviceProvider.GetService<IPrintJob>().Print(),
                "* * * * *"
                );
        }
    }
}

Now if I run the application, I will see the same response as I saw before when I was using memory storage. The only difference is now it will be starting a SQL Server memory storage, and we can see it in the Console capture below.

hangfire sql server
SQL Server Storage

The next very important feature we get out of using SQL Server is that the states are available across processes and process restarts. Meaning now, if I restart the application and open the dashboard, I can see even the job executed earlier are available in the dashboard.

job history
Dashboard

DB Structure

Once we run the application, Hangfire will create all the necessary tables. There are only two tables that are more meaningful for us, one is the Hash table and the second one is the Job table.

The Hash table is where the serialized job is stored by the Hangfire.

Hangfire Hash table
Hash table

The Job table is the one that holds all the job history.

Hangfire Job table
Job Table

Deleting and Triggering Jobs through Hangfire Dashboard

Using the dashboard we can trigger a job as well as delete a job.

So if we have to delete the recurring job, we can just go into the dashboard, select the job and click Delete.

Delete Jobs
Delete Jobs

Now, if we go to the database and select the Hash table, we will not see any rows there.

If we have to trigger the Job, we can just select the job and click Trigger now. That will trigger and run the job immediately outside of the cycle.

Dashboard trigger job
Trigger Job

Conclusion

As we can see from the experiment, using Hangfire for recurring jobs is really simple and easy. And on top of that using the dashboard makes it really powerful.

It works very well with the ASP.Net Core ecosystem. And it can be configured with few lines of code to do any kind of recurring jobs. I just love this framework for any recurring jobs.

YouTube Video URL: https://youtu.be/sQyY0xvJ4-o