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. ↩
Top comments (3)
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?
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 anAggregateException
with an innerHttpRequestException
due to theTask
) for network errors, andTaskCanceledException
if you're using timeouts.In my own code I often catch and convert those exceptions to
Result
s, 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
Thanks for finding the time to reply,
much appriciated