β

Using Flurl to easily build URLs and make testable

Scott Hanselman's Blog 38 阅读

Flurl I 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.io/ 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.



© 2018 Scott Hanselman. All rights reserved.
作者:Scott Hanselman's Blog
Scott Hanselman on Programming, User Experience, The Zen of Computers and Life in General
原文地址:Using Flurl to easily build URLs and make testable, 感谢原作者分享。

发表评论