DEV Community

Justin Hewlett
Justin Hewlett

Posted on

Creating a Functional Wrapper in F#

One of the big selling points of F# is the wealth of .NET libraries available to you, both in the Base Class Library and NuGet.

Many of these libraries, however, are developed primarily with C# in mind. They can usually be consumed without issue, but I often find it helpful to write a little wrapper around them to provide a more idiomatic, functional interface. (Plus, it never hurts to build a facade around a third-party dependency!)

Let's walk through how we might build a wrapper around HttpClient from the Microsoft.Extensions.Http package.

To start off, here's how we could use HttpClient to make a GET request:

let (statusCode, body) =
    async {
        use httpClient = new HttpClient()

        httpClient.Timeout <- TimeSpan.FromSeconds 2.

        use requestMessage =
            new HttpRequestMessage(
                HttpMethod.Get,
                "https://hacker-news.firebaseio.com/v0/item/8863.json"
            )

        requestMessage.Headers.Add("Accept", "application/json")

        use! response = httpClient.SendAsync(requestMessage) |> Async.AwaitTask
        let! body = response.Content.ReadAsStringAsync() |> Async.AwaitTask

        return (response.StatusCode, body)
    }
    |> Async.RunSynchronously
Enter fullscreen mode Exit fullscreen mode

This isn't awful, but I think we can do better. It'd be nice to encapsulate some of these details and make the code a bit more declarative.

Let's start by defining some types for the interface that we want:

type HttpMethod =
| Get
| Delete

type Request = {
    Url : string
    Method : HttpMethod
    Timeout : TimeSpan option
    Headers : (string * string) list
}
Enter fullscreen mode Exit fullscreen mode

For now, we'll just support GETs and DELETEs, but it will be easy to add support for other methods as we need to. We'll require the user to specify a Url and Method, but Timeout and Headers are optional.

For our response, we can just capture the status code and body for now:

type Response = {
    StatusCode : int
    Body : string
}
Enter fullscreen mode Exit fullscreen mode

Now that we have our types, let's think about what our function signature might look like:

module Http =
    let execute (httpClientFactory : IHttpClientFactory) (request : Request) : Async<Response> = ...
Enter fullscreen mode Exit fullscreen mode

We'll take in an instance of IHttpClientFactory 1 and Request, and return an Async<Response>.

Let's see how ergonomic it would be to use at this point:

let request =
    {
        Url = "https://hacker-news.firebaseio.com/v0/item/8863.json"
        Method = Get
        Timeout = Some (TimeSpan.FromSeconds 2.)
        Headers = [ ("Accept", "application/json") ]
    }

let response =
    request
    |> Http.execute httpClientFactory
    |> Async.RunSynchronously
Enter fullscreen mode Exit fullscreen mode

Not too bad. One thing to note is that even if we don't want to provide a Timeout or Headers, we still have to set the properties:

let request =
    {
        Url = "https://hacker-news.firebaseio.com/v0/item/8863.json"
        Method = Get
        Timeout = None
        Headers = []
    }
Enter fullscreen mode Exit fullscreen mode

Let's create a helper function to make a request with some default values:

module Http =
    let createRequest url method =
        {
            Url = url
            Method = method
            Timeout = None
            Headers = []
        }
Enter fullscreen mode Exit fullscreen mode

Now we have:

let request = Http.createRequest "https://hacker-news.firebaseio.com/v0/item/8863.json" Get
let requestWithTimeout = { request with Timeout = Some (TimeSpan.FromSeconds 2.) }
let requestWithTimeoutAndHeaders = { requestWithTimeout with Headers = [ ("Accept", "application/json") ] }
Enter fullscreen mode Exit fullscreen mode

Since records are immutable, we use with to create a new instance with some additional properties set.

This is more tedious than just creating the Request ourselves and setting all the properties. Let's create some more helper functions for adding a timeout and headers:

[<AutoOpen>]
module Request =
    let withTimeout timeout request =
        { request with Timeout = Some timeout }

    let withHeader header request =
        { request with Headers = header :: request.Headers }
Enter fullscreen mode Exit fullscreen mode

Notice that request comes in as the last parameter. This is important to enable pipelining:

let request =
    Http.createRequest "https://hacker-news.firebaseio.com/v0/item/8863.json" Get
    |> withTimeout (TimeSpan.FromSeconds 2.)
    |> withHeader ("Accept", "application/json")
Enter fullscreen mode Exit fullscreen mode

Pretty slick, right? We've created a little domain-specific language (DSL) to describe the different options, and we can pick and choose what we need.

From Overloads to DSLs

In C# land, you might use overloads, optional parameters, and mutable properties to capture the different configuration options. If you layer on dot chaining and some well-designed methods, you can create a fluent builder.

In F#, there's a strong focus on creating DSLs, using features like records, the pipeline operator, discriminated unions2, and computation expressions3. We've created a builder here, not too different than something like LINQ, though it uses function pipelines instead of dot chaining.

Implementing 'execute'

Now that we've talked about the interesting bits, we can see what the implementation of our execute function might look like:

module HttpMethod =
    let value method =
        match method with
        | Get -> System.Net.Http.HttpMethod.Get
        | Delete -> System.Net.Http.HttpMethod.Delete

module Http =
    let execute (httpClientFactory : IHttpClientFactory) (request : Request) : Async<Response> =
        async {
            use httpClient = httpClientFactory.CreateClient()

            request.Timeout
            |> Option.iter (fun t -> httpClient.Timeout <- t)

            use requestMessage = new HttpRequestMessage(request.Method |> HttpMethod.value, request.Url)

            request.Headers
            |> List.iter requestMessage.Headers.Add

            use! response = httpClient.SendAsync(requestMessage) |> Async.AwaitTask
            let! body = response.Content.ReadAsStringAsync() |> Async.AwaitTask

            return
                {
                    StatusCode = int response.StatusCode
                    Body = body
                }
        }
Enter fullscreen mode Exit fullscreen mode

Not too much different from our initial example. We did add a HttpMethod.value function to convert between our representation and System.Net.Http.HttpMethod, and we optionally set the timeout via Option.iter which only runs the callback if we have a value.

Modules vs. Objects

Where possible, I tends to use modules and functions over objects. For dependencies that would typically be passed into a class constructor, we just pass to the function directly (like IHttpClientFactory in our example). By positioning them at the beginning of the parameter list, you'll be able to use partial application if you want to.

Here's what that might look like with execute:

let makeRequest = Http.execute httpClientFactory    //only apply the first parameter

let request =
    Http.createRequest "https://hacker-news.firebaseio.com/v0/item/8863.json" Get
    |> withTimeout (TimeSpan.FromSeconds 2.)
    |> withHeader ("Accept", "application/json")

request
|> makeRequest    //pipe in the final parameter, 'request'
|> Async.RunSynchronously
Enter fullscreen mode Exit fullscreen mode

We can even do the same thing with createRequest if we want:

let resource = Http.createRequest "https://hacker-news.firebaseio.com/v0/item/8863.json"

let get = resource Get
let delete = resource Delete
Enter fullscreen mode Exit fullscreen mode

Perhaps this is overkill, but these are the kinds of things I think about when deciding how to order the parameters — from more general to more specific; dependencies first, data second.

Conclusion

We were able to create a nice wrapper around HttpClient that will play nicely with the rest of our code. It was no accident that we spent a good chunk of time upfront thinking about the types and the different interactions that we want our interface to support. That's the tricky part to get right; the implementation usually ends up just doing a bit of translation and delegation to the underlying library. Note that you don't have to expose the entire library — indeed, it's often easier on consumers if you just expose the stuff you actually use.

Here's a gist that shows the full example, including some additional code for handling other HTTP methods, request bodies, and dealing with query strings4. Enjoy!

Thanks to Isaac Abraham and Brett Rowberry for reviewing the draft of this post!


  1. IHttpClientFactory is fairly new and basically acts as a pool for HttpClients. You need to use the built-in DI container to get an instance of it. 

  2. You can do overloading and optional parameters in F#, too, if you use methods rather than functions. I tend to avoid them because they don't play very nicely with type inference, and discriminated unions are a great alternative

  3. Take a look at Saturn and Farmer for two good examples of using computation expressions to create DSLs. 

  4. I have successfully used this in production, but the wrapper is by no means exhaustive. Take a look at FsHttp for a more fully-featured library. 

Top comments (4)

Collapse
 
arnondanon profile image
ArnonDanon

Great article, trying to wrap my had around F# this days and articles like that, real world scenarios like wrapping third party dlls or my own for example are great examples how to it would look like working on real F# project, i do wonder though after walking through the gist as well, where is the error handling staff?

Collapse
 
jhewlett profile image
Justin Hewlett

Thanks for the reply, glad you liked it. The error handling is no different than it would be in C# here: callers would need to be prepared to handle HttpRequestException (or an AggregateException with an inner HttpRequestException due to the Task) for network errors, and TaskCanceledException if you're using timeouts.

In my own code I often catch and convert those exceptions to Results, but that's somewhat opinionated and felt beyond the scope of this post.

Also, non-200 status codes will not throw and can be handled through pattern matching

Collapse
 
arnondanon profile image
ArnonDanon

Thanks for finding the time to reply,
much appriciated