ASP .NET Core -  Usando HttpClientFactory


 Hoje vamos tratar da classe HttpClientFactory introduzida a partir da versão 2.1 da .NET Core.

A classe HttpClient foi introduzida a partir do .Net Framework 4.5 e, é a maneira mais popular de consumir uma Web API no seu código do lado do servidor. Mas ela apresenta alguns problemas como:

  1. Embora essa classe seja Disposable, usá-la com a instrução using não é a melhor opção porque, mesmo quando você descarta o objeto HttpClient, o soquete subjacente não é liberado imediatamente e pode causar um problema sério chamado 'esgotamento de soquetes'. (Veja o meu artigo tratando disso aqui)

    Portanto, HttpClient deve ser instanciada uma única vez e reutilizada durante a vida útil de um aplicativo. A criação de uma instância de uma classe HttpClient para cada solicitação esgotará o número de soquetes disponíveis em condições de carga pesada. Esse problema resultará em erros de SocketException.
     
  2. Um segundo problema com o HttpClient pode ocorrer quando ele for usado como um objeto singleton ou estático. Nesse caso, um HttpClient singleton ou estático não respeita as alterações de DNS, conforme é explicado aqui : repositório do GitHub do .NET Core.

Para resolver esses problemas mencionados e facilitar o gerenciamento das instâncias do HttpClient, a partir do .NET Core 2.1 foi introduzido um novo HttpClientFactory que também pode ser usado para implementar chamadas HTTP resilientes pela integração do Polly a ele.

O que é o HttpClientFactory

O HttpClientFactory foi projetado para:

Nota: Polly é uma biblioteca de resiliência e manipulação de falhas transientes que permite aos desenvolvedores expressarem políticas como Retry, Circuit Breaker, Timeout, Isolamento de Bulkhead e Fallback de uma maneira fluente e segura para thread. A partir da versão 6.0.1, Polly segmenta o .NET Standard 1.1 e 2.0+. (Acesse o link para saber mais)

Podemos usar HttpClientFacory das seguintes formas:

  1. Usar o HttpClientFactory diretamente
  2. Usar clientes nomeados
  3. Usar clientes tipados
  4. Usar clientes gerados(*)

Vejamos um resumo de como aplicar os 3 primeiros tipos de clientes.

1- Usando o HttpClientFactory diretamente

O IHttpClientFactory pode ser registrado chamando o método de extensão AddHttpClient em IServiceCollection, dentro do método Startup.ConfigureServices.

 

  services.AddHttpClient();

 

Depois de registrado, o código pode aceitar um IHttpClientFactory em qualquer lugar em que os serviços possam ser injetados com injeção de dependência. O IHttpClientFactory pode ser usado para criar uma instância de HttpClient.

Abaixo temos um exemplo usando uma Razor Page:

public class BasicUsageModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubBranch> Branches { get; private set; }
    public bool GetBranchesError { get; private set; }

    public BasicUsageModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "https://api.github.com/repos/aspnet/AspNetCore.Docs/branches");

        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = _clientFactory.CreateClient();

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            Branches = await response.Content
                .ReadAsAsync<IEnumerable<GitHubBranch>>();
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty<GitHubBranch>();
        }                              
    }
}

Neste código injetamos uma instância de IHttClientFactory no construtor e usamos essa instância para criar um HttpClient para a seguir enviar um request.

Usar o IHttpClientFactory dessa forma é uma ótima maneira de refatorar um aplicativo existente.

Não há nenhum impacto na maneira como o HttpClient é usado. Nos locais em que as instâncias de HttpClient são atualmente criadas, substitua essas ocorrências por uma chamada a CreateClient.

2- Usando o HttpClientFactory com clientes nomeados

O código anterior permite definir o HttpClient no momento do uso. No entanto, a configuração do cliente, como um URL de terminal de API, pode ser definida em um local na classe Startup ou classe Program.

O cliente nomeado pode então ser solicitado via injeção de dependência, tornando o código mais reutilizável. Isso é ótimo se você tiver muitos usos distintos do HttpClient ou se tiver muitos clientes com configurações diferentes.

O código abaixo mostra como nomeamos e definimos um HttpClient dentro da classe Program e na classe Startup no método ConfigureServices:

   ...

  builder.Services.AddHttpClient("Reservas", httpClient=>
  {
    httpClient.BaseAddress = new Uri("https://localhost:7211/");
  });

 

    public void ConfigureServices(IServiceCollection services)
    {
         AddHttpClient("Reservas", httpClient=>
        {
           httpClient.BaseAddress = new Uri("https://localhost:7211/");
         });
    }

Para consumir o cliente definido basta injetar uma instância de IHttpClientFactory no construtor usar esta instância e o com o método CreateClient criar uma instância do cliente nomeado :

public class ReservasController : Controller
{

     private readonly JsonSerializerOptions _options;
     private readonly IHttpClientFactory _clientFactory;
     private const string apiUrl = "/api/reservas/";
    
     public ReservasController(IHttpClientFactory httpClientFactory)
     {
          _clientFactory = httpClientFactory;
          _options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
     }

     ....

    public async Task<IActionResult> Index()
    {
        var client = _clientFactory.CreateClient("Reservas");
        var response = await client.GetAsync(apiUrl);

        if (response.IsSuccessStatusCode)
        {
            var apiResponse = await response.Content.ReadAsStreamAsync();
            listaReservas = await JsonSerializer.DeserializeAsync<IEnumerable<Reserva>>(apiResponse, _options);
        }
        return View(listaReservas);
    }

 

3- Usando o HttpClientFactory com clientes tipados

Um "cliente tipado" é apenas um HttpClient pré-configurado para algum uso específico. Essa configuração pode incluir valores específicos, como o servidor base, cabeçalhos HTTP ou tempo limite.

Usando esta abordagem implementamos uma entidade separada ou arquivo de classe por domínio de API. Assim, todas as implementações de comunicação Http são registradas em uma classe específica por domínio de API. Nesta abordagem, cada entidade ou classe será injetada com o objeto 'System.Net.Http.HttpClient' pelo 'HttpClienFactory'. Portanto não usaremos 'HttpClientFactory' diretamente como fizemos para as demais técnicas.

Dessa forma para consumir uma API de terceiros precisamos criar uma classe específica para ela, se tivermos um 'n' número de APIs Rest para consumir em nosso projeto, vamos precisar criar 'n' classes para implementar a lógica específica do HttpClientFactory para cada domínio de API a consumir.

Como exemplo podemos inclusive definir uma interface e fazer sua implementação e a seguir registrar esta interface no container DI:

a- Define a interface e a classe do cliente tipado

public interface IReservasClient
{
    Task<Reserva> GetReservaById(int id);
}
public class ReservasClient : IReservasClient
{
   public HttpClient _client;
   private const string apiUrl = "/api/reservas/";
   public ReservasClient(HttpClient client)
   {
      _client = client;
       _client.BaseAddress = new Uri("https://localhost:7211/");
      _client.Timeout = new TimeSpan(0, 0, 50);
   }

  public async Task<Reserva> GetReservaById(int id)
  {
     var response = await _client.GetFromJsonAsync<Reserva>(apiUrl + id);
     return response;
  }
}

b- Registra no container DI (classe Program)


builder.Services.AddHttpClient<IReservasClient, ReservasClient>();
 

c- Exemplo de uso

    [HttpPost]
    public async Task<IActionResult> GetReserva(int id)
    {
        var response = await _reservasClient.GetReservaById(id);
        return View(response);
    }

Aguarde outros artigos tratando deste assunto em breve.

"E, quanto fizerdes por palavras ou por obras, fazei tudo em nome do Senhor Jesus, dando por ele graças a Deus Pai."
Colossenses 3:17

Referências:


José Carlos Macoratti