Creating a Functional Wrapper in F#
Justin Hewlett
Posted on July 27, 2020
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
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
}
For now, we'll just support GET
s and DELETE
s, 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
}
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> = ...
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
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 = []
}
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 = []
}
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") ] }
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 }
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")
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
}
}
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
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
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!
-
IHttpClientFactory
is fairly new and basically acts as a pool forHttpClient
s. You need to use the built-in DI container to get an instance of it. ↩ -
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. ↩
-
Take a look at Saturn and Farmer for two good examples of using computation expressions to create DSLs. ↩
-
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. ↩
Posted on July 27, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.