DEV Community

loading...

C#: Llamadas confiables a APIs con Polly y RestEase

Isaac Ojeda
.NET Core, Azure, C#, TypeScript & Angular 😁.
・6 min read

Continuando el post anterior de llamadas a APIs sin esfuerzo con RestEase donde vimos como generar clientes Http especificando solamente la interfaz y RestEase genera la implementación de manera automática. Si aun no lo has leído, te recomiendo que lo leas primero.

Cuando trabajamos con Web Services desde C#, queremos que las llamadas que realicemos sean "resilient" y "reliables". Es decir, que sean confiables y flexibles en el sentido de que si ocurre un error (esperado o no) estas de manera automática manejen alguna política de reintentos, y así poder llevar a cabo su tarea.

Esto significa que cuando nuestra aplicación hace una llamada HTTP a algún servicio, estamos 100% seguros que puede fallar y tenemos que contemplarlo siempre, sí o sí.

Si nuestra llamada HTTP falló, nuestra aplicación debe de ser capaz de decidir si debe reintentarlo o no. También debe de decidir cuantas veces reintentar la operación y así tener un "circuit breaker" y no ciclar y alentar todo.

image

Para esto entra Polly, una librería que te ayuda a manejar estos escenarios sin tener que rompernos la cabeza.

Polly ayuda a crear políticas de reintentos, circuit breakers, timeouts y entre otras cosas (la página de GitHub explica todo muy bien) de una forma muy sencilla.

Para mostrarles el funcionamiento, crearemos una aplicación web:

> dotnet new webapp -o RestEasePollyExample
Enter fullscreen mode Exit fullscreen mode

Tendremos una aplicación con Razor Pages con la plantilla bootstrap default, que por ahora no le pondremos mucha atención.

Hay que instalar los siguientes NuGet packages que son necesarios:

> dotnet add package RestEase
> dotnet add package Microsoft.Extensions.Http.Polly
Enter fullscreen mode Exit fullscreen mode

También, necesitamos a IGitHubAPI y User del post anterior:

using System;
using Newtonsoft.Json;

namespace RestEasePollyExample
{
    public class User
    {
        public string Name { get; set; }
        public string Blog { get; set; }
        [JsonProperty("created_at")]
        public DateTime CreatedAt { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode
using System.Threading.Tasks;
using RestEase;

namespace RestEasePollyExample
{
    [Header("User-Agent", "RestEase")]
    public interface IGitHubApi
    {
        [Get("users/{userId}")]
        Task<User> GetUserAsync([Path] string userId);
    }
}
Enter fullscreen mode Exit fullscreen mode

Recuerden que esto es solo el contrato, RestEase se encargará de la implementación en Runtime.

Para configurar IGitHubApi y ser resuelta por Dependency Injection y además configurarle Polly, hay que hacerlo en el Startup.cs generado por la plantilla:

using System;
using System.Collections.Generic;
using System.Net.Http;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Polly;
using RestEase;

namespace RestEasePollyExample
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public const string GitHubClientName = "github";
        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            // Paso1: HttpClient para GitHub
            services.AddHttpClient(GitHubClientName, options =>
            {
                options.BaseAddress = new Uri("https://api.github.com");
            })
            .AddTransientHttpErrorPolicy(builder =>
            {
                var retries = new List<TimeSpan>()
                {
                    TimeSpan.FromSeconds(5),
                    TimeSpan.FromSeconds(10)
                };

                return builder.WaitAndRetryAsync(retries);
            });

            // Paso 2: Implementación ReastEase para GitHub
            services.AddTransient<IGitHubApi>(provider =>
            {
                var factory = provider.GetService<IHttpClientFactory>();
                var httpClient = factory.CreateClient(GitHubClientName);

                var restClient = new RestClient(httpClient).For<IGitHubApi>();

                return restClient;
            });

            services.AddRazorPages();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
            });
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Esto es muy estándar en aplicaciones Razor/MVC, pero lo que es diferente es el método ConfigureServicesen el cual nos enfocaremos:

AddHttpClient y AddTransientHttpErrorPolicy

Aquí se está configurando el HttpClient (puedes leer más aquí) y simplemente estamos registrando un Cliente con el nombre de github. Esto tiene un propósito especial para el performance de la aplicación, pero para no entrar en mucho detalle, puedes leer el enlace de la documentación de Microsoft.

Lo que nos agrega la librería de Polly es el método AddTransientHttpErrorPolicy que para fines prácticos estamos configurando una política de 2 reintentos. En el primer reintento esperamos 5 segundos y en el segundo 10 segundos.

Se pueden configurar cosas más exóticas y elaboradas pero hacemos esto para dejarlo sencillo.

AddTransient

Aquí, estamos registrando una interface en el contenedor de dependencias para que sea resuelta por RestEase.

Como ya configuramos el HttpClient con Polly, hay que reutilizarlo con la auto-implementación de RestEase. Es por eso que primero resolvemos (con el factory) el cliente HttpClient con el nombre github y se lo asignamos a RestClient para que lo utilice.

En el ejemplo pasado no especificábamos ningún HttpClient, en ese caso RestEase solicitaba uno por el mismo. En este escenario le especificamos uno que configuramos nosotros con Polly.

Index.cshtml.cs

Para poder terminar este ejemplo, hay que consultar la información de la API de GitHub:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;

namespace RestEasePollyExample.Pages
{
    public class IndexModel : PageModel
    {
        private readonly IGitHubApi _github;

        public IndexModel(IGitHubApi github)
        {
            _github = github;
        }

        public User GitHubUser { get; set; }

        public async Task OnGet(string userName)
        {
            if (!string.IsNullOrEmpty(userName))
            {
                GitHubUser = await _github.GetUserAsync(userName);
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Index.cshtml.cs ya existe en la plantilla webapp que usamos, la modificamos para inyectar el cliente Github y lo usamos en el OnGet de la página.

Index.cshtml

@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

<div class="text-center">
    <h1 class="display-4">Bienvenido</h1>
    <form method="GET">
        <p>Escribe algún nombre de usuario de Github</p>
        <div class="form-group row">
            <label class="col-form-label col-md-3" for="userName">Nombre</label>
            <div class="col-md-9">
                <input type="text" class="form-control" name="userName" id="userName" required />
            </div>
        </div>
        <div class="form-group row">
            <div class="col-md-1 offset-md-3">
                <button class="btn btn-primary">
                    Buscar
                </button>
            </div>
        </div>
    </form>
    @if (Model.GitHubUser != null)
    {
        <div class="form-group row">
            <label class="col-form-label col-md-3">Nombre</label>
            <div class="col-md-4">
                <div class="form-control-plaintext">@Model.GitHubUser.Name</div>
            </div>
        </div>
        <div class="form-group row">
            <label class="col-form-label col-md-3">Blog</label>
            <div class="col-md-4">
                <div class="form-control-plaintext">@Model.GitHubUser.Blog</div>
            </div>
        </div>
        <div class="form-group row">
            <label class="col-form-label col-md-3">Se unió</label>
            <div class="col-md-4">
                <div class="form-control-plaintext">@Model.GitHubUser.CreatedAt.ToString("dd/MM/yyyy")</div>
            </div>
        </div>
    }
</div>
Enter fullscreen mode Exit fullscreen mode

La vista (que también fue creada por la plantilla) la cambiamos por este contenido para agregar un buscador de usuarios de GitHub.

Manteniendo el ejemplo simple, solo hacemos un formulario GET de la manera tradicional y mostramos la información si esta existe.

Lo importante es lo que ocurre en consola, para demostrar que los reintentos se realizan, en lugar de consultar api.github.com, cambié el Uri por localhost (lo cual en mi PC esto fallará).

Si buscamos isaacOjeda y "provocamos" un fallo, esta se ejecutará 3 veces: la llamada inicial y los 2 reintentos:

info: System.Net.Http.HttpClient.github.LogicalHandler[100]
      Start processing HTTP request GET https://localhost/users/isaacOjeda
info: System.Net.Http.HttpClient.github.ClientHandler[100]
      Sending HTTP request GET https://localhost/users/isaacOjeda
info: System.Net.Http.HttpClient.github.ClientHandler[100]
      Sending HTTP request GET https://localhost/users/isaacOjeda
info: System.Net.Http.HttpClient.github.ClientHandler[100]
      Sending HTTP request GET https://localhost/users/isaacOjeda
Enter fullscreen mode Exit fullscreen mode

Cuando se acabaron los reintentos (lo consideramos como el circuit breaker) lanzará una excepción de que no pudo realizar con éxito la llamada HTTP:

Sending HTTP request GET https://localhost/users/isaacOjeda
fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
      An unhandled exception has occurred while executing the request.
      System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception.
       ---> System.IO.IOException: Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host..
       ---> System.Net.Sockets.SocketException (10054): An existing connection was forcibly closed by the remote host.
         --- End of inner exception stack trace ---
       ...
Enter fullscreen mode Exit fullscreen mode

Como no podemos hacer fallar api.github.com pues lo simulamos con localhost, pero la idea es usar obviamente el Uri correcto para obtener los resultados correctos, de una forma confiable y a prueba de fallos:

info: System.Net.Http.HttpClient.github.LogicalHandler[100]
      Start processing HTTP request GET https://api.github.com/users/isaacOjeda
info: System.Net.Http.HttpClient.github.ClientHandler[100]
      Sending HTTP request GET https://api.github.com/users/isaacOjeda
info: System.Net.Http.HttpClient.github.ClientHandler[101]
      Received HTTP response headers after 448.2601ms - 200
info: System.Net.Http.HttpClient.github.LogicalHandler[101]
      End processing HTTP request after 468.1723ms - 200
Enter fullscreen mode Exit fullscreen mode

image

De esta forma pudimos configurar un HttpClient con una politica de reintentos para hacerlo confiable en caso de algún error.

También complementamos esta funcionalidad con RestEase y su autoimplementación de interfaces para consumir servicios web.

Si tienes preguntas, no dudes en unirte a nuestro Discord o por medio de mi twitter en @balunatic

Code4Fun

Discussion (0)