.NET

16 nov, 2018

ASP.Net Core 2.1: Http Client + Refit

Publicidade

Simplesmente pelo prazer de codificar, muitas vezes caímos no tipico cenário de reinventar a roda. Quantas vezes você já ouviu: “Meu amigo, isso aí já tem pronto, não precisa reinventar a roda.”? Muitas, tenho certeza.

Essa frase tem suas controvérsias. Para fins didáticos acredito muito na reinvenção da roda. Não há nada melhor para aprender do que reinventar a roda. Faça seu mini-framework de desenvolvimento, invente seu próprio event bus, crie seu próprio logger, aprenda a customizar as coisas para entender como elas funcionam! Porém, utilize essa abordagem somente para fins didáticos.

Asp.Net Core 2.1: Http Client

Tenho plena certeza de que você já sabe de cor como fazer um typed client utilizando http client. Provavelmente você já fez tanto isso na sua vida e também implementar um client com uma mão só sem olhar. Principalmente quando estamos falando de um client simples. Já pensou se tivesse um cara que criasse CRUD automaticamente? Tem! Refit.

PM > Install-Package refit -version 4.6.48

Veja como é fácil configurar o Refit. Não é necessário se preocupar com implementação – basta configurar uma abstração utilizando os annotations do Refit.

public interface IExtremelyDifficultCrudApi<TAnyType, in TAnyKey> where TAnyType : class
{
    [Get("")]
    Task<ApiResponse<List<TAnyType>>> GetAll();

    [Get("/{key}")]
    Task<ApiResponse<TAnyType>> GetById(TAnyKey key);

    [Post("")]
    Task<ApiResponse<TAnyType>> Create([Body] TAnyType payload);

    [Put("/{key}")]
    Task<ApiResponse<TAnyType>> Update(TAnyKey key, [Body] TAnyType payload);

    [Delete("/{key}")]
    Task Delete(TAnyKey key);
}

A interface IExtremelyDifficultCrudApi<TAnyType, in TAnyKey> recebe como parâmetro o tipo do objeto e o tipo da chave primária do objeto. Note que é uma interface bem tranquila, somente com as cinco operações padrões, porém, caso queira injetar operações mais avançadas, basta torná-las genéricas e inseri-las na interface.

Já no setup final, para poder injetar o client em qualquer classe e utilizar, basta registrá-lo como um serviço no container no startup do projeto utilizando os métodos AddHttpClient e AddTypedClient.

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new Info { Title = "Product API", Version = "v1" });
        var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
        var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
        c.IncludeXmlComments(xmlPath);
    });

    // Refit + HttpClient
    services.AddHttpClient("ProductClient", client =>
    {
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        client.BaseAddress = new Uri(Configuration.GetSection("CrudClient:BaseAddress").Value);
    })
    .AddTypedClient(RestService.For<IExtremelyDifficultCrudApi<Product, string>>);
}

Para testar o Refit você pode consumir qualquer API de sua preferência. Neste caso estou utilizando como server para este client o projeto de API que criamos nos dois artigos anteriores. Você pode conferir o código aqui, e para maiores detalhes da implementação, os artigos são estes a seguir:

Veja abaixo a implementação da ProductController que possui todos os seus endpoints apontados para os endpoints criados nos dois artigos acima (CRUD de Product). Para utilizar o Refit na controller, basta injetá-lo no construtor.

[Route("api/[controller]")]
[ApiController]
public class ProductController : ControllerBase
{
    private readonly IExtremelyDifficultCrudApi<Product, string> _clientFactory;

    public ProductController(IExtremelyDifficultCrudApi<Product, string> clientFactory)
    {
        _clientFactory = clientFactory;
    }

    /// <summary>
    /// Select all products
    /// </summary>
    /// <returns></returns>
    [HttpGet]
    [ProducesResponseType(200)]
    [ProducesResponseType(204)]
    public async Task<IActionResult> GetAll()
    {
        var allProductsRequest = await _clientFactory.GetAll();
        if (allProductsRequest.Content != null)
        {
            return Ok(allProductsRequest.Content);
        }

        return NoContent();
    }

    /// <summary>
    /// Select a single product
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    [HttpGet]
    [Route("{id}", Name = "GetById")]
    [ProducesResponseType(200)]
    [ProducesResponseType(404)]
    [ProducesResponseType(400)]
    public async Task<IActionResult> GetById([FromRoute] string id)
    {
        //Avoiding invalid id to call to api
        if (string.IsNullOrEmpty(id))
            return BadRequest();

        var product = await _clientFactory.GetById(id);
        if (product != null && product.IsSuccessStatusCode)
        {
            return Ok(product.Content);
        }

        return NotFound();
    }

    /// <summary>
    /// Creates a single product
    /// </summary>
    /// <param name="product"></param>
    /// <returns></returns>
    [HttpPost]
    [ProducesResponseType(201)]
    public async Task<IActionResult> Register([FromBody] Product product)
    {
        var createdProduct = await _clientFactory.Create(product);
        return Created(nameof(GetById), createdProduct.Content);
    }

    /// <summary>
    /// Updates a single product
    /// </summary>
    /// <param name="id"></param>
    /// <param name="product"></param>
    /// <returns></returns>
    [HttpPut]
    [Route("{id}")]
    [ProducesResponseType(202)]
    [ProducesResponseType(400)]
    [ProducesResponseType(404)]
    public async Task<IActionResult> Update([FromRoute] string id, [FromBody] Product product)
    {
        //Avoiding invalid id to call to api
        if (string.IsNullOrEmpty(id))
            return BadRequest();

        var productFromApi = await _clientFactory.Update(id, product);
        if (productFromApi != null && productFromApi.IsSuccessStatusCode)
        {
            return Accepted(productFromApi.Content);
        }

        return NotFound();
    }


    /// <summary>
    /// Removes a single product
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    [HttpDelete]
    [Route("{id}")]
    [ProducesResponseType(200)]
    [ProducesResponseType(400)]
    [ProducesResponseType(404)]
    public async Task<IActionResult> Delete([FromRoute] string id)
    {
        //Avoiding invalid id to call to api
        if (string.IsNullOrEmpty(id))
            return BadRequest();

        var product = await _clientFactory.GetById(id);
        if (product is null)
            return NotFound();

        await _clientFactory.Delete(id);
        return Ok();
    }
}

Exige pouco código de implementação, é muito simples e muito útil. Espero que tenha gostado desta dica!

Por hoje é só. Muito obrigado pela leitura e qualquer dúvida fique a vontade para me procurar e enviar e-mails! Espero que de alguma forma eu tenha ajudado!

Um grande abraço e até a próxima!

Quer saber mais sobre boas práticas de web api, docker, docker compose, testes unitários e de integração? Baixe meu e-book free!