Alguns problemas não são evidentes
Construir um software que funcione isolado sem realizar integrações com outros sistemas é um fato muito raro. Praticamente todo software realiza integrações com outros componentes, sejam eles internos ou externos. Atualmente, a forma mais popular para promover a integração entre sistemas é o protocolo HTTP, e para que softwares em .NET possam se comunicar através deste protocolo, conhecer a classe HttpClient é obrigatório para o desenvolvimento adequado.
O uso indevido do HttpClient pode gerar exaustão de recursos (principalmente de sockets), pois mesmo que a classe implemente a interface IDisposable, no caso específico do HttpClient, a liberação de recursos pelo método Dispose não acontece para os sockets de conexão TCP. Tal característica é bem documentada pela Microsoft, contudo muitos desenvolvedores acabam realizando implementações como o exemplo abaixo:
public async Task<IEnumerable<SpaceflightNewDTO>> GetUsingAntiPattern() { using var httpClient = new HttpClient(); var response = await httpClient.GetAsync("https://api.spaceflightnewsapi.net/v3/articles"); var json = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject<IEnumerable<SpaceflightNewDTO>>(json); }
Embora a utilização do using seja reconhecidamente uma boa prática em casos de implementação de IDisposable, o HttpClient foi planejado para ser utilizado de forma compartilhada, aproveitando ao máximo as conexões já criadas (sockets), mantendo-as em aberto mesmo que o objeto HttpClient seja liberado pela instrução using, sendo um potencial motivo da máquina sofrer com Socket Starvation.
Para mitigar este problema, podemos declarar uma instância de HttpClient como static, ou configurar o contêiner de injeção de dependência para criar a instância como singleton. Essa abordagem garante que teremos uma instância apenas de HttpClient, deixando o gerenciamento de sockets como responsabilidade desta única instância. Contudo, embora seja uma implementação válida, essa não é a melhor alternativa, pois o client não funcionará da forma esperada em caso de alteração do DNS.
O que fazer para solucionar o problema?
No .NET Core 2.1 foi introduzida a classe DefaultHttpClientFactory para construção de instâncias de HttpClient. Essa implementação obedece o contrato IHttpClientFactory e pode ser integrado com o pacote Microsoft.Extensions.DependencyInjection.
Podemos utilizar o IHttpClientFactory de 4 formas distintas, sendo elas:
- Basic usage: o IHttpClientFactory é injetado diretamente no construtor, onde é utilizado o método CreateClient para obter uma instância HttpClient;
- Named Clients: instâncias HttpClient são nomeadas e definidas na configuração do contêiner de injeção de dependências, sendo criadas através de seus alias pelo método CreateClient do IHttpClientFactory;
- Generated Clients: faz uso do IHttpClientFactory em combinação com bibliotecas de terceiros;
- Typed Clients: essa abordagem traz consigo o benefício de poder injetar diretamente o HttpClient na classe desejada.
Dentre as abordagens citadas, recomendamos a abordagem de typed clients, pois, conforme mencionado acima, ela possibilita a injeção direta do HttpClient, além de prover os mesmos recursos da Named Clients, sem a necessidade de utilizar strings como chaves, possibilitando o encapsulamento da lógica de consumo dos endpoints em serviços isolados. Entretanto, vale ressaltar que essa abordagem depende da integração com o DI da Microsoft, através do método de extensão AddHttpClient. Basicamente, o seu funcionamento consiste em deixar o contêiner de DI realizar a criação do HttpClient e injetar a instância dentro de uma classe de serviço.
Na prática, precisamos apenas de uma classe que receberá, por injeção de dependência, uma instância HttpClient, como o código abaixo demonstra:
public sealed class CorrectPatternHttpService : ICorrectPatternHttpService { private readonly HttpClient _httpClient; public CorrectPatternHttpService(HttpClient httpClient) { _httpClient = httpClient; } public async Task<IEnumerable<SpaceflightNewDTO>> GetUsingCorrectPatternAsync() { var response = await _httpClient.GetAsync("https://api.spaceflightnewsapi.net/v3/articles"); var serializer = new JsonSerializer(); using var stream = await response.Content.ReadAsStreamAsync(); using var streamReader = new StreamReader(stream); using var jsonTextReader = new JsonTextReader(streamReader); return serializer.Deserialize<IEnumerable<SpaceflightNewDTO>>(jsonTextReader); } }
Por fim, o código abaixo, demonstra a configuração dentro do contêiner de DI que permite que o IHttpClientFactory faça o trabalho de gerenciar as instâncias de HttpClient:
internal static IServiceCollection AddHttpClients(this IServiceCollection services) { services.AddHttpClient<ICorrectPatternHttpService, CorrectPatternHttpService>(); return services; }
Conclusão
A utilização equivocada do HttpClient para realizar integrações entre sistemas via HTTP, pode causar diferentes tipos de problemas e trazer enormes dores de cabeça. Portanto, conhecer a melhor abordagem para a realização de requisições HTTP não é negociável, mas sim imprescindível nos dias de hoje.