ASP. NET Core - Usando HttpClientFactory - I


 Hoje vamos recordar como usar o recurso HttpClientFactory em aplicações ASP .NET Core.

Eu já apresentei a classe HttpClientFactory neste artigo - Usando HttpClientFactory - e hoje veremos como podemos usar este recurso em aplicações ASP .NET Core no ambiente do .NET 6.

Para mostrar isso na prática vamos usar uma aplicação ASP .NET Core Web API chamada ApiReservas que já esta pronta e que eu mostro como criar neste artigo : ASP .NET Core -  Criando uma Web API - I

Os endpoints expostos por esta API são:

HTTP URL Dados do cliente Retorno
GET /api/reservas Sem dados Todas as resrevas no formato JSON
GET /api/reservas/1, /api/reservas/2, etc Sem dados A reserva do id que foi enviado como parâmetro
POST /api/reservas O objeto reserva no formato JSON A nova reserva criada no formato JSON
PUT /api/reservas O objeto reserva no formato JSON A nova reserva atualizada no formato JSON
DELETE /api/reservas/1, /api/reservas/2, etc Sem dados Nada
PATCH /api/reservas/1, /api/reservas/2, etc Um JSON que contém um conjunto de alterações a serem aplicadas A confirmação que a alteração foi aplicada.

Seguindo o roteiro mostrado no artigo eu recriei o projeto usando o .NET 6 no VS 2022 onde defini o nome da Solution UsandoHttpClientFactory. Este será o nosso ponto de partida.

A seguir vamos criar uma aplicação ASP .NET Core MVC e consumir esta API usando os recursos de HttpClientFactory.

Para criar a aplicação MVC inclua na solução um novo projeto usando o template ASP .NET Core Web App (Model-View-Controller) e informe o nome ReservasWeb.

Vamos configurar a nossa Solution para iniciar os dois projetos quando for executada e assim teremos a API em atendimento e a aplicação MVC pronta para consumir a API:

Temos assim uma Solução contendo o projeto Web API que esta pronta e o projeto MVC que iremos usar para consumir a API usando o HttpClientFactory.

Consumindo a API com HttpClientFactory

Vamos agora preparar a aplicação ASP .NET Core MVC para consumir os endpoints da  Web API ApiReservas.

Vamos criar na pasta Models a classe Reserva:

public class Reserva
{
    public int ReservaId { get; set; }
    public string? Nome { get; set; }
    public string? InicioLocacao { get; set; }
    public string? FimLocacao { get; set; }
}

Vamos incluir no projeto o pacote Nuget Microsoft.Extensions.Http usando o comando Install-Package Microsoft.Extensions.Http na janela do Package Manager Console.

A seguir vamos adicionar o serviço de IHttpClientFactory no contêiner DI nativo incluindo na classe Program o código a seguir:

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

Com isso estamos definido o que é conhecido como named HttpClient ou cliente nomeado, onde já definimos a propriedade BaseAddress para acessar a URL onde a API ApiReservas esta em atendimento.

A seguir vamos criar na pasta Controllers o controlador ReservasController e vamos injetar uma instância de IHttpClientFactory no construtor :

public class ReservasController : Controller
{
    private readonly IHttpClientFactory _clientFactory;
    private readonly JsonSerializerOptions _options;
 
    public ReservasController(IHttpClientFactory httpClientFactory)
    {
        _clientFactory = httpClientFactory;
        _options = new JsonSerializerOptions {
PropertyNameCaseInsensitive = true };
    }

   ....
}

   

Definimos também a propriedade PropertyNameCaseInsensitive como true para indicar que o nome das propriedades usa uma comparação que não é case sensitive durante a serialização.

Precisamos definir também o endpoint para acessar a API pois temos apenas o endereço base; para a nossa API os endpoints são /api/reservas/ :

public class ReservasController : Controller
{
    private readonly IHttpClientFactory _clientFactory;
    private readonly JsonSerializerOptions _options;

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

   ....
}

Vamos criar duas propriedades listaReservas e reserva para tratar com um objeto Reserva e com uma lista de objetos Reserva:

public class ReservasController : Controller
{
    private readonly IHttpClientFactory _clientFactory;
    private readonly JsonSerializerOptions _options;

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

    public IEnumerable<Reserva>? listaReservas { get; set; }
    public Reserva? reserva { get; set; }

   ....
}

Agora podemos iniciar a criação dos métodos Action do Controlador começando com o método Index que vai retornar uma lista de reservas existentes.

Para isso vamos incluir o código abaixo no controlador ReservasController:

    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);
    }

Neste código criamos um método Action Index assíncrono (async) e que retorna um Task<IActionResult>, onde usamos um named HttpClient - Reservas -  para obter uma instância e realizar uma requisição assíncrona usando await, e o método GetAsync() usando a Url da API. A resposta será armazenada na variável response.

Como a resposta esta no formato JSON, para poder desserializar facilmente o JSON para uma lista de objetos Reserva, usamos a biblioteca System.Text.Json e o código:

            var apiResponse = await response.Content.ReadAsStreamAsync();
            listaReservas = await JsonSerializer.DeserializeAsync<IEnumerable<Reserva>>(apiResponse, _options)

A lista de objetos reserva armazenados na variável "listaReservas" serão retornados à view Index como um model.

Ajustando o arquivo _Layout.cshtml

Vamos ajustar o arquivo _Layout.cshtml para exibir um link para acessar as reservas. Para isso altere o código deste arquivo incluindo as linhas de código em azul, conforme mostrado a seguir:

...

  <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
      <ul class="navbar-nav flex-grow-1">
      <li class="nav-item">
       <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
       </li>

         <li class="nav-item">
           <a class="nav-link text-dark" asp-area="" asp-controller="
Reservas" asp-action="Index">Reservas</a>
         </li>

    
  </ul>
  </div>
...

Criando o arquivo de leiaute das views

Agora temos que criar a view Index na pasta Views/Reservas para exibir a lista de reservas existente.

Antes disso vamos criar uma partial view chamada _reservaLayout.cshtml na pasta /Views/Shared onde vamos definir o leiaute das todas as views usando o código abaixo neste arquivo:

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    <link asp-href-include="lib/bootstrap/dist/css/*.min.css" rel="stylesheet" />
</head>
<body>
    <div class="container-fluid">
        @RenderBody()
    </div>
</body>
</html>

Agora todas as views do nosso controlador ReservasController vão usar esse leiaute.

Clique com o botão direto do mouse no método Action Index e a seguir em Add View;

Na janela Add New Scaffolded Item selecione Razor View e clique em Add;

A seguir informe os dados conforme mostra a figura:

Note que estamos definindo como página de leiaute o nosso arquivo de leiaute: _reservaLayout.cshtml.

Criando a view Index : Exibindo uma lista de reservas

A seguir inclua o código no arquivo Index.cshtml gerado na pasta /Views/Reservas :

@model IEnumerable<Reserva>
@{
    ViewData["Title"] = "Todas as Reservas";
    Layout = "~/Views/Shared/_reservaLayout.cshtml";
}

<a asp-action="AddReserva" class="btn btn-sm btn-primary">Incluir Reserva</a>
<a asp-action="GetReserva" class="btn btn-sm btn-secondary">Obter Reserva</a>

<table class="table table-sm table-striped table-bordered m-2">
    <thead><tr>
        <th>ID</th><th>Nome</th><th>Início</th><th>Fim</th><th>Atualiza</th><th>Deleta</th>
    </tr></thead>
    <tbody>
        @foreach (var r in Model)
        {
            <tr>
                <td>@r.ReservaId</td>
                <td>@r.Nome</td>
                <td>@r.InicioLocacao</td>
                <td>@r.FimLocacao</td>
                <td>
                    <a asp-action="UpdateReserva" asp-route-id="@r.ReservaId"><img src="/icon/edit.png" /></a>
                </td>
                <td>
                    <form asp-action="DeleteReserva" method="post">
                        <input type="hidden" value="@r.ReservaId" name="ReservaId" />
                        <input type="image" src="/icon/close.png" />
                    </form>

                </td>
            </tr>
        }
    </tbody>
</table>

Este código usa os recursos do Bootstrap para definir a view que mostra a lista de reservas. A view é fortemente tipada recebendo um tipo IEnumerable<Reserva> como seu model e usar o laço foreach para popular a tabela com os dados.

Executando o projeto MVC (lembre-se que o projeto API deve estar em execução) iremos obter o seguinte resultado:

Acessando o link Reservas estaremos enviando um request usando o HttpClientFactory para consumir a API e o resultado obtido será a lista de reservas conforme mostra a figura a seguir:?

Obtendo uma reserva pelo Id usando um Typed Client

Para obter a lista de reservas definimos um método Action e usamos o HttpClientFactory conhecido como named client.  Vamos agora retornar uma reserva pelo seu Id e vamos usar o HttpClientFactory conhecido como typed client.

Para isso vamos criar uma pasta Reservas no projeto e criar a interface IReservasClient e sua implementação na classe ReservasClient:

1- IReservasClient

public interface IReservasClient
{
    Task<Reserva> GetReservaById(int id);
}

2- ReservasClient

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;
    }
}

Observe que no código da classe já definimos o cliente HttpClient e definimos o endereço base e o método que vai retornar a reserva pelo seu Id usando a instância do HttpClient e o método GetFromJsonAsync.

A seguir vamos registrar o serviço para esta interface na classe Program:

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

...

A seguir para implementar a funcionalidade de obter uma reserva pelo seu Id no controlador ReservasController criando os métodos Action GetReserva para o GET e a seguir para o POST.

Antes precisamos injetar a interface IReservasClient do cliente tipado que definimos no construtor do Controlador:

public class ReservasController : Controller
{
    private readonly IHttpClientFactory _clientFactory;
    private const string apiUrl = "/api/reservas/";
    private readonly JsonSerializerOptions _options;

    private readonly IReservasClient _reservasClient;
    public ReservasController(IHttpClientFactory httpClientFactory,
        IReservasClient reservasClient)
    {
        _clientFactory = httpClientFactory;
        _reservasClient = reservasClient;
        _options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
    }
    public IEnumerable<Reserva>? listaReservas { get; set; }
    public Reserva? reserva { get; set; }
   ...
}

A versão GET da Action apenas retorna a View.

Já a versão POST faz uma requisição para a API para obter a reserva passando o Id da reserva usando a instância do cliente tipado _reservasClient que foi injetado no controlador:

   public ViewResult GetReserva() => View();

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

Com isso encapsulamos na classe ReservasClient do cliente tipado as funcionalidades necessárias para consumir a API.

Vamos então criar a view ‘GetReserva’  na pasta ‘Views/Reservas’ com o código abaixo:

@model Reserva

@{
    ViewData["Title"] = "GetReserva";
    Layout = "~/Views/Shared/_reservaLayout.cshtml";
}

<h2>Obter reserva pelo Id <a asp-action="Index" class="btn btn-sm btn-success">Retornar</a></h2>

<form method="post">
    <div class="form-group">
        <label for="id">Id:</label>
        <input class="form-control" name="id" />
    </div>
    <div class="text-center panel-body">
        <button type="submit" class="btn btn-sm btn-primary">Obter Reserva</button>
    </div>
</form>

@if (Model != null)
{
    <h2>Reserva</h2>
    <table class="table table-sm table-striped table-bordered m-2">
        <thead><tr><th>ID</th><th>Nome</th><th>Início</th><th>Fim</th></tr></thead>
        <tbody>
            <tr>
                <td>@Model.ReservaId</td>
                <td>@Model.Nome</td>
                <td>@Model.InicioLocacao</td>
                <td>@Model.FimLocacao</td>
            </tr>
        </tbody>

    </table>
}

Esta view temos um formulário onde podemos informar o Id da reserva e clicar no botão para enviar uma requisição para a web api.

A expressão @if (Model != null) verifica se o model não é null e exibe os dados obtidos da API.

Executando o projeto novamente e clicando em Obter Reserva teremos o seguinte resultado:

No próximo artigo vamos continuar consumindo a API criando os demais métodos Action.

"Ele(Jesus) respondeu: "Cuidado para não serem enganados. Pois muitos virão em meu nome, dizendo: ‘Sou eu! ’ e ‘o tempo está próximo’. Não os sigam."
Lucas 21:8

 

Referências:


José Carlos Macoratti