It's common when running applications to have to occasionally execute background work outside the context of a specific request. For example, perhaps you have a database table which is recalculated every ten minutes based on some other data plus an expensive calculation. You can use queue messages for this purpose - especially for tasks that need to run on a scheduled basis - but a more lightweight option exists in the form of Background Tasks with come included with newer versions of ASP .NET.
There are essentially two abstractions to know about:
IHostedService
- a "bare bones" interface for running services.BackgroundService
- a base class which implementsIHostedService
but includes some extra bells and whistles and is designed for long-running background tasks.
Here's an example Background Service which can be hooked into ASP .NET:
/// A full background service using a dedicated type.
type MyBackgroundService(logger: ILogger<unit>) =
inherit BackgroundService()
/// Called when the background service needs to run.
override _.ExecuteAsync cancellationToken =
task {
while true do
logger.LogInformation "Background service running."
do! Task.Delay(2000, cancellationToken)
}
/// Called when a background service needs to gracefully shut down.
override _.StopAsync cancellationToken =
task { logger.LogInformation "Background service shutting down." }
// inject into ASP .NET via Saturn
app {
service_config (fun s -> s.AddHostedService<MyBackgroundService>())
}
In this example, the logger is injected by ASP .NET's dependency injection framework.
Of course, in the FP world, parameterisation through inheritance is a little unusual. You can therefore also use something like the following instead:
let serviceFactory (serviceProvider:IServiceProvider) =
let logger = serviceProvider.GetService<ILogger<unit>>()
// Create a BackgroundService using an object expression.
{ new BackgroundService() with
member _.ExecuteAsync cancellationToken =
task {
while true do
logger.LogInformation "Background service running."
do! Task.Delay(2000, cancellationToken)
}
}
// Inject into ASP .NET via Saturn
app {
service_config (fun s -> s.AddHostedService serviceFactory)
}
You can even take this further and abstract all of the boilerplate away, leaving just the body of ExecuteAsync
as a function.
let workerAsFunction (sp: IServiceProvider) (cancellationToken: CancellationToken) : Task =
let logger = sp.GetService<ILogger<unit>>()
task {
while true do
logger.LogInformation "Functional background service running."
do! Task.Delay(2000, cancellationToken)
}
// Code to adapt into a BackgroundService elided...
This looks like a much more idiomatic F# approach, and thankfully with object expressions we can "adapt" a function into an interface without too much difficulty. However, there are a couple of limitations with this approach:
- We are only implementing the ExecuteAsync method; if you want to perform some shutdown logic, you'll need to implement that, too. At this point rather than passing a record or tuple of functions, a class may be a better fit.
- ASP .NET couples background services to types (presumably because of its dependency injection feature) i.e. you may only have one instance of a background worker for a given type. This might be totally reasonable in the OO world, whereby each type represents unique logic, but in F#, it's standard to parameterise logic via parameters rather than e.g. type inheritance. Therefore if you require more than one background service in your application, object expressions are not a great fit.
On the other hand, what is nice is that Saturn's custom keywords can be added in your own packages or code through extension methods. Here's an example to add Background Service support directly into Saturn's app { }
block:
type ApplicationBuilder with
/// Custom keyword to more easily add a background worker to ASP .NET
[CustomOperation "background_service"]
member _.BackgroundService(state: ApplicationState, serviceBuilder) =
{ state with
ServicesConfig =
(fun svcCollection -> svcCollection.AddHostedService serviceBuilder)
:: state.ServicesConfig }
app {
background_service (fun serviceProvider -> new FullBackgroundService(serviceProvider))
}
There's a fully-working repository which expands on the ideas shown here for you to try out yourself.
Summary
Background services in ASP .NET are a useful tool for code-focused long-running or scheduled tasks. Out of the box, they focus on an object-oriented approach. However, you can use F# features such as object expressions (with limitations) to allow for a more functional approach, and can easily integrate them into a standard Saturn application builder with extension methods.