15 April 2015

Nancy with F#, some helpers for routes

Nancy is such a great framework for making HTTP services on the .NET platform, especially if you are using C#. Nancy depends on a lot of the C# trappings like dynamic variables, inheritance, and so forth. However, it's quite unpalatable when used from F#. There isn't a dynamic type in F#. The common alternative is defining my own ? operator to access members parameters. Also defining routes is just plain weird from F#, and only gets weirder when you try to do asynchronous routes. The translation from an async C# lambda is not a smooth one. There's also having to box everything because F# will not do automatic upcasting to System.Object.

I think this is why it seems like there are a good handful of Nancy frameworks for F#. Most of them do more than just provide more sugary syntax for defining routes, however, which is all I really wanted for the time being.

So here are my helper functions for making routes, especially async routes, a bit more palatable from F#.

    let private addRouteSync path f (router:NancyModule.RouteBuilder) =
        router.[path] <-
            fun (parameters:obj) ->
                f (parameters :?> DynamicDictionary) |> box

    // async is the default
    let private addRoute path f (router:NancyModule.RouteBuilder) =
        router.[path, true] <-
            fun (parameters:obj) cancellationToken ->
                async { // unwrap and box the result
                    let! result = f (parameters :?> DynamicDictionary) cancellationToken
                    return box result
                } |> Async.StartAsTask

    // more f# friendly methods
    type NancyModule with
        member me.post path f = me.Post |> addRoute path (f me)
        member me.get path f = me.Get |> addRoute path (f me)
        member me.put path f = me.Put |> addRoute path (f me)
        member me.delete path f = me.Delete |> addRoute path (f me)
        member me.patch path f = me.Patch |> addRoute path (f me)
        member me.options path f = me.Options |> addRoute path (f me)

        member me.postSync path f = me.Post |> addRouteSync path (f me)
        member me.getSync path f = me.Get |> addRouteSync path (f me)
        member me.putSync path f = me.Put |> addRouteSync path (f me)
        member me.deleteSync path f = me.Delete |> addRouteSync path (f me)
        member me.patchSync path f = me.Patch |> addRouteSync path (f me)
        member me.optionsSync path f = me.Options |> addRouteSync path (f me)

Here is a dumb example of usage, asynchronous:

        m.put "/orders/{id:string}"
            <| fun nmodule parameters cancelToken ->
                let id = parameters.["id"].ToString()
                async { return id } // echo id

Now the function you setup receives the parameters as a DynamicDictionary instead of obj. You can also return whatever you want (e.g. Response), and these helpers will box it for you before providing it back to Nancy. Your function can also directly return an async, and these helpers will convert it to a task type which Nancy expects. I'm also passing in the NancyModule in case your code hangs off an F# module (essentially static code) instead of the Nancy module class itself.

I am basically only use the NancyModule as an entry point (like a static main void) and try to remain functionally-styled with my real code.

No comments: