Easily Use Flurl Testable HttpClient

Flurl is a modern, portable testable fluent based Httpclient library for .net. It is open sourced for commercial usage also. It supports a wide variety of platforms like .net, .net core, Xamarine, and UWP.

There are many ways you can call a URL and get a response in .net, some of them are,

  1. Using .Net very own HttpClient
  2. Using RestSharp
  3. Using Flurl

In this article, we will see how these three libraries can be used by comparing the code.

First let's see HttpCleint and RestSharps RestClient and then dive deep into flurl.

I have created a sample code to compare the usage by calling a simple open weather API to get weather of Bengaluru. The full code is available here.

Note
If the sample does not work, Then API Key needs to be replaced if it is blocked already.

HttpClient

private const string BaseUrl = "https://community-open-weather-map.p.rapidapi.com";
private const string ResourcePath = "/forecast/daily?q=bengaluru%2CIN&lat=35&lon=139&cnt=10&units=metric%20or%20imperial";
private const string XRapidapiHost = "X-RapidAPI-Host";
private const string XRapidapiKey = "X-RapidAPI-Key";
private const string XRapidapiHostValue = "community-open-weather-map.p.rapidapi.com";
var client = new HttpClient();
var request = new HttpRequestMessage {
    Method = HttpMethod.Get,
        RequestUri = new Uri(BaseUrl + ResourcePath),
        Headers = {
            {
                XRapidapiHost,
                XRapidapiHostValue
            },
            {
                XRapidapiKey,
                XRapidapiKeyValue
            },
        },
};
using(var response = client.Send(request)) {
    response.EnsureSuccessStatusCode();
    var body = response.Content.ReadAsStringAsync().Result;
    Console.WriteLine(body);
}

As one can see here calling a simple URL with headers needs the above complex code. Also complex wrappers need to be created to test the client. But still, if you do not want external libraries dependency in your code and have very few URL calls to be made then this would be a better choice.

Another popular client is portable, asynchronous, supports serialization and deserialization and also supports various authentication methods.

private const string BaseUrl = "https://community-open-weather-map.p.rapidapi.com";
private const string ResourcePath =
    "/forecast/daily?q=bengaluru%2CIN&lat=35&lon=139&cnt=10&units=metric%20or%20imperial";
private const string XRapidapiHost = "X-RapidAPI-Host";
private const string XRapidapiKey = "X-RapidAPI-Key";
private const string XRapidapiHostValue = "community-open-weather-map.p.rapidapi.com";
var client = new RestClient(BaseUrl);
var request = new RestRequest(ResourcePath);
request.AddHeader(XRapidapiHost, XRapidapiHostValue);
request.AddHeader(XRapidapiKey, XRapidapiKeyValue);
var response = client.ExecuteGetAsync<string>(request).Result;
Console.WriteLine();
Console.Write(response.Content);

As you can see the calls are greatly simplified and deserialization of the results is also supported. To test this restclient you need to wrap it and pass into your code or use the MockHttpMessageHandler.

Short form for Fluent Url, Flurl is a modern, fluent, asynchronous, testable, portable, URL builder and HTTP client library for .NET. Flurl is available on NuGet and is free for commercial use. It runs on a wide variety of platforms, including .NET Framework, .NET Core, Xamarin, and UWP

private const string BaseUrl = "https://community-open-weather-map.p.rapidapi.com";
private const string ResourcePath =
    "/forecast/daily?q=bengaluru%2CIN&lat=35&lon=139&cnt=10&units=metric%20or%20imperial";
private const string XRapidapiHost = "X-RapidAPI-Host";
private const string XRapidapiKey = "X-RapidAPI-Key";
private const string XRapidapiHostValue = "community-open-weather-map.p.rapidapi.com";
Console.WriteLine((BaseUrl + ResourcePath).WithHeader(XRapidapiHost, XRapidapiHostValue)
    .WithHeader(XRapidapiKey, XRapidapiKeyValue).GetStringAsync().Result);

As you can see all the code is simplified to one line of code from the sample to call the weather API and print the results. It supports fluent format for configuring the request and also supports deserializing the response. Hence the URL call is reduced to just one line.

Often URL calls are reduced to one line as below

var poco = await "http://api.foo.com".GetJsonAsync<T>();
string text = await "http://site.com/readme.txt".GetStringAsync();
byte[] bytes = await "http://site.com/image.jpg".GetBytesAsync();
Stream stream = await "http://site.com/music.mp3".GetStreamAsync();

Testability

Flurl.Http provides a set of testing features that make isolated arrange-act-assert style testing dead simple.

private HttpTest _httpTest;
[SetUp]
public void CreateHttpTest() {
    _httpTest = new HttpTest();
}
[TearDown]
public void DisposeHttpTest() {
    _httpTest.Dispose();
}
[Test]
public void Test_Some_Http_Calling_Method() {
    // Flurl is in test mode
}
httpTest
    .RespondWith("some response body")
    .RespondWithJson(someObject)
    .RespondWith("error!", 500);
sut.DoThingThatMakesSeveralHttpCalls();

Behind the scenes, each RespondWith* adds a fake response to a thread-safe queue.

httpTest
    .ForCallsTo("*.api.com*", "*.test-api.com*") // multiple allowed, wildcard supported
    .WithVerb("put", "PATCH") // or HttpMethod.Put, HttpMethod.Patch
    .WithQueryParam("x", "a*") // value optional, wildcard supported
    .WithQueryParams(new { y = 2, z = 3 })
    .WithAnyQueryParam("a", "b", "c")
    .WithoutQueryParam("d")
    .WithHeader("h1", "f*o") // value optional, wildcard supported
    .WithoutHeader("h2")
    .WithRequestBody("*something*") // wildcard supported
    .WithRequestJson(new { a = "*", b = "hi" }) // wildcard supported in sting values
    .With(call => true) // check anything on the FlurlCall
    .Without(call => false) // check anything on the FlurlCall
    .RespondWith("all conditions met!", 200);

Once a Test is created and any specific responses are queued, simply call into a test subject. When the SUB makes an HTTP call with Flurl, the real call is effectively blocked and the next fake response is dequeued and returned instead. However, when only one response remains in the queue (matching any later criteria, if provided), that response becomes "sticky", i.e. it is not dequeued and hence gets returned in all subsequent calls. There is no need to mock or stub any Flurl objects in order for this to work. HttpTest uses the logical asynchronous call context to How a signal through the SUB and notify Flurl to fake the call.

It also supports assertions.

httpTest.ShouldHaveCalled("http://some-api.com/*")
    .WithQueryParam("x", "1*")
    .WithVerb(HttpMethod.Post)
    .WithContentType("application/json")
    .WithoutHeader("my-header-*")
    .WithRequestBody("{\"a\":*,\"b\":*}")
    .Times(3);

Conclusion

Flurl is a modern fluent based HTTP client which is easily testable, more importantly, it is free to use commercially and open source. This makes it a very attractive candidate for modern .net programming.