Para crear una API REST efectiva, es fundamental implementar un enfoque limpio, en el cual la gestión de versiones sea uno de los aspectos cruciales. Un buen sistema de versionado asegura que los cambios en la API no afecten a los clientes existentes, permitiendo una evolución continua del sistema sin interrumpir el servicio.

En primer lugar, cuando se configura la API, el uso de versiones debe estar claramente definido. Por ejemplo, en un escenario con ASP.NET Core, podemos utilizar métodos como WithApiVersionSet(versionSet).MapToApiVersion(1.0) para asignar versiones específicas a los distintos puntos finales de la API. Esta técnica no solo ayuda a organizar las versiones de manera eficiente, sino que también permite que los consumidores de la API elijan qué versión utilizar a través de las URL, como /version para la versión 1.0, /version2only para la versión 2.0, y /versionneutral para una versión neutral. Este tipo de enfoque hace que sea claro y sencillo para los desarrolladores y los usuarios saber qué versión están utilizando.

Es crucial entender que esta versión de la API no es algo que se agregue a la URL de forma arbitraria, sino que tiene que estar bien gestionada y registrada, por ejemplo, usando el método WithApiVersionSet(versionSet). Este tipo de configuración permite que la API responda correctamente a las solicitudes, y facilita la actualización o desactivación de versiones anteriores a medida que se implementan nuevas versiones. En la documentación generada por Swagger, esto es reflejado de forma visual, mostrando las distintas versiones de la API y permitiendo que el usuario seleccione cuál desea ver. Sin embargo, en algunas versiones previas de ASP.NET Core, podría haber problemas con la visualización de todas las versiones disponibles, lo que se soluciona mediante la codificación de las versiones directamente.

La interacción de Swagger con las versiones de la API también incluye la capacidad de permitir a los usuarios configurar la versión a través del encabezado api-version, lo cual facilita la tarea de probar la API en diferentes versiones directamente desde la interfaz gráfica proporcionada por Swagger.

Además de gestionar las versiones, la documentación clara y detallada de los endpoints es un componente esencial. Para ello, el uso de comentarios XML en las clases de entrada y en los parámetros es fundamental. Aunque los comentarios XML no se aplican directamente a las expresiones lambda en los métodos de los endpoints, sí pueden añadirse a los parámetros de los métodos de la API. Para ello, se puede usar una clase estática como SwaggerXmlComments que se encarga de incluir los comentarios XML en la configuración de Swagger. Esto permite que los parámetros de entrada de las solicitudes sean claramente documentados en la interfaz de Swagger, mejorando la comprensión del comportamiento de la API.

Sin embargo, agregar estos comentarios XML no es suficiente para toda la documentación. En ocasiones, será necesario utilizar anotaciones de Swagger, como el atributo SwaggerOperation, para proporcionar descripciones detalladas sobre los métodos de la API. Estas anotaciones ofrecen un control más fino sobre la documentación, permitiendo añadir detalles adicionales como una descripción extensa o una nota explicativa acerca del propósito de cada endpoint. Las anotaciones también son útiles cuando se desea reemplazar los comentarios XML en los métodos que no pueden recibirlos, como los de tipo lambda.

Además de estas configuraciones, una correcta integración de Swagger con las funcionalidades de versionado, junto con una documentación bien estructurada y detallada, permite a los desarrolladores garantizar una experiencia más fluida para los consumidores de la API. De esta manera, no solo se facilita la interacción con las versiones de la API, sino también la comprensión y el uso adecuado de cada endpoint.

Es importante que los lectores comprendan que la correcta gestión de versiones y la adecuada documentación no son tareas aisladas, sino que son aspectos interrelacionados que, cuando se implementan adecuadamente, mejoran significativamente la calidad y la sostenibilidad de la API a largo plazo. La actualización constante de versiones debe ir acompañada de una documentación actualizada para evitar confusión y facilitar la integración de nuevos desarrolladores o consumidores de la API.

¿Cómo funcionan realmente los middlewares condicionales y ramificados en ASP.NET Core?

En ASP.NET Core, la forma en que se ejecutan los middlewares dentro del pipeline puede parecer confusa al principio, especialmente cuando se introducen variantes condicionales como Map, MapWhen, UseWhen y sus combinaciones. Una comprensión errónea de cómo estas estructuras funcionan puede llevar a comportamientos inesperados o endpoints que nunca se ejecutan. Por eso es crucial entender no solo qué hace cada middleware, sino también cuándo y cómo se ejecuta en el flujo completo de la aplicación.

Los middlewares como MapGet, MapPost y similares no inician una nueva rama del pipeline, a diferencia de Map, que sí lo hace. Esto es un punto clave: MapGet y sus variantes simplemente agregan un endpoint a la rama principal del pipeline, mientras que Map crea una bifurcación completa, basada en una coincidencia de ruta. Esta bifurcación no continúa con la ejecución del pipeline principal: si se cumple la condición de ruta, se ejecuta exclusivamente la rama definida por Map.

Por otra parte, middlewares como MapWhen o UseWhen se comportan de forma similar a sus versiones base (Map y Use, respectivamente), pero solo si se cumple una condición definida. Cuando esta condición no se cumple, el pipeline principal sigue ejecutándose normalmente, ignorando la rama condicional. Sin embargo, si se cumple, la ejecución se desvía completamente hacia esa rama, y si dentro de esa rama existe un Run, el pipeline se detiene allí.

Un ejemplo claro de este comportamiento se da al definir un MapWhen que evalúa si existe un parámetro q en la cadena de consulta de una petición GET a /test. Si q está presente, el pipeline condicional se ejecuta, deteniéndose en un Run. Lo importante aquí es que el orden en el que se declara el endpoint MapGet("/test") no afecta su ejecución, ya que este se encuentra en la rama principal, mientras que MapWhen inicia una rama independiente cuando se cumple la condición. Si q está presente, la ejecución nunca llegará al MapGet("/test").

Del mismo modo, al usar UseWhen con una condición basada en la presencia del parámetro p en la cadena de consulta, se puede alterar el flujo. Si p no está presente, se ejecutan los middlewares en orden —incluyendo MapGet("/test")— y no se entra en la rama condicional. Pero si p sí está presente y dentro de la rama de UseWhen se encuentra un Run, la ejecución se detiene allí, sin llegar nunca al endpoint /test, aunque esté correctamente definido en la rama principal.

Otro punto esencial es el comportamiento de Use y Run. Use permite la ejecución del middleware siguiente al invocar next(), mientras que Run detiene por completo el pipeline una vez ejecutado. La ubicación de un Run, dentro o fuera de una rama condicional, puede cambiar por completo el comportamiento de la aplicación.

Finalmente, es posible encapsular middlewares personalizados usando clases, como en el caso de UseMiddleware, que se comporta como un Use, pero con la lógica definida externamente. Esto promueve una arquitectura más limpia, especialmente en proyectos complejos donde la reutilización de middlewares es común. Un ejemplo es la clase LoggingMiddleware, que simplemente registra información, pero puede usarse en múltiples puntos del pipeline sin duplicar código.

Es importante también entender que todas estas decisiones de diseño —el uso de Map, Use, condiciones en tiempo de ejecución, o la presencia de Run— afectan no solo al flujo de la aplicación, sino también a su capacidad de mantenimiento, extensibilidad y control de errores. Usar MapWhen o UseWhen sin comprender que inician ramas completamente independientes puede llevar a endpoints que "desaparecen" o no se ejecutan nunca, y la causa suele estar en un middleware Run que detiene la ejecución antes de llegar a ellos.

¿Cómo gestionar la identidad de usuario mediante Claims y un servicio personalizado?

Cuando un usuario inicia sesión, su identidad se define a través de los Claims, que son datos de identidad proporcionados por el proveedor de identidad. Estos Claims definen el perfil del usuario, es decir, contienen información como el nombre o los roles del usuario. Esta información es completamente personalizable desde el lado del proveedor de identidad, aunque no profundizaré en esos detalles en este momento. Lo importante es entender cómo podemos gestionar estos Claims de forma más accesible y comprensible en nuestra aplicación.

Para lograr esto, podemos diseñar un servicio que exponga los datos del perfil del usuario de manera más sencilla. Tomemos como ejemplo la clase UserProfile, que implementa la interfaz IUserProfile y contiene dos propiedades básicas: Name y Roles. La implementación de esta clase es bastante directa, y a continuación presento un fragmento de código:

csharp
using System.Security.Claims;
namespace AspNetCore8MinimalApis.Identity { public class UserProfile : IUserProfile { private readonly IHttpContextAccessor _context; public UserProfile(IHttpContextAccessor context) { _context = context; } public string Name => _context.HttpContext.User?.Claims.FirstOrDefault(x => x.Type == "name")?.Value; public IEnumerable Roles => _context.HttpContext.User?.Claims.Where(x => x.Type == ClaimTypes.Role).Select(x => x.Value); } }

En este ejemplo, se accede a los Claims mediante LINQ, una técnica muy útil para filtrar y seleccionar la información que necesitamos. Es relevante recordar que la clase ClaimsPrincipal no está disponible directamente mediante inyección de dependencias, como en un endpoint mínimo cuando se trata de un servicio personalizado. Para solucionarlo, no olvides registrar el servicio IUserProfile con su implementación UserProfile y el servicio HttpContextAccessor utilizando el método de extensión AddHttpContextAccessor.

Una vez configurado el servicio, podemos utilizar la interfaz IUserProfile para acceder a la identidad del usuario de forma más ordenada, como se muestra en la figura correspondiente a la implementación del endpoint.

Por otro lado, es fundamental comprender que la autenticación basada en JWT (JSON Web Token) es la más utilizada en la actualidad debido a su fiabilidad y la amplia adopción de OpenID Connect. El uso de JWT facilita la autenticación de APIs y su integración en aplicaciones más complejas. Es importante, sin embargo, probar correctamente las APIs que implementen este tipo de autenticación para asegurar su correcto funcionamiento.

Si bien no entraremos en detalles sobre las pruebas de integración o de extremo a extremo en este caso, es esencial que cualquier aplicación que utilice autenticación mediante JWT se someta a pruebas unitarias para garantizar que cada componente funcione correctamente de forma aislada, sin depender de servicios externos.

Al realizar pruebas unitarias, debemos centrarnos en verificar el comportamiento específico de cada unidad de código, asegurándonos de que los test sean legibles, rápidos y lo más completos posibles. El uso de herramientas adecuadas como xUnit para las pruebas unitarias o Microsoft.NET.Test.Sdk para la ejecución de pruebas en .NET es indispensable para llevar a cabo pruebas efectivas.

Además, para mejorar la cobertura de pruebas y asegurar que todos los componentes del sistema estén debidamente validados, se recomienda realizar tests de los registros de dependencias, como las clases, repositorios y middleware que hemos configurado en nuestra aplicación.

El manejo adecuado de Claims y la implementación de pruebas unitarias no solo aumentan la seguridad y fiabilidad de nuestras aplicaciones, sino que también nos permiten mantener el control total sobre el comportamiento de los servicios de autenticación y perfil de usuario, lo cual es crucial en entornos de desarrollo profesional y en producción.