ASP.NET is a great framework for building web applications. It’s tried and trusted, fully extensible, has a great community, and thanks to .NET Core, supported across Linux, MacOS, and Windows. Being built with .NET, we have the wise option of building our application with F#, but without intervention, we’d be stuck with using the object-oriented C# types that are throughout ASP.NET.
To overcome this, we’ll leverage Suave, which can be a full replacement for ASP.NET, and
Suave.AspNetCore to tie the two together. Let’s start with a generated project thanks to
yo aspnet and the
Web API Application (F#) template it provides, stripping out some of the template’s defaults to have a blank slate. We’ll still have some of that object-oriented C# feel, but it will be restricted to the console application:
and a minimal
Suave.AspNetCore exposes a
UseSuave extension method on ASP.NET’s
ApplicationBuilder that acts as the connection point between ASP.NET and Suave. Overall,
Suave.AspNetCore accomplishes this connection through two main pieces:
SuaveMiddlewarethat implements ASP.NET Middleware‘s method signatures
- A bi-directional mapping between ASP.NET’s
We can use Suave’s API to control request/response pipeline from here, sticking to full F# and Suave’s
HttpContext (opposed to the ASP.NET
We’re going through this setup to leverage one of Suave’s core principles: the
WebPart. From Suave’s documentation:
A web part is a thing that acts on a HttpContext, the web part could fail by returning
Noneor succeed and produce a new HttpContext. Each web part can execute asynchronously, and it’s not until it is evaluated that the async is evaluated. It will be evaluated on the same fibre (asynchronous execution context) that is consuming from the browser’s TCP socket.
WebParts give two benefits, composability (combining small pieces into larger ones) and asynchronism (which also aids in composability). In essence, it’s type boils down to this (which is useful to know as your editor may display either the left-hand side or the right-hand side depending on how F# infers the type for a given expression):
WebPart accepts Suave’s
HttpContext and returns an async option. The option is what gives applications the ability to control execution flow. When execution of a code path should stop, the
WebPart will return
None, but otherwise, it will return
Some httpContext with a new
HttpContext with any desired updates. Because this process is wrapped in
async, we aren’t penalized too much as our application decides how to handle an incoming request.
One note to make is that instead of F#’s normal function composition operator (
>>), Suave exposes a fish operator (
>=>) to aid in working with
WebParts and removes some necessary handling of
Async<HttpContext option>that aids developer productivity. We’ll see this operator in action later on as we build up our application.
For now, let’s just begin with a small starter
WebPart, thanks to
As our application grows, we’ll use
choose to facilitate paths a request may take and
path to filter part of the decision tree based on request path. Here, we add some basic routes:
Not only can we use these combinators to create a decision tree to route requests, we can also create a
WebParts to set a header or affect the context other ways:
setServerHeader in our
app expression at the top level, but it would be just as happy deeper in the expression.
path can prevent further combinators from affecting the response, so if
setServerHeader is added after a
path expression (or some similar combinator), the response will only have the header set if that part of the decision tree is successfull. For instance, with:
/server will have the
Server header set with the value
kestrel + suave, while responses for
/no-server will have the default value set for the
Server header, thanks to the
WebPart type (remember, it returns an
We can also use
WebParts to compose multiple application segments, introducing some order as our application grows:
Let’s see if we can clean up
api to remove some duplication. I saw this pattern out on the web at some point:
I like the separation here, but honestly, I’m not sure it helps the situation much. We still have a similar problem, plus the additional code for managing the paths. Still not acceptable in my book, so lets try something else. My next inclination is to attempt to nest
Sadly, this doesn’t work as expected and results in a
404 Not Found. I wasn’t lucky in looking for an official solution yet, but are custom combinators an option? Let’s try to build one. Here’s what
path‘s implementation looks like:
Essentially, it checks the path given to
path against the request’s path, returning
None if there’s no match. For our custom combinators, we’ll need to check the request path against the string passed to our new
path as well as the path set above it.
HttpContext has a
userState field, meant for storing state information within a single request, perfect for our use-case of storing info about the entire path for a given code path. Here are our new combinators:
rootPath allows us to specify a path prefix for
subPath calls specified deeper in the decision tree. Because
rootPath stores any previous root path concatenated with the supplied value, we luckily get nesting support beyond a single level. Here’s a simple example, clearing up our previous
I’m looking to contribute this functionality for inclusion into Suave’s API and am currently awating feedback from the team.
.NET Core is still relatively new when compared to the mainstream .NET Framework. Because of this, Suave’s support for it is still in progress (only two of seven additional official packages provide .NET Core support), and community extension of its .NET Core support is still improving (
Suave.AspNetCore doesn’t yet support all of Suave’s feature set). As the community progress .NET Core support for F# and its projects, this relative newness feeling should diminish, and Suave + F# applications on the ASP.NET Core stack should be ready for production.
That being said, there’s no reason Suave cannot be used with ASP.NET Core in projects where 100% compatibility isn’t required. Personal projects, one-off projects, etc. would, in my opinion, give you a chance to use Suave and ASP.NET Core together in a lower-risk situation.