Functional ASP.NET Core
Published
In December 2016 I participated in the F# Advent Calendar where I wrote a blog post on running Suave in ASP.NET Core. As part of that blog post I introduced the Suave.AspNetCore NuGet package which makes it possible to run a Suave web application inside ASP.NET Core via a new middleware.
So far this has been pretty good and as of last week the GitHub repository has been moved to the official SuaveIO GitHub organisation as well. A while ago someone even tweeted me that the performance has been pretty good too:
Even though this made me very happy there was still one thing that bugged me until today.
Why I created Suave.AspNetCore
My main motivation for running Suave inside ASP.NET Core was to benefit from the speed and power of Kestrel while still being able to build a web application in a functional approach. Suave.AspNetCore made this possible, but afterwards I realised that this was not my final goal yet.
Ultimately I would like to build a web application in ASP.NET Core with a functional framework which does not only benefit from Kestrel but also from the entire ASP.NET Core eco system, including other middleware such as static files, authentication, authorization, security, the flexibility of the config system, logging or simply being able to retrieve information from the current hosting environment and more.
There is a lot of great features in ASP.NET Core which have been carefully crafted by experts (e.g. security) and I wouldn't want to miss out on those based on the framework of my choice. Unfortunately with Suave and Suave.AspNetCore I am limited in what other middleware I can use in combination with a Suave ASP.NET Core web application.
Suave and ASP.NET Core - Buddies but not family
Suave doesn't naturally fit into ASP.NET Core the way MVC does.
The reason is because Suave is not only a web framework but more of a web platform similar to what ASP.NET Core is itself. It's probably never been intended to be integrated with ASP.NET Core in the first place.
Think of it like this, ASP.NET Core is a web platform which sets the ground work for building any web application and MVC is a framework on top of the platform, which enables building web applications with an object oriented Model-View-Controller design pattern. Ideally as an F# developer I would like to replace the object oriented MVC framework with a functional equivalent, but keep the rest of ASP.NET Core's offering at the same time. This is very difficult with Suave at the moment.
Suave has its own HTTP server, its own Socket server and its own HTTP abstractions. As a result Suave's Socket implementation is not compatible with ASP.NET Core's web socket server and Suave's HttpContext is vastly different from ASP.NET Core's HttpContext.
This is why the Suave.AspNetCore middleware has to translate one HttpContext
into another and then back again. It works, but it is not ideal, because there's a lot of information that gets lost along the way (e.g. the User
object in ASP.NET Core is of type IPrincipal
and in Suave it is a Map<string, obj>
). This is a limiting factor. For example there's no way to access the Authentication
property, the Session
object or the Features
collection of the original ASP.NET Core HttpContext
from inside a Suave application.
This means that even though I can run a Suave web application in ASP.NET Core, I still have to re-build a lot of the ground work that has been laid out for me by other ASP.NET Core middleware. In some cases this might be a minor problem, but in others I consider it a big issue. Especially when it comes to critical components such as security I would much rather want to rely on the implementation provided by Microsoft and other industry experts than (re-)inventing it myself.
None of this is Suave's fault though, because it has not been designed with ASP.NET Core in mind, which is more than fair, but for people like me who want to benefit from both platforms this is still an important issue to consider.
Why ASP.NET Core?
If Suave is already a well working standalone product for building web applications in a functional way, why would I even bother with ASP.NET Core? Well this is a good question and I can only answer it for myself.
For me the main reasons for using ASP.NET Core are the following:
Performance
Kestrel is extremely fast and the Microsoft team is working hard on making it even faster. Depending on what type of application you are trying to build this can be a big or a small selling point.
Personally I am working on a project which anticipates a significant load at most times and therefore performance is key. I also have a few smaller side projects which run in Docker containers in the cloud and the quicker these applications can handle web requests, the less I have to pay for additional computation power.
Security
As mentioned before I am very reluctant to using 3rd party security code which hasn't been vetted, audited and tested to the same depth as the one provided by Microsoft and the ASP.NET Core team. They have years of invaluable experience and I trust them with security more than other individuals who I barely know or even myself. Call me pessimistic, but security is such a complex topic that I don't even think that a single person should do this on their own. It's one of those things where I believe you have to have a big team of experts and resources behind you to stand a chance against the various vulnerabilities of today.
Laziness
I am a lazy developer. I don't enjoy building the 100th logging framework or coming up with yet another configuration API. I want to build new original ideas and not waste my time on stuff which has been done by thousands of other developers before me. ASP.NET Core is a platform which offers many things out of the box and essentially saves me a lot of valuable time.
Additionally it offers easy integration points for other 3rd party code and has a huge developer base behind it which gives me access to even more useful tools and libraries which can benefit my projects.
Community
The community around ASP.NET (Core) is probably the biggest of all .NET web frameworks. There is significantly more people reporting issues, working on bug fixes and building new features into the product than anyone else. The value of such a vibrant community shall not be underestimated. There's nothing better than having bugs fixed by other people before I even encounter them myself.
Another great side effect is that a lot of my questions might have already been answered on StackOverflow and that there is a ton of other useful blog posts explaining stuff that I don't have to work out myself. As an employer it might be also beneficial to pick a framework which has a bigger talent pool than others.
Impatience
I am very impatient and for me it is very important that certain issues get addressed fairly quickly. For instance in recent years many web servers were upgrading to support HTTP/2 which is an important improvement over HTTP/1.1. My chances of getting such critical updates are probably higher with a product which is used by thousands of developers than something else which is maybe a little bit more niche. This is not always true, but from my experience this is generally the case.
Fear of missing out
We don't know what the future holds for us. Tomorrow someone might release something incredibly awesome which might not be available on smaller platforms in its initial phase. I love working with bleeding edge technology and as such I do have a certain degree of FOMO when it comes to software innovations.
Support
Lastly ASP.NET Core, even though open source, remains an enterprise supported product and that has a lot of value as well. Over the years I had to replace many successful open source packages because the original maintainers got tired of supporting it and no one else stepped in to replace them, which meant they became stale and essentially completely out of date. There is no guarantee that Microsoft will support the ASP.NET (Core) platform for ever, but from the looks of it I don't expect it to go away any time soon either. In fact the current signs seem to suggest the exact opposite considering that ASP.NET itself has already more than a decade on its shoulders and Microsoft recently decided to invest in a complete new re-design to continue its success.
Not everyone might agree with my reasoning, but for me these are very compelling points to stick with ASP.NET Core as my preferred .NET web platform and try to use as much of its ready made features as possible.
Building a functional framework for ASP.NET Core
Now that I have explained why I would like to build a web application with ASP.NET Core I have the only problem that as an F# developer there's no ideal framework available yet.
This got me thinking what if I could build my own little micro framework in F# that borrows the functional design pattern of Suave, but embraces the power of ASP.NET Core?
Defining a functional HttpHandler
A functional ASP.NET Core web application could look as simple as a function which takes in a HttpContext
and returns a HttpContext
. In functional programming everything is a function after all. Inside that function it would have full access to the Request
and Response
object as well as all the other objects to successfully process an incoming web request and return a response.
I could call such a web function a HttpHandler
:
type HttpHandler = HttpContext -> HttpContext
Not every incoming web request can or should be handled by a HttpHandler
. For example if the request was made to a route which wasn't anticipated, like a static file which should be picked up by the static file middleware, then a HttpHandler
should be able to skip this particular request.
In this case there should be an option to return nothing so that another HttpHandler
can try to satisfy the incoming request or the calling middleware can defer the HttpContext
to the next middleware:
type HttpHandler = HttpContext -> HttpContext option
By making the HttpContext
optional the function can now either return Some HttpContext
or None
.
Additionally the HttpHandler
shouldn't block on IO operations or other long running tasks and therefore return the HttpContext option
wrapped in an asynchronous workflow:
type HttpHandler = HttpContext -> Async<HttpContext option>
This is slowly taking shape, but there's still something missing.
In ASP.NET Core MVC controller dependencies are automatically resolved during instantiation. This is very useful, because in ASP.NET Core dependencies are registered in a central place inside the ConfigureServices
method of the Startup.cs
class file.
Automatic dependency resolution is not really a thing in functional programming, because dependencies are normally functions and not objects. Functions can be passed around or partially applied which usually makes object oriented dependency management obsolete.
However, because most ASP.NET Core dependencies are registered as objects inside an IServiceCollection
container, a HttpHandler
can resolve these dependencies through an IServiceProvider
object.
This is done by wrapping the original HttpContext
and an IServiceProvider
object inside a new type called HttpHandlerContext
:
type HttpHandlerContext =
{
HttpContext : HttpContext
Services : IServiceProvider
}
By changing the HttpHandler
function definition we can take advantage of this new type:
type HttpHandler = HttpHandlerContext -> Async<HttpHandlerContext option>
With that one should be able to build pretty much any web application of desire. If you have worked with Suave in the past then this should look extremely familiar as well.
Combining smaller HttpHandlers to bigger applications
In principal there's nothing you cannot do with an HttpHandler
, but it wouldn't be very practical to build a whole web application in one function. The beauty of functional programming is the composition of many smaller functions to one bigger application.
The simplest combinator would be a bind function which takes two HttpHandler
functions and combines them to one:
let bind (handler : HttpHandler) (handler2 : HttpHandler) =
fun (ctx : HttpHandlerContext) ->
async {
let! result = handler ctx
match result with
| None -> return None
| Some ctx2 -> return Some ctx2
}
As you can see the bind
function takes two different HttpHandler
functions and a HttpHandlerContext
. First it evaluates the first handler and checks its result. If the result was None
then it will stop at this point and return None
as the final result. If the result was Some HttpHandlerContext
then it will take the resulting context and use it to evaluate the second HttpHandler
. Whatever the second HttpHandler
returns will be the final result in this case.
This pattern is often referred to as railway oriented programming. If you are interested to learn more about it then please check out Scott Wlaschin's slides and video on his website or this lengthy blog post on that topic.
One more thing that should be considered in the bind
function is to check if a HttpResponse
has been already written by the first HttpHandler
before invoking the second HttpHandler
. This is required to prevent a potential exception when a HttpHandler
tries to make changes to the HttpResponse
after another HttpHandler
has already written to the response. In this case the bind
function should not invoke the second HttpHandler
:
let bind (handler : HttpHandler) (handler2 : HttpHandler) =
fun (ctx : HttpHandlerContext) ->
async {
let! result = handler ctx
match result with
| None -> return None
| Some ctx2 ->
match ctx2.HttpContext.Response.HasStarted with
| true -> return Some ctx2
| false -> return! handler2 ctx2
}
To round this up we can bind the bind
function to the >>=
operator:
let (>>=) = bind
With the bind
function we can combine unlimited HttpHandler
functions to one.
The flow would look something like this:
Another very useful combinator which can be borrowed from Suave is the choose
function. The choose
function let's you define a list of multiple HttpHandler
functions which will be iterated one by one until the first HttpHandler
returns Some HttpHandlerContext
:
let rec choose (handlers : HttpHandler list) =
fun (ctx : HttpHandlerContext) ->
async {
match handlers with
| [] -> return None
| handler :: tail ->
let! result = handler ctx
match result with
| Some c -> return Some c
| None -> return! choose tail ctx
}
In order to better see the usefulness of this combinator it's best to look at an actual example.
Let's define a few simple HttpHandler
functions first:
let httpVerb (verb : string) =
fun (ctx : HttpHandlerContext) ->
if ctx.HttpContext.Request.Method.Equals verb
then Some ctx
else None
|> async.Return
let GET = httpVerb "GET" : HttpHandler
let POST = httpVerb "POST" : HttpHandler
let route (path : string) =
fun (ctx : HttpHandlerContext) ->
if ctx.HttpContext.Request.Path.ToString().Equals path
then Some ctx
else None
|> async.Return
let setBody (bytes : byte array) =
fun (ctx : HttpHandlerContext) ->
async {
ctx.HttpContext.Response.Headers.["Content-Length"] <- new StringValues(bytes.Length.ToString())
ctx.HttpContext.Response.Body.WriteAsync(bytes, 0, bytes.Length)
|> Async.AwaitTask
|> ignore
return Some ctx
}
let setBodyAsString (str : string) =
Encoding.UTF8.GetBytes str
|> setBody
This already gives a good illustration of how easily one can create different HttpHandler
functions to do various things. For instance the httpVerb
handler checks if the incoming request matches a given HTTP verb. If yes it will proceed with the next HttpHandler
, otherwise it will return None
. The two functions GET
and POST
re-purpose httpVerb
to specifically check for a GET or POST request.
The route
function compares the request path with a given string and either proceeds or returns None
again. Both, setBody
and setBodyAsString
write a given payload to the response of the HttpContext
. This will trigger a response being made back to the client.
Each HttpHandler
is kept very short and has a single responsibility. Through the bind
and choose
combinators we can combine many HttpHandler
functions into one larger web application:
let webApp =
choose [
GET >>=
choose [
route "/" >>= setBodyAsString "Index"
route "/ping" >>= setBodyAsString "pong"
]
POST >>=
choose [
route "/submit" >>= setBodyAsString "Submitted!"
route "/upload" >>= setBodyAsString "Uploaded!"
]
]
Even though I've barely written any code this functional framework already proves to be quite powerful.
At last I need to create a new middleware which can run this functional web application:
type HttpHandlerMiddleware (next : RequestDelegate,
handler : HttpHandler,
services : IServiceProvider) =
do if isNull next then raise (ArgumentNullException("next"))
member __.Invoke (ctx : HttpContext) =
async {
let httpHandlerContext =
{
HttpContext = ctx
Services = services
}
let! result = handler httpHandlerContext
if (result.IsNone) then
return!
next.Invoke ctx
|> Async.AwaitTask
} |> Async.StartAsTask
And finally hook it up in Startup.cs
:
type Startup() =
member __.Configure (app : IApplicationBuilder) =
app.UseMiddleware<LambdaMiddleware>(webApp) |> ignore
This web framework shows what I love about F# so much. With very little code I was able quickly write a basic web application from the ground up. It looks very much like an ASP.NET Core clone of Suave, but with the difference that it fully embraces the ASP.NET Core architecture and its HttpContext
.
Functional ASP.NET Core framework
By extending the above example with a few more useful HttpHandler
functions to return JSON, XML, HTML or even templated (DotLiquid) views someone could create a very powerful ASP.NET Core functional web framework, which could be easily seen as an MVC replacement for F# developers.
This is exactly what I did and I named it ASP.NET Core Lambda, because I simply couldn't think of a more descriptive or "cooler" name. It is a functional ASP.NET Core micro framework and I've built it primarily for my own use. It is still very early days and in alpha testing, but I already use it for two of my private projects and it works like a charm.
How does it compare to other .NET web frameworks
Now you might ask yourself how does this compare to other .NET web frameworks and particularly to Suave (since I've borrowed a lot of ideas from Suave and from Scott Wlaschin's blog)?
I think this table explains it very well:
Paradigm | Language | Hosting | Frameworks | |
---|---|---|---|---|
MVC | Object oriented | C# | ASP.NET (Core) only | Full .NET, .NET Core |
NancyFx | Object oriented | C# | Self-hosted or ASP.NET (Core) | Full .NET, .NET Core, Mono |
Suave | Functional | F# | Primarily self-hosted | Full .NET, .NET Core, Mono |
Lambda | Functional | F# | ASP.NET Core only | .NET Core |
MVC and NancyFx are both heavily object-oriented frameworks mainly targeting a C# audience. MVC probably the most feature rich framework and NancyFx with its super-duper-happy-path are probably the most wide spread .NET web frameworks. NancyFx is also very popular with .NET developers who want to run a .NET web application self-hosted on Linux (this was a big selling point pre .NET Core times).
Suave was the first functional web framework built for F# developers. It is a completely independent standalone product which was designed to be cross platform compatible (via Mono) and self-hosted. A large part of the Suave library is compounded of its own HTTP server and Socket server implementation. Unlike NancyFx it is primarily meant to be self-hosted, which is why the separation between web server and web framework is not as clean cut as in NancyFx (e.g. the Suave NuGet library contains everything in one while NancyFx has separate packages for different hosting options).
ASP.NET Core Lambda is the smallest of all frameworks (and is meant to stay this way). It is also a functional web framework built for F# developers, but cannot exist outside of the ASP.NET Core platform. It has been tightly built around ASP.NET Core to leverage its features as much as possible. As a result it is currently the only (native) functional web framework which is a first class citizen in ASP.NET Core.
I think it has its own little niche market where it doesn't really compete with any of the other web frameworks. It is basically aimed at F# developers who want to use a ASP.NET Core in a functional way.
While all these web frameworks share some similarities they still have their own appliances and target a different set of developers.
Watch this space for more updates on ASP.NET Core Lambda in the future and feel free to try it out and let me know how it goes! So far I've been very happy with the results and it has become my goto framework for new web development projects.