Scott Hanselman

Using Flurl to easily build URLs and make testable HttpClient calls in .NET

June 23, 2018 Comment on this post [9] Posted in ASP.NET | DotNetCore | Open Source
Sponsored By

FlurlI posted about using Refit along with ASP.NET Core 2.1's HttpClientFactory earlier this week. Several times when exploring this space (both on Twitter, googling around, and in my own blog comments) I come upon Flurl as in, "Fluent URL."

Not only is that a killer name for an open source project, Flurl is very active, very complete, and very interesting. By the way, take a look at the https://flurl.dev/ site for a great example of a good home page for a well-run open source library. Clear, crisp, unambiguous, with links on how to Get It, Learn It, and Contribute. Not to mention extensive docs. Kudos!

Flurl is a modern, fluent, asynchronous, testable, portable, buzzword-laden URL builder and HTTP client library for .NET.

You had me at buzzword-laden! Flurl embraces the .NET Standard and works on .NET Framework, .NET Core, Xamarin, and UWP - so, everywhere.

To use just the Url Builder by installing Flurl. For the kitchen sink (recommended) you'll install Flurl.Http. In fact, Todd Menier was kind enough to share what a Flurl implementation of my SimpleCastClient would look like! Just to refresh you, my podcast site uses the SimpleCast podcast hosting API as its back-end.

My super basic typed implementation that "has a" HttpClient looks like this. To be clear this sample is WITHOUT FLURL.

public class SimpleCastClient
{
private HttpClient _client;
private ILogger<SimpleCastClient> _logger;
private readonly string _apiKey;

public SimpleCastClient(HttpClient client, ILogger<SimpleCastClient> logger, IConfiguration config)
{
_client = client;
_client.BaseAddress = new Uri($"https://api.simplecast.com"); //Could also be set in Startup.cs
_logger = logger;
_apiKey = config["SimpleCastAPIKey"];
}

public async Task<List<Show>> GetShows()
{
try
{
var episodesUrl = new Uri($"/v1/podcasts/shownum/episodes.json?api_key={_apiKey}", UriKind.Relative);
_logger.LogWarning($"HttpClient: Loading {episodesUrl}");
var res = await _client.GetAsync(episodesUrl);
res.EnsureSuccessStatusCode();
return await res.Content.ReadAsAsync<List<Show>>();
}
catch (HttpRequestException ex)
{
_logger.LogError($"An error occurred connecting to SimpleCast API {ex.ToString()}");
throw;
}
}
}

Let's explore Tim's expression of the same client using the Flurl library!

Not we set up a client in Startup.cs, use the same configuration, and also put in some nice aspect-oriented events for logging the befores and afters. This is VERY nice and you'll note it pulls my cluttered logging code right out of the client!

// Do this in Startup. All calls to SimpleCast will use the same HttpClient instance.
FlurlHttp.ConfigureClient(Configuration["SimpleCastServiceUri"], cli => cli
.Configure(settings =>
{
// keeps logging & error handling out of SimpleCastClient
settings.BeforeCall = call => logger.LogWarning($"Calling {call.Request.RequestUri}");
settings.OnError = call => logger.LogError($"Call to SimpleCast failed: {call.Exception}");
})
// adds default headers to send with every call
.WithHeaders(new
{
Accept = "application/json",
User_Agent = "MyCustomUserAgent" // Flurl will convert that underscore to a hyphen
}));

Again, this set up code lives in Startup.cs and is a one-time thing. The Headers, User Agent all are dealt with once there and in a one-line chained "fluent" manner.

Here's the new SimpleCastClient with Flurl.

using Flurl;
using Flurl.Http;

public class SimpleCastClient
{
// look ma, no client!
private readonly string _baseUrl;
private readonly string _apiKey;

public SimpleCastClient(IConfiguration config)
{
_baseUrl = config["SimpleCastServiceUri"];
_apiKey = config["SimpleCastAPIKey"];
}

public Task<List<Show>> GetShows()
{
return _baseUrl
.AppendPathSegment("v1/podcasts/shownum/episodes.json")
.SetQueryParam("api_key", _apiKey)
.GetJsonAsync<List<Show>>();
}
}

See in GetShows() how we're also using the Url Builder fluent extensions in the Flurl library. See that _baseUrl is actually a string? We all know that we're supposed to use System.Uri but it's such a hassle. Flurl adds extension methods to strings so that you can seamlessly transition from the strings (that we all use) representations of Urls/Uris and build up a Query String, and in this case, a GET that returns JSON.

Very clean!

Flurl also prides itself on making HttpClient testing easier as well. Here's a more sophisticated example of a library from their site:

// Flurl will use 1 HttpClient instance per host
var person = await "https://api.com"
.AppendPathSegment("person")
.SetQueryParams(new { a = 1, b = 2 })
.WithOAuthBearerToken("my_oauth_token")
.PostJsonAsync(new
{
first_name = "Claire",
last_name = "Underwood"
})
.ReceiveJson<Person>();

This example is doing a post with an anonymous object that will automatically turn into JSON when it hits the wire. It also receives JSON as the response. Even the query params are created with a C# POCO (Plain Old CLR Object) and turned into name=value strings automatically.

Here's a test Flurl-style!

// fake & record all http calls in the test subject
using (var httpTest = new HttpTest()) {
// arrange
httpTest.RespondWith(200, "OK");
// act
await sut.CreatePersonAsync();
// assert
httpTest.ShouldHaveCalled("https://api.com/*")
.WithVerb(HttpMethod.Post)
.WithContentType("application/json");
}

Flurl.Http includes a set of features to easily fake and record HTTP activity. You can make a whole series of assertions about your APIs:

httpTest.ShouldHaveCalled("http://some-api.com/*")
.WithVerb(HttpMethd.Post)
.WithContentType("application/json")
.WithRequestBody("{\"a\":*,\"b\":*}") // supports wildcards
.Times(1)

All in all, it's an impressive set of tools that I hope you explore and consider for your toolbox! There's a ton of great open source like this with .NET Core and I'm thrilled to do a small part to spread the word. You should to!


Sponsor: Check out dotMemory Unit, a free unit testing framework for fighting all kinds of memory issues in your code. Extend your unit testing with the functionality of a memory profiler.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Hosting By
Hosted in an Azure App Service
June 25, 2018 12:47
Hello,

this is sure a very helpful tool. If you need to get down and dirty on testing all aspects of something you want to develop.
BUT: this is similar to the users that first install Windows "System Optimizers - Fix 100.000 registry erros with one click" and two days later install "TheTOOL: Fix any broken Windows installation".
First we get taken down to the level where we need to send HTTP VERBS, bulid URLs, analyze HTTP response codes and parse unstructured JSON and then we get the super cool tool that makes this job SOOOO much easier.
What about using standard protocols like OData, SOAP, COM+ or even application frameworks?
Productivity matters. If you start your transport business with understanding how exactly spark plugs work (which is amazingly complicated by the way) you might never ever reach your business goals. Kids: focus on stuff that delivers business value.
June 25, 2018 16:03
Beats having to create these helper methods yourself and makes for very readable code.
June 26, 2018 1:53
Scott,

A nice as the example is saving 2 lines of code for 1 call is not worth it. Create a method with a try/catch wrapper around the body of GetShows() results in 1) simpler code and, importantly, 2) no reliance of a third party library and the increased risk of bugs, breaks, lack of support and end of life of the third party library.

Use 20+ open source packages in a large project and you are most likely to have one or more of those open source libraries broken or breaking something in your large project every day of the year.

Little luck with finding someone experienced in one of the obsolete open source packages 3 years after a large solution goes into production.

June 26, 2018 23:33
I've used Flurl on a couple of minor projects, and it is indeed a really nice tool. It makes it supe-easy to test, and the fluent API it provides gives you some nice code.

The only thing I don't like about it, is that you're hiding a dependency. For small private projects that doesn't matter much, but for larger projects involving more people I would definitely go for some other way of abstracting your HTTP requests.
June 27, 2018 2:18
I think my RestClient is pretty fluent too, your example using DalSoft.RestClient:



using (var restClient = new RestClient(_baseUrl))
{
return restClient.v1.Podcasts.ShowNum
.Query(new { api_key = _apiKey })
.Get("episodes.json");
}


Works with .NET Standard 2.0, uses delegate handlers in a pipeline, so easily extendable. Has a UnitTestHandler built-in making these types of tests trivial, in fact it was one of the user cases for creating the project.

Working on IHttpClientFactory support, will be finished really soon.

Single maintainer since 2015 looking for some love too :)
June 29, 2018 13:48
I don't see it handling singleton HttpClient DNS issue. Is this still something we need to worry about?
June 29, 2018 16:12
I have to say that while I like the fluent syntax, using string extension methods to make an HTTP call seems truly awful. I would much prefer to have something similar to:

new FlurlClient("https://api.com").AppendPathSegment("person")...
July 03, 2018 21:18
@Paul,

The string extensions are purely optional. They're syntactic sugar for:

new Url("https://api.com").AppendPathSegment("person")...

You can also do almost exactly what you suggested:

new FlurlClient("https://api.com").Request("person")...

In this case you're on the hook for reusing the FlurlClient if you want to reuse the underlying HttpClient, as their lifespans are tightly coupled. Starting with a URL will look up a cached instance (by host (by default)). Although in .NET Core 2.1 reusing HttpClient is a lot less important.
July 06, 2018 7:37
@Zbigniew, it does indeed handle that DNS issue too: https://github.com/tmenier/Flurl/issues/222

We were using straight HttpClient, until we started having a mix of .NET Framework code and .NET Core code and all these HttpClient gotchas kept coming up requiring new patterns and settings. That made it very clear we needed something to abstract all that, and Flurl has fit that need incredibly and gratefully well :)

Comments are closed.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.