Add caching for static resources in Saturn
Published onSaturn is a great open source functional-first web framework that makes using ASP.NET Core more attractive to F# developers. If you’ve never used it before I highly recommend reading the docs and trying it out yourselves.
Saturn is built around Giraffe taking it’s
core concept of HttpHandler
to a whole new level, by introducing composable pipelines
, routers
, controllers
configured using powerful computation expressions.
Let’s take a look at how a basic web app is configured in Saturn:
open Saturn
open Giraffe
let helloWorld = text "hello world"
let mainRouter = router {
get "/" helloWorld
not_found_handler (setStatusCode 404 >=> text "Oops, not found")
}
let app = application {
use_router mainRouter
url "http://0.0.0.0:8080"
use_static "public"
}
run app
The application
computation expression makes configuring the app very concise. As you can see, the standard use_static
workflow operation can be configured with the name of the folder that is used to serve static files from, but no way of configuring caching for these static resources.
Let’s see how we can extend the Saturn ApplicationBuilder
type with a new custom operation to enable this. (I will assume you’re familiar with F# going further):
module SaturnExtensions
open Saturn
open System
open System.IO
open Microsoft.Extensions.Primitives
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.StaticFiles
open Microsoft.AspNetCore.Builder
type Saturn.Application.ApplicationBuilder with
[<CustomOperation("use_static_with_cache")>]
member _.UseStaticWithCache (state, path, (cacheMaxAge: CacheDuration)) =
let middleware (app : IApplicationBuilder) =
let options = StaticFileOptions()
options.OnPrepareResponse <- fun ctx ->
let h = ctx.Context.Response.Headers
let hv = StringValues($"public, max-age={cacheMaxAge.Seconds}")
h.["Cache-Control"] <- hv
match state.MimeTypes with
| [] -> ()
| mimes ->
let provider =
FileExtensionContentTypeProvider()
mimes
|> List.iter (fun (extension, mime) ->
provider.Mappings.[extension] <- mime)
options.ContentTypeProvider <- provider
app.UseDefaultFiles()
.UseStaticFiles(options)
let webHostConfig (builder: IWebHostBuilder) =
let p = Path.Combine(Directory.GetCurrentDirectory(), path)
builder.UseWebRoot(p)
{ state with
AppConfigs = middleware::state.AppConfigs
WebHostConfigs = webHostConfig::state.WebHostConfigs
}
We’ve added a new custom operation called use_static_with_cache
that is very simmilar to the standard use_static
but also enables the caller to provide a CacheDuration
argument.
We make use of the standard UseStaticFiles
middleware and we hook into the OnPrepareResponse
callback to inject the Cache-Control HTTP response header required by caching to work. We also plug into the configuration pipeline in order to specify the web root for the given static resources path.
The cache duration is specified in seconds and this is computed by the Seconds
member attached to the CacheDuration
type, defined bellow:
type CacheDuration =
| Days of int
| Weeks of int
| Months of int
member this.Seconds =
let now = DateTime.Now
let diff x =
let t = x - now
t.TotalSeconds |> int
match this with
| Days v -> now.AddDays(float(v)) |> diff
| Weeks v -> now.AddDays(float(v * 7)) |> diff
| Months v -> now.AddMonths(v) |> diff
Using the new custom operation is easy. Simply replace the call to use_static
with the new operation:
let app = application {
use_router mainRouter
url "http://0.0.0.0:8080"
use_static_with_cache "public" (Months 6)
}
Make sure you open up the the
SaturnExtensions
module in order to have access to the type extension. Or mark the extensions module with theAutoOpen
attribute.
Any static resource served from the public
folder should now have the following HTTP response header attached: Cache-Control: public, max-age=15897599
I hope this article gives a glimpse on how easy Saturn is to use, but also extend when required.