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.
You can also see the details in the database.
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!