En el mundo del desarrollo de APIs, la organización y estructuración del código son cruciales para mantener la claridad y la escalabilidad de una aplicación. En ASP.NET Core 8, una de las características que permite optimizar el trabajo con rutas es el agrupamiento de rutas, una técnica que organiza múltiples puntos finales bajo un mismo "tronco" o prefijo de URL, simplificando el manejo de rutas y evitando la repetición de código. Esta funcionalidad facilita tanto el trabajo de los desarrolladores como el mantenimiento de la aplicación.

Para ilustrar cómo funcionan los grupos de rutas, consideremos el siguiente ejemplo de un conjunto de rutas que gestionan datos sobre países. En el código se agrupan tres rutas principales: una que lista todos los países, otra que permite obtener un país por su ID, y la última que devuelve los idiomas hablados en un país dado.

csharp
public static class MyGroups { public static RouteGroupBuilder GroupCountries(this RouteGroupBuilder group) {
var countries = new string[] { "France", "Canada", "USA" };
var languages = new Dictionary<string, List<string>> {
{ "France", new List<string> { "french" } },
{
"Canada", new List<string> { "french", "english" } },
{ "USA", new List<string> { "english", "spanish" } }
};
group.MapGet("/", () => countries); group.MapGet("/{id}", (int id) => countries[id]); group.MapGet("/{id}/languages", (int id) => { var country = countries[id]; return languages[country]; }); return group; } }

Este código ilustra cómo un grupo de rutas se puede definir utilizando la extensión GroupCountries, la cual agrupa tres rutas distintas bajo el prefijo /countries. La funcionalidad de agrupamiento no solo organiza las rutas, sino que también las vincula bajo un mismo conjunto de reglas, facilitando su gestión. En el archivo Program.cs de la aplicación, se debe registrar este grupo de rutas:

csharp
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapGroup("/countries").GroupCountries(); app.Run();

Es importante destacar que al definir el grupo de rutas con MapGroup, todas las rutas del grupo heredarán este prefijo, produciendo las siguientes URLs:

  • /countries

  • /countries/{id}

  • /countries/{id}/languages

El uso del prefijo es un elemento fundamental en el agrupamiento, ya que mejora la organización y facilita la lectura y el mantenimiento del código.

Reutilización de restricciones en las rutas

Una ventaja importante del agrupamiento de rutas en ASP.NET Core es la capacidad de reutilizar restricciones dentro de un grupo. En el ejemplo anterior, el parámetro {id} aparece en dos rutas diferentes, lo que nos lleva a la posibilidad de definir una variable de restricción para este parámetro. Esto se logra a través de la extensión MapGroup, que permite definir restricciones que pueden ser reutilizadas:

csharp
var idGroup = group.MapGroup("/{id}");
idGroup.MapGet("/", (int id) => countries[id]); idGroup.MapGet("/languages", (int id) => { var country = countries[id]; return languages[country]; });

De esta manera, los dos puntos finales comparten la misma restricción en el parámetro {id} sin necesidad de reescribir la lógica de validación, lo que mejora la eficiencia y la claridad del código. Además, esta característica permite encadenar múltiples restricciones y porciones de URL, brindando una gran flexibilidad, aunque siempre es recomendable no abusar de esta capacidad para evitar que el código se vuelva difícil de entender.

El principio KISS en la práctica

El agrupamiento de rutas sigue el principio KISS (Keep It Simple, Stupid), que se refiere a la simplicidad en el diseño del código. Este principio es crucial cuando se trabaja con APIs que contienen muchos puntos finales. Si se escriben rutas sin agrupar, se corre el riesgo de duplicar código innecesariamente, lo cual dificulta el mantenimiento y la extensión de la aplicación.

El uso adecuado de los grupos de rutas es especialmente útil cuando se tiene un gran número de rutas relacionadas. En lugar de duplicar la misma estructura o lógica de ruta en varios puntos, el agrupamiento permite centralizarla y reutilizarla, haciendo que el código sea más limpio, más fácil de gestionar y más sencillo de entender.

Vinculación de parámetros en ASP.NET Core 8

En la sección anterior, se habló de cómo ASP.NET Core maneja la vinculación de parámetros. Este proceso implica que el framework convierta los parámetros de una solicitud HTTP en parámetros tipados que se pasan a la función que maneja la solicitud. Además de los tipos primitivos, ASP.NET Core 8 permite vincular tipos complejos como colecciones, objetos complejos (con ciertas limitaciones) y servicios inyectados.

Una de las características más poderosas de ASP.NET Core es la capacidad de vincular parámetros desde diversas fuentes de una solicitud HTTP. Estos parámetros pueden provenir de:

  • Rutas

  • Query strings

  • Cuerpo de la solicitud en formato JSON

  • Datos de formulario (pares clave/valor)

  • Encabezados

  • Otros, como clases de instancias de servicios inyectados

El uso de atributos explícitos para vincular parámetros permite a los desarrolladores manejar de forma clara y precisa de dónde provienen los datos. Por ejemplo, en el caso de un cuerpo JSON, se puede utilizar el atributo [FromBody] para indicar que el parámetro se vinculará con los datos contenidos en el cuerpo de la solicitud:

csharp
app.MapPost("/Addresses", ([FromBody] Address address) => { return Results.Created(); });

Esta capacidad de vinculación explícita de parámetros es fundamental para el manejo flexible de solicitudes HTTP, permitiendo que los datos provengan de diferentes partes de la solicitud de manera coherente y organizada.

Consideraciones finales

Al trabajar con grupos de rutas y vinculación de parámetros en ASP.NET Core, es esencial comprender cómo estas herramientas pueden facilitar el diseño de APIs limpias y mantenibles. El agrupamiento de rutas no solo organiza los puntos finales bajo un prefijo común, sino que también ofrece la flexibilidad de reutilizar restricciones y simplificar la gestión de rutas. Además, la capacidad de vincular parámetros de diversas fuentes de manera explícita permite una gran precisión en la forma en que se manejan los datos de las solicitudes. Sin embargo, es crucial no caer en la trampa de la sobrecomplicación; el principio de simplicidad siempre debe guiar las decisiones de diseño.

¿Cómo optimizar el uso de caché en las APIs con In-Memory Cache y el Patrón Decorador?

Al ejecutar un punto de acceso por primera vez, es probable que no percibas nada inusual. La respuesta será completamente normal y no se configurarán encabezados específicos. Sin embargo, si el punto de acceso ha sido previamente almacenado en caché por otro usuario, notarás la presencia del encabezado Age, que indica el tiempo que los datos llevan en la caché. Este encabezado es un indicativo importante, ya que te informa sobre la frescura de los datos que estás recibiendo, lo que puede ser crucial para ciertos casos de uso.

Aunque esta técnica de almacenamiento en caché no se utiliza con mucha frecuencia debido a sus limitaciones, es valioso comprender su funcionamiento. Para aquellos interesados en profundizar en este aspecto, Microsoft ofrece documentación adicional que puede ser útil para una comprensión más detallada sobre el almacenamiento en caché de salida: https://learn.microsoft.com/en-us/aspnet/core/performance/caching/output?view=aspnetcore-8.0.

Por otro lado, el caché en memoria (In-Memory Cache) es mucho más eficiente y controlable. En este caso, no se almacena en caché toda la respuesta HTTP, sino solo los datos que decidas almacenar. Esto otorga un control mucho mayor sobre lo que ocurre con las solicitudes, lo que permite, por ejemplo, realizar un seguimiento estadístico o registrar las acciones de los usuarios. Esta técnica es la más utilizada cuando tu aplicación no está distribuida, es decir, cuando un único servidor expone tu API. En un entorno distribuido, donde existen múltiples servidores, este tipo de caché se duplicará en cada servidor, lo que puede llevar a ciertas inconsistencias si no se gestiona adecuadamente.

En este escenario, la solución ideal es el uso de cachés distribuidos, un tema que se trata en la siguiente sección. Es importante tener en cuenta que cuando tienes una infraestructura de servidores con balanceo de carga, la caché será copiada en cada servidor individual, lo que podría generar problemas de sincronización si no se implementa un mecanismo adecuado para manejar dicha caché de manera centralizada.

El flujo de trabajo del caché en memoria puede observarse de manera gráfica para facilitar su comprensión. Este mecanismo se utiliza principalmente cuando quieres almacenar en caché una porción específica de los datos, lo cual es mucho más eficiente en términos de control que almacenar toda la respuesta HTTP.

Para integrar este tipo de caché en una arquitectura basada en el principio de responsabilidad única (SRP), se puede utilizar el Patrón Decorador. Este patrón es útil porque permite agregar responsabilidades adicionales a un objeto sin modificar su estructura interna. En este caso, puedes decorar una clase CountryService que maneja la obtención de datos desde una base de datos, y agregarle la responsabilidad de manejar la caché de manera dinámica.

El patrón Decorador ofrece una forma flexible de extender funcionalidades sin tener que recurrir a la herencia. La idea es crear una clase decoradora, como CachedCountryService, que implementa la interfaz ICountryService y delega las llamadas a la clase original CountryService. De esta forma, podemos aplicar el caché solo a los métodos que lo requieren, sin tener que modificar la implementación original.

El uso de IMemoryCache dentro del decorador permite almacenar los datos en memoria. En el ejemplo práctico, el caché se configura con una expiración absoluta de 30 segundos, lo que significa que después de ese tiempo, los datos almacenados en la caché serán descartados y se solicitará nuevamente la información al servicio de base de datos. Esta es una técnica comúnmente utilizada cuando no es necesario mantener los datos en caché por largos períodos de tiempo.

Sin embargo, el uso de SetSlidingExpiration para el control de la duración del caché es una opción menos recomendable en la mayoría de los casos. Esto se debe a que los elementos en caché no se actualizarán hasta que no haya solicitudes durante un periodo de tiempo determinado. Si los datos son modificados frecuentemente, esta técnica puede no ser adecuada, ya que podría retrasar la actualización de la caché.

En cuanto a la implementación de la caché en memoria, es importante configurar adecuadamente los servicios en el contenedor de dependencias de la aplicación. El ejemplo de código que se presenta muestra cómo configurar el patrón Decorador para el servicio ICountryService, la integración de IMemoryCache y la creación de un punto de acceso que utiliza la caché de manera eficiente. En este caso, se utiliza la clave de caché countries-{paging.PageIndex}-{paging.PageSize} para asegurar la unicidad de los datos almacenados, considerando los parámetros de la solicitud.

Este tipo de optimización puede mejorar significativamente el rendimiento de la API, reduciendo el tiempo de respuesta y la carga sobre el servidor de base de datos. Es una técnica especialmente útil en aplicaciones que manejan grandes volúmenes de datos o en aquellos casos donde la consistencia de los datos no es crítica y se pueden tolerar ciertos retrasos en la actualización de la caché.

Para que esta optimización sea efectiva, es importante tener en cuenta varios aspectos adicionales: primero, la correcta gestión del ciclo de vida del caché es fundamental. Esto incluye la configuración adecuada de la expiración, la eliminación de datos obsoletos y el manejo de la invalidación cuando los datos cambian. Segundo, el uso de caché debe ser evaluado cuidadosamente para cada caso de uso, ya que no todas las operaciones o datos deben ser almacenados en caché. Esto dependerá de la naturaleza de la aplicación y de los requisitos específicos de rendimiento y consistencia.