United Kingdom: +44 (0)208 088 8978

Background tasks with Hangfire

We're hiring Software Developers

Click here to find out more

We love the cloud at Compositional IT. One thing that working with a cloud platform like Azure makes easy is the ability to quickly and easily provision infrastructure. In addition, there are ready-made SDKs for working with that infrastructure.

For example, if you want a way to schedule tasks to run later, you can provision an Azure Queue and use functions in the Azure WebJobs SDK to both publish messages to a queue and subscribe to messages from it. There are simple patterns like the outbox pattern that you can use to make this all more resilient.

In summary, the cloud is great. However, we understand that people choose to run applications outside the cloud for a variety of reasons. Can those applications benefit from a resilient way of scheduling tasks for later execution? Hangfire is a library built for exactly that scenario.

Hangfire

In its own words, Hangfire is

An easy way to perform background processing in .NET and .NET Core applications. No Windows Service or separate process required.

Backed by persistent storage. Open and free for commercial use.

In case it's not apparent, this is quite significant. Running background tasks in an ASP.NET Core application is risky because the host may decide to take your app down. If the host isn't aware of your background task (for example because it's running on a thread outside the request/response cycle), it may shut down your app partway through task execution.

Hangfire has a variety of job types to choose from:

  • Fire-and-forget (executed immediately)
  • Delayed (executed at scheduled time)
  • Recurring (executed on a cron schedule)
  • Continuations (executed after another job has finished)

Hangfire and SAFE

It's pretty easy to translate Hangfire's ASP.NET Core getting started guide to SAFE Stack. I've published a safe-hangfire GitHub repo that you can check out, but I'll show the important steps in this post.

Install Hangfire packages using paket

Add Hangfire packages to ./paket.dependencies

+nuget Hangfire.Core
+nuget Hangfire.SqlServer
+nuget Hangfire.AspNetCore

Reference them in ./src/Server/paket.references

+Hangfire.Core
+Hangfire.SqlServer
+Hangfire.AspNetCore

Install the new packages

dotnet tool restore
dotent paket install

Set up a database

If you have Docker installed, run the following in your shell

docker run --name safe-hangfire -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=yourStrong(!)Password' -p 1433:1433 -d mcr.microsoft.com/mssql/server:2019-latest

Then use your method of choice to create a database in that database server called SafeHangfire. For example, connect to the database server using Azure Data Studio and execute the following query

CREATE DATABASE [SafeHangfire]
GO

If you create your database in a different manner, you will need to use a different connection string in ./src/Server/appsettings.json.

Configure Hangfire

Add ./src/Server/appsettings.json including a Hangfire connection string

{
    "ConnectionStrings": {
        "HangfireConnection": "Data Source=localhost;Database=SafeHangfire;User=sa;Password=yourStrong(!)Password"
    },
    "Logging": {
        "LogLevel": {
            "Default": "Debug",
            "System": "Information",
            "Microsoft": "Information",
            "Hangfire": "Information"
        }
    }
}

Configure the server to use Hangfire by changing ./src/Server.fs

open Microsoft.AspNetCore.Builder
open Microsoft.Extensions.Configuration
open Microsoft.Extensions.DependencyInjection
open Hangfire
open Hangfire.SqlServer
open System

...

let configureServices (services: IServiceCollection) =
    let config = services.BuildServiceProvider().GetService<IConfiguration>()

    services.AddHangfire(fun (configuration: IGlobalConfiguration) ->
        configuration
            .SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
            .UseSimpleAssemblyNameTypeSerializer()
            .UseRecommendedSerializerSettings()
            .UseSqlServerStorage(
                 config.GetConnectionString("HangfireConnection"),
                 SqlServerStorageOptions
                    (
                        CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
                        SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
                        QueuePollInterval = TimeSpan.Zero,
                        UseRecommendedIsolationLevel = true,
                        DisableGlobalLocks = true
                    ))
            |> ignore)
    |> ignore

    services.AddHangfireServer() |> ignore

    services

let configure (app: IApplicationBuilder) =
    app.UseHangfireDashboard()

...

let app =
    application {
        url "http://*:8085"
        use_router webApp
        memory_cache
        use_static "public"
        use_gzip
        service_config configureServices
        app_config configure
    }

Add a button to queue jobs

Add a new API endpoint to ITodosApi in ./src/Shared/Shared.fs

      queueJob: unit -> Async<unit>

Implement it in ./src/Server/Server.fs

      queueJob =
          fun () ->
              async {
                  BackgroundJob.Enqueue(fun () -> Console.WriteLine $"Job queued at {DateTime.Now}") |> ignore

Add a new Msg case in ./src/Client/Index.fs

    | QueueJob

Handle the new case in the update function by calling the new API endpoint

    | QueueJob -> model, Cmd.OfAsync.attempt todosApi.queueJob () (fun _ -> failwith "Error queuing a job.")

Define a new button triggeting that message in the view function

                            Bulma.buttons [
                                Bulma.button.button [
                                    color.isPrimary
                                    prop.onClick (fun _ -> dispatch QueueJob)
                                    prop.text "Queue a job"
                                ]
                            ]

Test it out

Start the app

dotnet run

Browse to http://localhost:8085/hangfire to see the Hangfire dashboard. Browse to http://localhost:8080 to see the app. Use the "Queue a job" button to send a request to the server to queue a job.

You can see the job details in the dashboard.

Dashboard showing a recently executed job

You can also see the details in the database.

Database query and results showing Hangfire job details

The server logs show the effect of the executed job.

server: Job queued at 02/12/2022 15:06:27

Thoughts

That was a very quick intro to Hangfire and how to set it up in a SAFE app. We only showed scheduling a job for immediate execution, but—as mentioned before—it's possible to schedule jobs to run later or periodically, or even after another has finished.

I really like that the persistence details are abstracted away. I can go and inspect the database tables if I'm so inclined, but it's very easy to use Hangfire without knowing how that works. This means that I can get all the resilience benefits that Hangfire offers without having to worry about persistence myself. Configuration was painless and I was very pleasantly surprised by how easy it was to integrate the dashboard.

Overall, Hangfire looks like a great option for easy resilient background processing in SAFE apps. It is especially worth considering when operating outside of a cloud platform. Thanks to the Hangfire team for making this tool available in the .NET ecosystem!