Understanding the ASP.NET Core Boilerplate in Giraffe

2022-01-05

Introduction

Giraffe is one of the leading web frameworks for F#. It provides a wonderful functional interface while allowing seamless access to existing ASP.NET Core libraries.

However, getting started with Giraffe may be intimidating to someone with no ASP.NET Core experience, because even the simplest application requires ~50 lines of ASP.NET Core-specific boilerplate code. The last thing one wants to do while learning F# and Giraffe is to have to learn another huge framework, documented mostly for C# developers.

I hope this article will be a useful guide for such a person.

Getting Started

I assume you have at least .NET 5.0 SDK installed on your system. If you're just starting out with F# on Linux, you can check out my article F# for Linux People. Start off by running:

dotnet new -i "giraffe-template::*"
dotnet new giraffe -o UrlShortener # (or whatever name you want)

Application Configuration

Now, let's start our exploration of the generated boilerplate code by looking at the main function in Program.fs:

[<EntryPoint>]
let main args =
    let contentRoot = Directory.GetCurrentDirectory()
    let webRoot     = Path.Combine(contentRoot, "WebRoot")
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(
            fun webHostBuilder ->
                webHostBuilder
                    .UseContentRoot(contentRoot)
                    .UseWebRoot(webRoot)
                    .Configure(Action<IApplicationBuilder> configureApp)
                    .ConfigureServices(configureServices)
                    .ConfigureLogging(configureLogging)
                    |> ignore)
        .Build()
        .Run()
    0

That sure looks a little intimidating! Fortunately, though, you won't have to change much of it for most basic projects. We'll examine this function from the inside out.

Now, you will most likely make changes when you want to use a middleware library (which, really, is the point of using Giraffe over something like Suave, which shares a similar design but is not based on ASP.NET Core). To this end, the two most interesting parts are:

(Note: Even though configureApp comes before configureServices in the code, configureServices is executed before configureApp. I'm not exactly sure why the boilerplate mixes up the order.)

configureServices

configureServices is responsible for enabling various features in our app. In the generated boilerplate code, you'll find that it looks like this:

let configureServices (services : IServiceCollection) =
    services.AddCors()    |> ignore
    services.AddGiraffe() |> ignore

Basically, this is telling ASP.NET: "Hey, we're going to be using Cors and Giraffe in this app, please get those subsystems ready!" Technically, each service we add is being registered as an object in the Dependency Injection system.

You'll be modifying this function when you enable other middleware libraries for your application, and when adjusting various parameters passed to the Add methods. For example, in order to enable authentication or authorization, you might add a line like services.AddAuthentication(...) or services.AddAuthorization(...), respectively.

There may be occasions where you access these injected objects directly. In this case, you can use HTTPContext's GetService method. (More on this in Part 3 of this series).

configureApp

Next, let's look at configureApp:

let configureApp (app : IApplicationBuilder) =
    let env = app.ApplicationServices.GetService<IWebHostEnvironment>()
    (match env.IsDevelopment() with
    | true  ->
        app.UseDeveloperExceptionPage()
    | false ->
        app .UseGiraffeErrorHandler(errorHandler)
            // .UseAuthentication() // If you were doing authentication
            .UseHttpsRedirection())

        .UseCors(configureCors)
        .UseStaticFiles()
        .UseGiraffe(webApp)

This function defines the app's middleware pipeline. Each middleware "layer" of the pipeline is declared using the corresponding "use" function.

First, notice that the calls to UseCors and UseGiraffe. These two middlewares were enabled by the two lines in configureServices. If you had enabled the authentication service, you would be able to call UseAuthentication here (shown above as a comment). The other middlewares are available by default and require no additional services.

Each HTTP request received by the app will be processed by this pipeline, in order. Yes, the order of declaration here is crucially important.

In the boilerplate code show above, the pipeline is (in production mode):

  1. Redirect to the exception handler, if there is an error (UseDeveloperExceptionPage)
  2. Redirect HTTP traffic to HTTPS (UseHttpsRedirection)
  3. Check to see if the request is Cross Origin and apply CORS logic (UseCors)
  4. Check to see if the request is for a static file (UseStaticFiles)
  5. Pass the request over to the Giraffe webapp (UseGiraffe)

Each middleware layer can make modifications and decide whether to continue with the pipeline, or exit early. For example, when the browser makes a request for a static file (images, CSS files, etc), the Static Files middleware will recognize that it is a file, send the response, and terminate the pipeline. Since the Giraffe app comes after the static file middleware, it never sees requests for static files.

Similarly, if you were using the Authentication/Authorization middleware, it would be critically important to have the middleware come before any middleware that can serve protected content. Otherwise, sensitive information could be served accidentally, before the Authz middlewares can examine it.

Giraffe itself is actually also a layer of middleware (UseGiraffe(webApp)) in the ASP.NET Core pipeline, responsible for calling your webApp. Note that within the webApp, you will define what is sometimes called the "Giraffe pipeline". This is distinct and separate from the larger ASP.NET Core pipeline.

Host Configuration

Now that we've covered service and pipeline configuration, let's return to the rest of the main function:

[<EntryPoint>]
let main args =
    let contentRoot = Directory.GetCurrentDirectory()
    let webRoot     = Path.Combine(contentRoot, "WebRoot")
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(
            fun webHostBuilder ->
                webHostBuilder
                    .UseContentRoot(contentRoot)
                    .UseWebRoot(webRoot)
                    .Configure(Action<IApplicationBuilder> configureApp)
                    .ConfigureServices(configureServices)
                    .ConfigureLogging(configureLogging)
                    |> ignore)
        .Build()
        .Run()
    0

The rest of main is what can broadly be called "Host" configuration, as opposed to the "Application" configuration that we've been concerned with thus far. For more about this distinction, see Andrew Lock's article.

Proceeding from the inside-out, we see lines for configuring the content root (UseContentRoot(contentRoot)), web root (UseWebRoot(webRoot)) and logging (ConfigureLogging(configureLogging)).

Content root and web root merely tell the app where to find static files during runtime. For example, the Giraffe template will have generated a main.css file in the WebRoot directory. Note that, as coded above, these directory are with respect to the currently running directory. Therefore, when running the application in production, you'll have to make sure to start it from the correct directory.

configureLogging (function not shown here) unsurprisingly configures logging. By default it logs to console at the "debug" log level.

Finally, we have Host.CreateDefaultBuilder and .ConfigureWebHostDefaults, which envelopes all the methods we have been talking about so far. Together, these two methods take care of:

Actually, it does a lot more than that, BUT in general you should not have to worry about most of it.

Incoming Changes in .NET 6.0

Everything in this article assumes .NET 5.0 because that's what the giraffe template uses. .NET 6.0 and ASP.NET Core 6.0 will both bring large changes. When Giraffe is updated for 6.0, some changes I anticipate are:

Overall, the syntactic changes may be major, but should similar semantically. You will need to register services with Add methods, middleware with Use methods.

Next Steps

Now that the ASP.NET Core bits of Giraffe are not too scary, I'll introduce our example URL Shortener project and begin coding its frontend with Giraffe.ViewEngine.