Nancy is my web framework of choice for .NET. It is free, lightweight, easy to use, and comes without all the bloat of the more common .NET frameworks.
Nancy makes routing in C# simple. If you need to handle a form post, you simply set up the POST endpoint, and access the form variables via the dynamic Form
object. For example if you’ve got a form with firstName
and lastName
, and need to respond with a “Hello”, here’s now that would be done.
1 2 3 4 5 6 7 |
// This code would go in the constructor of your NancyModule: Post["/submit"] = _ => { dynamic form = this.Request.Form; string firstName = form.firstName; string lastName = form.lastName; return "Hello " + firstName + " " + lastName; }; |
When using from F# however, there is no “dynamic” functionality, so it’s not so simple. The common advice is to create an operator that mimics the dynamic functionality of C# as follows.
1 2 3 |
let (?) (parameters : obj) param = let dic = parameters :?> DynamicDictionary dic.[param] |
Then you can use that operator like this.
1 2 3 4 5 |
self.Post.["/submit"] <- fun _ -> let form = self.Request.Form :?> Nancy.DynamicDictionary let firstName = (form?firstName).ToString() let lastName = (form?lastName).ToString() box <| "Hello " + firstName + " " + lastName |
Okay, well, that works, but it’s not pretty. All that casting to and from String
pollutes the code, and the ?
operator just looks out-of-place. It can get especially unwieldy in a long module with many endpoints, since all of that code must go into the constructor of your NancyModule
, and ends up being one big long block of mess. Externalizing these handlers into separate functions is an option, but then you’re passing dynamic variables around into the heart of your app, which is very antithetical to F#.
Fortunately there’s a better way. First off, use JSON encoding on your HTML form rather than URL encoding. This will mean a few extra lines of Javascript on your form, but this is standard with frameworks like AngularJS anyway. Now you can create a helper function using the excellent JSON.NET library to parse the JSON into a record. There’s also a function here to convert the result back to JSON:
1 2 3 4 5 6 7 8 9 |
let parseBody<'a>(m: NancyModule) = use rdr = new StreamReader(m.Request.Body) let s = rdr.ReadToEnd() let result = JsonConvert.DeserializeObject<'a> s result let toResp o = if obj.ReferenceEquals(o, null) then box Response.NoBody else box <| JsonConvert.SerializeObject o |
Now within your NancyModule
constructor, you can add the following inline function that maps an endpoint to a handler function.
1 2 |
let map endpoint f = self.Post.[endpoint] <- fun _ -> toResp(f(parseBody self)) |
What does this do? It sets up your endpoint to parse the body into a record, call your handler function f
with that record, and return the response. So now, assuming we have a record type Person = {firstName: string; lastName: string}
, we can write the handler as:
1 2 |
map "/submit" (fun user -> "Hello " + user.firstName + " " + user.lastName) |
What’s especially nice is this can now be externalized to a function, so if we define let sayHi user = "Hello " + user.firstName + " " + user.lastName
then our form handler becomes simply
1 |
map "/submit" sayHi |
All in all, this pattern makes API creation so simple and declarative, I find it much better than even the C# dynamic pattern. Your handlers go from being line-per-variable messes, to a simple line-per-endpoint mapping. You can put your handlers and datatypes in different files and namespaces too, to organize things even better. The full code is here, along with an async option as well. You can see how much easier this makes the API endpoint mapping to read, and how well componentized your code becomes, especially as the list of endpoints continues to grow.
DataTypes.fs:
1 2 3 4 |
module DataTypes type Person = { firstName: string lastName: string } |
Handlers.fs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
module Handlers open DataTypes let sayHi user = "Hello " + user.firstName + " " + user.lastName let sayBye user = "Goodbye " + user.firstName + " " + user.lastName let sayHiAsync user = async { do! Async.Sleep 100 return "Hello " + user.firstName + " " + user.lastName } let sayByeAsync user = async { do! Async.Sleep 100 return "Goodbye " + user.firstName + " " + user.lastName } |
NancyHelp.fs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
module NancyHelp open Nancy open Nancy.Responses open Newtonsoft.Json open System.IO let parseBody<'a>(m: NancyModule) = use rdr = new StreamReader(m.Request.Body) let s = rdr.ReadToEnd() let result = JsonConvert.DeserializeObject<'a> s result let toResp o = if obj.ReferenceEquals(o, null) then box Response.NoBody else box <| JsonConvert.SerializeObject o let getAsync nancy f = let req = parseBody nancy async { let! resp = f req return toResp resp } let map (m:NancyModule) endpoint f = m.Post.[endpoint] <- fun _ -> toResp <| f(parseBody m) let mapAsync (m:NancyModule) endpoint f = m.Post.[endpoint, true] <- fun _ _ -> getAsync m f |> Async.StartAsTask |
API.fs:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
module API open Nancy open Nancy.Responses type LinusModule() as self = inherit NancyModule() let map = NancyHelp.map self let map_async = NancyHelp.mapAsync self do map "/login" Handlers.sayHi map "/logout" Handlers.sayBye map_async "/login-async" Handlers.sayHiAsync map_async "/logout-async" Handlers.sayByeAsync |