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 no controlada en nuestra aplicación, nuestras APIs devolverán el estado 500 Internal Server Error. Aunque esta indicación señala un error del lado del servidor, no proporciona al cliente una comprensión detallada de lo que está ocurriendo.
En lugar de recurrir a estados tan generales, podemos enriquecer nuestras APIs para que devuelvan información más detallada. Como explicamos en el post anterior, contamos con varios códigos de estado para las categorías 4xx o 5xx. Además, podemos añadir un mensaje descriptivo del error. Esto permitirá a los clientes comprender con precisión lo que sucedió, identificando específicamente el problema y permitiéndoles tomar decisiones informadas sobre la situación.
En ASP.Net, podemos aprovechar un middleware para gestionar errores y excepciones de manera global, controlando las solicitudes y las respuestas. De forma predeterminada, ASP.Net ofrece un middleware para gestionar errores y excepciones, lo que evita la necesidad de utilizar numerosos bloques try…catch dispersos en 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 APIi 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 nuestros servicios:
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
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 este punto, hemos explorado algunas prácticas adicionales que complementan el contenido del primer post, contribuyendo al desarrollo efectivo de nuestros servicios APIs en .NET 6 o versiones superiores. Es importante recordar que muchas de estas prácticas no solo son beneficiosas en el entorno de .NET, sino que también pueden aplicarse a otros lenguajes. Un ejemplo claro es la implementación de pruebas de unidad, ya que estas prácticas se basan en principios fundamentales.
Si tienes alguna otra práctica que consideras esencial y te gustaría compartir, no dudes en dejarme un comentario; con gusto la tomaré en cuenta. ¡Gracias!