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:
- The
configureServices
function you pass to the.ConfigureServices()
method, and - The
configureApp
function you pass to the.Configure()
method.
(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):
- Redirect to the exception handler, if there is an error (
UseDeveloperExceptionPage
) - Redirect HTTP traffic to HTTPS (
UseHttpsRedirection
) - Check to see if the request is Cross Origin and apply CORS logic (
UseCors
) - Check to see if the request is for a static file (
UseStaticFiles
) - 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:
- Loading configuration settings from environment variables, configuration files (
appsettings.json
), and other sources; - Configuring the built-in HTTP server (called "Kestrel");
- Enabling logging and other basic features;
- Adding some default middleware handlers, such as Host Filtering and Forwarded Headers.
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:
Host.CreateDefaultBuilder()
will becomeWebApplication.CreateBuilder()
.- The call to
ConfigureWebHostDefaults
(or whatever its replacement will be called) may be implicit and therefore not present. - There might not be a
[<EntryPoint>]
function, as .NET 6 renders entry points optional.
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.