O HttpClient do .NET é uma das classes mais usadas em projetos que consomem algum tipo de web service ou API externa. Com ele é possível fazer chamadas HTTP totalmente assíncronas e facilmente ler strings de serviços. Desde sua chegada ele se tornou a forma oficial de fazer requisições HTTP, porém, se você já usou bibliotecas de outras linguagens, pode concordar que sua legibilidade cai à medida que você precisa montar requests mais complexos. Para ajudar nesses cenários é que existe o projeto Flurl.

Flurl

O Flurl é um trocadilho com Fluent URL, ele se descreve como “um builder de URL moderno, fluent, assíncrono, testável, portável, cheio-de-buzzords e uma biblioteca com um client HTTP para .NET”. Eu diria que é uma descrição bem precisa, mas para ficar mais mais claro, vamos dividir suas funcionalidades em 3 partes: URL Builder, Client HTTP e Testes HTTP.

URL Builder

A parte mais “fácil” do que o Flurl oferece é a estrutura de URL Builder. Com ela você pode construir a URL de chamada de maneira fluent e de forma que tudo fique mais legível. Seus métodos ficam todos no pacote Flurl.

Mas qual é o problema com a construção de URLs que temos hoje no .NET? O problema é que basicamente precisamos cuidar de tudo, o HttpClient não tem nenhum tipo de tratamento para concatenar parâmetros, fazer append de segmentos (/usuarios, /empresas, etc.), escapar strings, etc.

Por exemplo, imagine o seguinte cenário: na sua API, filtros são opcionais, e passados por query string, você deve ser capaz de compor a URL de forma que ela contenha apenas o parâmetros escolhidos pelo usuário. Então essa chamada pode se tornar algo assim:

http://www.some-api.com/usuarios?name=Maria&max_results=20&page=2

ou simplesmente assim:

http://www.some-api.com/usuarios?max_results=20

O HttpClient não te ajuda a compor essa URL, então você deve acabar com um código assim:

if (maxResults > 0)
{
	if (string.IsNullOrWhiteSpace(queryParams))
		queryParams += $"?max_results={maxResults}";
	else
		queryParams += $"&max_results={maxResults}";
}

São coisas chatas que precisamos nos preocupar: eu concateno com ? ou com &? Mas será que já tem uma / na URL ou eu devo adicionar? São coisas que não tem nada a ver com nosso sistema, mas que temos que nos preocupar, e o pior, esse código se repetiria para cada parâmetro opcional. Então como o Flurl nos ajuda nesse cenário ou em outros mais complexos?

O Flurl tem uma série de Extension Methods para strings. Com isso você pode compôr sua URL de maneira muito mais natural, como no exemplo abaixo:

var url = "http://www.some-api.com"
    .AppendPathSegment("endpoint")
    .SetQueryParams(new {
        api_key = ConfigurationManager.AppSettings["SomeApiKey"],
        max_results = 20,
        q = "Don't worry, I'll get encoded!"
    })
    .SetFragment("after-hash");

Ele se preocupa com os detalhes de &, ? ou /, o que importa é que no final você terá uma URL corretamente construída e com qualquer tipo de caractere especial escapado para que ela seja um URL válida. O exemplo acima gera essa URL:

http://www.some-api.com/endpoint?api_key=1234&max_results=20&q=Don%27t%20worry%2C%20I%27ll%20get%20encoded%21#after-hash

E se você quiser parar por aqui, ou seja, utilizar apenas o URL Builder do Flurl, é só chamar os métodos .ToString() ou .ToUri() e passar a URL para o HttpClient, como você já faria normalmente. Tudo vai funcionar e nada na chamada HTTP em si vai mudar.

Client HTTP

Se você quiser dar um passo a mais e “substituir” o HttpClient, você pode utilizar o pacote Flurl.Http para ter métodos helpers para construir não só a URL, mas também o corpo dos requests. Assim como o URL Builder, a ideia desse pacote é ter uma API mais fluent e mais legível para construir o corpo dos requests HTTP. No final das contas, o Flurl continua usando o HttpClient para fazer as chamadas, ele só está lá para te auxiliar na escrita e construção dos requests.

O exemplo mais clássico de qualquer API é ler JSON, hoje o HttpClient te ajuda a ler strings, mas não necessariamente JSONs, para ajudar nisso, o Flurl já vem com um helper para ler JSONs e deserializá-los em objetos:

var usuario = await "http://api.foo.com".GetJsonAsync<Usuario>();

Outro caso comum é fazer POST de JSONs, no HttpClient o boilerplate é muito grande:

await client.PostAsync("https://api.foo.com", new StringContent(json, Encoding.UTF8, "application/json"));

São muitos parâmetros padrões que se repetem: encoding, “application/json”, etc. Além disso, eu já vi várias pessoas quebrando a cabeça porque um POST não estava funcionando justamente por falta de algum desses parâmetros. Para facilitar uma escrita tão comum, o Flurl.Http tem helpers que tornam a chamada acima simples assim:

await url.PostJsonAsync(objetoUsuario);

Repare que você passa diretamente o objeto e ele automaticamente faz a serialização para você.

O Flurl.Http também te dá helpers para adicionar Cookies, Headers e outros parâmetros. Se você preferir você também pode pedir um Client e fazer as chamadas a partir dele, inclusive respeitando as diretrizes para HttpClient do .NET. Tudo pode ser customizado e configurado por você, desde qual serializador deve ser usado até quais status HTTP devem ser tratados como exceptions.

Testes HTTP

Por último, outra facilidade proporcionada pelo Flurl é a escrita de testes de unidade HTTP.

Testar classes que fazem chamadas HTTP pode ser um ponto de dúvida, afinal, eu devo acessar os serviços externos nos testes? Mas se eu não for acessar, como faço para impedir que isso aconteça? E como eu crio um Mock do HttpClient? São muitas complicações a serem respondidas.

Começando pela parte filosófica: se você quer testar seu código por unidade, você não deve acessar os serviços externos, assim você garante que seus testes não dependem de um serviço totalmente externo ao seu código. Imagine se seus testes começassem a falhar porque o servidor da API de fotos que você está usando está fora do ar. Você não vai querer isso. Mas o que vale a pena ser testado, então?

Eu gosto de testar pelo menos dois fluxos:

  • Estou fazendo o request para URL correta, com todos os headers, parâmetros e objetos necessários?
  • Sei tratar o retorno do API de acordo com a especificação do corpo do response?

Para testar esses fluxos, eu preciso de uma maneira de verificar o Request sendo feito, e forçar um Response do meu client. Normalmente acabo usando Mocks para isso, mas existe outro problema: a classe HttpClient não foi feita para ser mockada, ela não tem interfaces nem métodos virtuais.

Esse é outro benefício de utilizar Flurl.Http, com ele é possível testar suas chamadas HTTP e simular respostas diretos no client. A classe HttpTest fornecida pelo Flurl é inteligente o suficiente para saber olhar para os requests feitos pelo Flurl e fazer tudo que você precisa. Para utilizá-la, basta instanciá-la no seu teste:

[Test]
public void Test_Some_Http_Calling_Method() {
    using (var httpTest = new HttpTest()) {
        // Flurl está em modo de Testes
        sut.CallThingThatUsesFlurlHttp(); // Chamadas HTTP são fake
    }
}

Feito isso, toda a estrutura de testes estará posicionada para que você a configure. Para simular uma resposta você configura tudo diretamente no HttpTest:

httpTest
    .RespondWith("some response body")
    .RespondWithJson(someObject)
    .RespondWith(500, "error!");

var response = await sut.GetUsuarios();

Imaginando que GetUsuarios() é o método que utiliza o Flurl.Http para bater numa API externa, você terá no response exatamente o que foi configurado: uma resposta com erro 500 e o JSON escolhido.

Além disso, é possível fazer Asserts de maneira fluent em cima das chamadas feitas:

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

Assim, se o seu código não estiver de acordo com alguma das expectativas configuradas, o seu teste falhará com uma mensagem de erro mostrando todas as expectativas não atendidas.

Infelizmente, por padrão, o HttpTest só funciona com o Flurl.Http, ou seja, você precisa fazer as chamadas (GET, POST, DELETE, etc.) todas pelo client dele, e não mais pelo HttpClient. É possível “enganar” o HttpTest do Flurl para que ele funcione com o HttpClient, mas isso ficará para outro post.

Conclusão

O Flurl é um pacote bem poderoso, mas relativamente simples, com ele você evita a escrita de códigos boilerplate que provavelmente são copiados de projeto para projeto, tendo uma série de helpers bem pensados e bem testados, para que você não tenha que perder tempo com isso.

Além disso, você pode escolher até onde quer ir com o Flurl: apenas URL Builder, ou HTTP Client também? O bônus de ter uma infra para testes HTTP de graça na biblioteca é um ponto bastante positivo para que você comece a fazer chamadas HTTP utilizando o Flurl por completo, assim você tem um código mais legível, mais simples de escrever e ainda testável.

Teste o Flurl no seu projeto e conte nos comentários o que achou.