0

Mejores Prácticas: APIs #2

En el anterior post vimos algunas de las buenas prácticas que deberíamos implementar en nuestras API en .Net. En este segundo post veremos algunas más que nos serán útiles y ayudarán a mejorar la calidad de las Apis.

Manejo de errores

Ante cualquier excepción no manejada o controlada en nuestra aplicación nuestras API devolver el estado 500 Internal Server Error. Si bien nos dice que es un error del lado del servidor, no le indica a cliente que es lo que exactamente lo que está sucediendo. 

En lugar de devolver estados tan generales podemos hacer que nuestras api devuelvan algo más detallado. Como vimos en este mismo post tenemos varios valores de estado para un 4xx o un 5xx, además, agregar algún mensaje descriptivo del error. Esto permitirá a los clientes que tener saber realmente qué sucedió y que específicamente está saliendo mal y así poder tomar una decisión sobre la situación.

En ASP.Net podemos usar un middleware para manejar errores y excepción de forma Global controlador las solicitudes y las respuestas. Por defecto, tenemos disponible un middleware para manejar los errores y las excepción en ASP.Net. Esto nos evitará tener que usar una gran cantidad de try…catch en medio de nuestro código.

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;

            // using static System.Net.Mime.MediaTypeNames;
            context.Response.ContentType = Text.Plain;

            await context.Response.WriteAsync("An exception was thrown.");

            var exceptionHandlerPathFeature =
                context.Features.Get<IExceptionHandlerPathFeature>();

            if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
            {
                await context.Response.WriteAsync(" The file was not found.");
            }

            if (exceptionHandlerPathFeature?.Path == "/")
            {
                await context.Response.WriteAsync(" Page: Home.");
            }
        });
    });

    app.UseHsts();
}

UseExceptionHandler no permitirá manejar las excepciones no controladas detectando todas las excepciones que genere nuestra api o nuestras aplicaciones permitiendo devolver un código de estado y el mensaje que creamos más convenientes.

Tenemos disponibles UseStatusCodePages que lo podemos utilizar para manejar todos los estados HTTP que sean diferentes a 200. El Middleware capturara todos los destinos de 200 y retorna el código de estado y el mensaje correspondiente.

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages();

Es posible escribir por otro lado nuestros handler para manejar los errores de manera personalizada creando middleware personalizados para este fin.

public class CustomExceptionMiddleware
{
    //constructor and service injection
 
    public async Task Invoke(HttpContext httpContext)
    {
        try
        {
            await _next(httpContext);
        }
        catch (Exception ex)
        {
            _logger.LogError("Unhandled exception ...", ex);
            await HandleExceptionAsync(httpContext, ex);
        }
    }
    
    //additional methods
}

Por último debemos registrarlo en nuestro contenedor de inyección de dependencias correspondiente a los middleware.

public static IApplicationBuilder UseCustomExceptionMiddleware(this IApplicationBuilder builder)
{
    return builder.UseMiddleware<CustomExceptionMiddleware>();
}
app.UseCustomExceptionMiddleware();

Versionado de APIs

Cuando nuestra api sufre un cambio, este cambio puede dañar la funcionalidad de los clientes que las consumen. Podemos controlar estas situaciones, cuándo y cómo, introducimos estos cambios críticos para obtener mayor flexibilidad y evitar posibles problemas.

Existen varias maneras de versiones nuestras API:

  • con atributos [ApiVersion(“2.0”)]
  • En la URL, como parte de la solicitud
  • Mediante el routing [Route(“api/{v:apiversion}/resource”]
  • En los encabezados HTTP.
  • Convenciones

El más utilizado es incluir en la dirección URL la versión que se desea consumir.

https://localhost:3000/api/v1/clientes

Esto permitirá que los clientes que consumen puedan seguir utilizando la versión que necesiten hasta poder comenzar a consumir las nuevas versiones. Veamos cómo lo configuramos en C# junto a la librería de Swagger:

Primero necesitaremos instalar los siguientes paquetes:

Install-Package Microsoft.AspNetCore.Mvc.Versioning  
Install-Package Swashbuckle.AspNetCore 
Install-Package Swashbuckle.AspNetCore.SwaggerGen   
Install-Package Swashbuckle.AspNetCore.SwaggerUi

Luego agregaremos en nuestro servicios

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

Tengo que buscar un mejor ejemplo

Mapeo de Objetos

La mayoría del tiempo nuestras vistas no muestran completamente los datos de las entidades. Con herramientas de mapeo podemos crear asignaciones entre un modelo de dominio o entidad a un modelo de vista sin realizar toda la asignación a mano. Esto es una buena práctica debido a que reduciremos el tráfico de red.

Los mapeadores se pueden utilizar para cualquier tipo de objeto como nuestros DTO (Data Transfer Object) o cualquier entidad de nuestro dominio. Por otro lado nos ayudará a mantener nuestro código limpio y que sea mantenible.

Las 2 librerías más utilizadas en .Net son Mapster y AutoMapper. La primera, puedes encontrar un post en web site de como utilizarla y qué funcionalidades tenemos disponibles. Ambos poseen una gran cantidad de documentación que podemos consultar en sus repositorios de GibHub. 

Logging en servicio API

Esto es algo que la mayoría de los programadores dejan afuera y verdaderamente es muy importante por 2 razones. La primera es que nos ayudará a depurar los errores en nuestro código, la segunda, nos puede proporcionar información de cómo se está consumiendo nuestra API.

Asp.Net Web API viene con funcionalidades integradas para registrar el servicio de login, solo deberemos hacer referencia a los paquetes Nuget: Microsoft.Extensions.Logging y Microsoft.Extensions.Logging.Console. Luego debemos inicializarla en nuestra método Configure:

public void Configure(ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();
}

Esto habilitará que todos los mensajes capturados de logging lo veamos en la consola de ejecución de nuestra aplicación.

Es posible utilizar otras librerías de terceros, como NLog o Serilog. Serilog es la más popular en estos momentos y crearé un post para esta librería con varios ejemplos. Ambas librerías tienen más funcionalidades que la que tenemos por default. Te invito a investigarlas.

Test de unidad

La mejor manera de garantizar que nuestros componentes de la aplicación funcionen correctamente de manera individual es utilizar pruebas unitarias. Nos ayudará a encontrar y corregir errores mientras estamos desarrollando antes de que lleguen a producción. En el caso de Visual Studio tenemos un modo Live que nos permite ver que test fallan sin la necesidad de ejecutar la batería de estos test.

Muchos programadores utilizan las pruebas de unidad como documentación del código. Es una buena práctica. Si respetamos todas las normas y nomenclaturas recomendadas pueden ser realmente claros en sus funcionalidades y que están ejecutando o probando.

A largo plazo puede ahorrarnos tiempo en pruebas de regresión. Por ejemplo, supongamos que modificamos varias partes de nuestro código que están asociadas a test de unidad, nuestro último test puede funcionar, pero los anteriores pueden fallar. Esto ayudaría a detectar los comportamientos no deseados.

Caching

Supongamos que nuestros clientes hacen solicitudes como por ejemplo, solicitar una lista de datos que no varía mucho. Para esto tiene que ir constantemente a la base de datos. Otro ejemplo puede ser un solicitud de un cálculo que se repite todo el tiempo y debe usarse procesamiento para resolverlo.

Cada vez que un cliente solicita información de esta manera es ineficiente. En el primer ejemplo debe ir a la base de datos todo el tiempo a pesar de que los datos siempre son los mismos o varían con poca frecuencia. El segundo ejemplo, el consumo de procesamiento por cada petición en una gran cantidad de peticiones puede ser excesivo.

Para solucionar esto podemos utilizar almacenamiento en caché. El caché funciona de una manera muy simple, tiene un KeyValue identificador y el objeto u objetos que deseamos almacenar. En el primer ejemplo, supongamos que nuestros datos son provincias o estados. Estos no cambian continuamente. Podemos crear un Caché con un KeyValue que se llame States y cada vez que venga la solicitud validar si lo tenemos en el cache, si no esta, irá a la base de de datos, si está, retorna el valor de esa KeyValue reduciendo así la transferencia de red.

Tenemos diferentes formas de implementar almacenamiento en caché. Podemos usar la memoria caché integrada que se encuentra implementada en ASP.Net o una librería como NCache o bien un caché distribuido como Redis.

Ten presente que el desafío con el caché no es utilizarlo, si no, cuando debemos invalidar este caché porque los datos han cambiado. Por esto último, ten presente el tiempo que debes mantenerlo en caché o inclusive, ver de implementar un caché reverso.

Conclusiones

Hasta aquí hemos visto algunas prácticas más que complementan al primer post para el desarrollo correcto de nuestros servicios APIs en .Net 6 o superior. Recuerda, muchos de estos no son solamente útiles en .Net, es posible llevarlos a otros lenguajes, como por ejemplo, Test de Unidad debido a que son principios.

Si deseas agregar alguna práctica más que recomiendas como importante. Dejame el comentario y con gusto lo haré. Gracias.

Fernando Sonego

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *