El modo de desarrollo en ASP.NET Core es una de las características más importantes para los desarrolladores, ya que ofrece una forma de adaptar el comportamiento de la aplicación durante el ciclo de vida de desarrollo, garantizando una mejor experiencia en la depuración y la configuración de aplicaciones. En este contexto, la configuración de la aplicación juega un papel esencial al permitir personalizar cómo se ejecutan ciertos parámetros de seguridad, como las cadenas de conexión, dependiendo de si el entorno es de desarrollo o de producción.

El modo de desarrollo se activa ajustando la variable de entorno ASPNETCORE_ENVIRONMENT a "Development" en el archivo launchSettings.json o directamente desde el panel de propiedades del proyecto. Esta variable determina cómo ASP.NET Core maneja ciertas configuraciones, como la exposición de información detallada de los errores no manejados. Esto es crucial durante el desarrollo, ya que permite a los desarrolladores obtener información más completa sobre cualquier problema que ocurra en el sistema, lo que facilita la depuración. Sin embargo, debido a la naturaleza sensible de esta información, se recomienda desactivar el modo de desarrollo en entornos de producción, para evitar la exposición de detalles que podrían ser aprovechados por atacantes.

En el archivo launchSettings.json, se pueden configurar tanto los perfiles para IIS Express como para la ejecución en modo autónomo, asegurando que la aplicación se ejecute correctamente en diferentes entornos. Por ejemplo, un archivo launchSettings.json para el modo de desarrollo podría incluir configuraciones como las siguientes:

json
{
"iisSettings": { "windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": { "applicationUrl": "http://localhost:57090", "sslPort": 44366 } }, "profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "MVCDemo": { "commandName": "Project", "dotnetRunMessages": "true",
"launchBrowser": true,
"applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } }

En este ejemplo, se configura tanto el servidor IIS Express como un servidor autónomo (autohospedado) para ejecutar la aplicación en modo de desarrollo. Es importante que este tipo de configuración se use solo en entornos de desarrollo, ya que en producción, la seguridad y el rendimiento podrían verse comprometidos.

Una vez activado el modo de desarrollo, las aplicaciones ASP.NET Core también pueden aprovechar características avanzadas como los Web API para exponer servicios que se puedan consumir desde otras aplicaciones. Los Web API en ASP.NET Core se basan en el patrón Model-View-Controller (MVC), pero en lugar de devolver vistas HTML, devuelven respuestas en formatos como JSON o XML, lo que los hace ideales para integrarse con aplicaciones móviles o aplicaciones web que requieren consumir datos de forma eficiente.

En la creación de una Web API con ASP.NET Core, el proceso comienza con la selección del tipo de proyecto adecuado en Visual Studio. Al seleccionar "ASP.NET Core Web API", el desarrollador tiene la posibilidad de elegir diversas opciones, como el tipo de autenticación, la compatibilidad con HTTPS, el soporte para Docker y la integración con OpenAPI. Estas opciones permiten personalizar el comportamiento de la API y asegurar que funcione de manera eficiente en una variedad de entornos.

Uno de los beneficios clave de esta plataforma es la integración con Swagger y OpenAPI. Swagger es una herramienta poderosa para documentar y explorar APIs RESTful, permitiendo a los desarrolladores ver de manera interactiva todos los puntos finales de la API, como se muestra en la interfaz Swagger UI. Esto facilita el consumo de los servicios ofrecidos por la API, ya que los desarrolladores pueden ver cómo interactuar con los diferentes puntos finales sin necesidad de escribir código adicional.

Además, ASP.NET Core permite utilizar herramientas como HttpRepl, un cliente ligero de línea de comandos que facilita la exploración de las APIs directamente desde la terminal. Esta herramienta es multiplataforma y permite realizar solicitudes HTTP a las APIs y ver los resultados de manera rápida y eficiente. Con comandos como DELETE, GET, POST y PUT, HttpRepl se convierte en una opción flexible para interactuar con las APIs de manera sencilla.

Por otro lado, es importante tener en cuenta que la seguridad de las aplicaciones en ASP.NET Core no debe verse comprometida por la facilidad de uso. Mientras que el modo de desarrollo permite ver detalles sobre errores no manejados, estos detalles deben ser protegidos en entornos de producción para evitar filtraciones de información sensible. Del mismo modo, las variables de entorno deben ser configuradas cuidadosamente, y las cadenas de conexión deben ser cifradas cuando sea posible, especialmente en aplicaciones de producción.

Otro aspecto relevante a considerar es el uso de Docker para la contenedorización de aplicaciones. Docker permite empaquetar una aplicación junto con sus dependencias y configuraciones, lo que facilita su ejecución en cualquier entorno sin preocuparse por las inconsistencias entre los sistemas operativos. Esta capacidad es especialmente útil cuando se desarrollan aplicaciones para la nube o cuando se necesita garantizar que la aplicación se ejecute de la misma manera en diferentes máquinas.

En resumen, ASP.NET Core proporciona un conjunto robusto de herramientas para desarrollar aplicaciones web y APIs de alto rendimiento. Al configurar adecuadamente el modo de desarrollo, los desarrolladores pueden aprovechar las ventajas del marco de trabajo, mientras mantienen la seguridad y la eficiencia del código en entornos de producción.

¿Cómo implementar el enlace de parámetros personalizados en las API REST con ASP.NET Core?

ASP.NET Core 8 ofrece dos tipos de enlace personalizado de parámetros:

  1. Datos provenientes de encabezados, cadenas de consulta o rutas.

  2. Datos provenientes del cuerpo (y datos de formularios).

Ambos métodos pueden ser útiles para manejar diferentes tipos de solicitudes y datos, pero cada uno tiene sus propias características y particularidades que deben entenderse bien para implementarlos de manera eficaz.

Enlace de parámetros personalizados desde encabezados

Supongamos que un cliente desea enviarte una lista de identificadores de países a través de una solicitud HTTP GET, utilizando los encabezados, concatenados por un guion. Por ejemplo, la cadena "1-2-3". Para manejar esto, se puede crear una clase CountryIds que contenga una propiedad Ids, la cual será una lista de enteros que intentaremos vincular desde la cadena de identificadores que recibimos en los encabezados. Para que este enlace funcione, es necesario implementar un método estático TryParse que ASP.NET Core reconocerá y ejecutará automáticamente.

Este método intentará analizar la cadena de texto recibida y convertirla en una lista de enteros. La clase CountryIds debe implementarse de tal manera que se pueda manejar correctamente este tipo de enlace, como se muestra en el siguiente código:

csharp
namespace AspNetCore8MinimalApis.Models
{ public class CountryIds { public List<int> Ids { get; set; } public static bool TryParse(string? value, IFormatProvider? provider, out CountryIds countryIds) { countryIds = new CountryIds(); countryIds.Ids = new List<int>(); try { if (value is not null && value.Contains("-")) { countryIds.Ids = value.Split('-').Select(int.Parse).ToList(); return true; } return false; } catch { return false; } } } }

En el ejemplo anterior, la clase CountryIds incluye el método TryParse que convierte la cadena de texto del encabezado en una lista de enteros. En el punto final de la API, el parámetro se recibe de la siguiente manera:

csharp
app.MapGet("/countries/ids", ([FromHeader] CountryIds ids) => {
Results.NoContent(); });

El atributo [FromHeader] es necesario para que ASP.NET Core sepa que el parámetro debe tomarse de los encabezados HTTP. Esta implementación es bastante sencilla y eficiente, aunque es importante tener en cuenta que el uso de este enfoque puede no ser algo común en muchas aplicaciones.

Enlace de parámetros personalizados desde el cuerpo de la solicitud

El enlace de parámetros desde el cuerpo de la solicitud funciona de manera algo diferente. En lugar de usar un método como TryParse, aquí se emplea el método estático BindAsync, que recibe el contexto HTTP como parámetro. Esto nos permite obtener los datos del formulario directamente desde el contexto y, por lo tanto, podemos omitir los atributos [FromForm] y [FromBody] en los parámetros de entrada.

Imaginemos que tu cliente desea subir un archivo y pasar metadata adicional. En el formulario de datos, el cliente pasaría un archivo, pero en lugar de enviarte los datos de un objeto Country en formato JSON bajo una propiedad llamada "Country", la estructura sería algo diferente. La clase Country se puede implementar para manejar este tipo de solicitudes:

csharp
using System.Reflection; using System.Text.Json; namespace AspNetCore8MinimalApis.Models { public class Country { public int? Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string FlagUri { get; set; }
public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) { var countryFromValue = context.Request.Form["Country"]; var result = JsonSerializer.Deserialize<Country>(countryFromValue); return ValueTask.FromResult(result); } } }

En este caso, el método BindAsync extrae la propiedad "Country" del cuerpo de la solicitud (que es parte de los datos del formulario), la deserializa en un objeto Country utilizando la API System.Text.Json, y luego devuelve el objeto deserializado.

En el punto final de la API, el método POST puede ser algo como esto:

csharp
app.MapPost("/countries/upload", (IFormFile file, Country country) => { Results.NoContent(); });

Al ejecutar esta solicitud, el comportamiento esperado es que los parámetros se vinculen correctamente, y los datos del formulario sean procesados y deserializados en el objeto adecuado.

Reflexiones adicionales

Es crucial recordar que, al implementar enlaces personalizados de parámetros, el orden y tipo de datos en las solicitudes HTTP son fundamentales. La forma en que se pasan los datos desde el cliente puede variar dependiendo del contexto, por lo que es importante ser flexible y tener en cuenta las mejores prácticas de seguridad, como la validación de los datos entrantes. El uso de atributos como [FromHeader], [FromForm], o [FromBody] facilita este proceso, pero también debe entenderse cuándo y cómo aplicar cada uno de ellos.

Además, si bien las soluciones mostradas aquí permiten trabajar con parámetros de una manera más avanzada, siempre se debe considerar la complejidad que puede agregar esta flexibilidad. El uso de enlaces personalizados no es algo que se deba hacer sin necesidad, ya que puede complicar la arquitectura y el mantenimiento de la API si no se maneja con cuidado.

La implementación de middleware, como se discutió en el capítulo anterior, también puede jugar un papel importante en la personalización del flujo de datos a través de la API, permitiendo aplicar transformaciones o validaciones antes de que los datos lleguen a los controladores o puntos finales específicos.

¿Cómo se implementa eficazmente el almacenamiento en caché distribuido con Redis en aplicaciones .NET modernas?

La configuración de una caché distribuida en una aplicación moderna basada en .NET permite una mejora sustancial en el rendimiento y la escalabilidad del sistema, especialmente en arquitecturas de alto tráfico. A diferencia de la caché en memoria tradicional, que reside localmente en el servidor web, la caché distribuida se aloja en máquinas remotas o servicios en la nube, como Microsoft Azure. Esta separación física garantiza mayor fiabilidad, persistencia y sincronización entre múltiples instancias de una misma aplicación desplegada en clústeres.

Redis se ha consolidado como la base de datos de caché distribuida más poderosa y extendida, gracias a su arquitectura en memoria, su bajo tiempo de latencia y su compatibilidad con múltiples lenguajes y plataformas. La implementación del patrón Decorador para integrar Redis dentro de la lógica de negocio de un servicio es una práctica recomendada que preserva el principio de responsabilidad única, permite el desacoplamiento de las funcionalidades y mejora la mantenibilidad del código.

La clase DistributedCachedCountryService actúa como envoltorio del servicio principal ICountryService. Su responsabilidad es interceptar la solicitud para verificar si la información requerida ya está almacenada en la caché Redis, y en tal caso, retornarla directamente desde allí, evitando una consulta redundante a la fuente de datos. Para ello, se genera una clave única basada en los parámetros de paginación, como countries-1-10, y se utiliza como identificador dentro de Redis. Si el valor no está presente, se delega la solicitud al servicio subyacente, se serializa el resultado con System.Text.Json, y se almacena utilizando SetStringAsync, con una expiración relativa definida por DistributedCacheEntryOptions.

El uso de GetStringAsync y SetStringAsync garantiza la compatibilidad con estructuras de datos complejas, aunque requiere la serialización explícita, lo que añade una ligera sobrecarga que se justifica ampliamente por la eficiencia en la recuperación posterior. La expiración automática del contenido en la caché es una funcionalidad esencial que evita la obsolescencia de los datos, permitiendo mantener la coherencia sin intervención manual.

La configuración del servicio Redis se realiza mediante la extensión AddStackExchangeRedisCache, donde se establece la cadena de conexión —obtenida del archivo appsettings.json— y un InstanceName, que se antepone a las claves para evitar colisiones en contextos multiaplicación. Esta instancia nominal no representa una instancia lógica dentro de Redis, sino una convención para la gestión de nombres de claves. En entornos compartidos, una mala gestión de estas claves puede provocar inconsistencias si dos aplicaciones diferentes usan las mismas claves para contenidos incompatibles.

Por tanto, se recomienda una política rigurosa de nomenclatura para evitar colisiones entre dominios lógicos distintos, especialmente en arquitecturas microservicio. A su vez, la serialización y deserialización de objetos deben manejarse cuidadosamente, con especial atención a la evolución del contrato de datos, para evitar errores al interpretar información antigua almacenada en la caché.

Una de las claves del éxito en esta implementación radica en mantener un equilibrio entre la duración del almacenamiento en caché y la necesidad de actualizar los datos. Un valor demasiado alto puede retornar información obsoleta, mientras que un valor demasiado bajo reduce los beneficios del almacenamiento en caché al obligar a consultar frecuentemente la fuente original.

Al emplear el patrón Decorador junto con Redis, la arquitectura conserva la claridad y la extensibilidad. Es posible, por ejemplo, añadir fácilmente métricas, registros o mecanismos de fallback sin alterar el núcleo del servicio principal. Además, esta estrategia puede replicarse en otros servicios de dominio, creando una arquitectura homogénea y robusta.

Es fundamental que el desarrollador comprenda que la caché distribuida no debe ser tratada como fuente de verdad, sino como un mecanismo de optimización. Redis debe siempre reflejar una representación transitoria del estado del sistema, no un reemplazo del almacenamiento persistente. En consecuencia, debe diseñarse con tolerancia a fallos: si Redis no está disponible, la aplicación debe continuar funcionando, aunque con menor rendimiento.

Comprender el ciclo de vida del contenido almacenado en Redis, sus implicaciones en la consistencia eventual, y los patrones de invalidación adecuados, forma parte del dominio técnico necesario para diseñar sistemas verdaderamente resilientes y eficientes.

¿Cómo realizar pruebas unitarias en APIs con xUnit y otros paquetes esenciales?

En el proceso de desarrollo de una API, la prueba de las funciones clave es esencial para asegurar su correcto funcionamiento. Utilizando herramientas como xUnit y varios paquetes auxiliares, es posible automatizar la comprobación del comportamiento de las funciones dentro de nuestra solución. A continuación, exploraremos cómo implementar una prueba unitaria eficaz para un endpoint básico de una API, usando el ejemplo del endpoint GET /countries.

Para comenzar, es necesario entender qué herramientas nos acompañarán en esta tarea. En primer lugar, necesitamos un conjunto de bibliotecas que faciliten las pruebas. Estas son algunas de las más comunes:

  • xUnit: Este paquete es esencial para realizar pruebas unitarias en .NET. Si no se incluye, Visual Studio no podrá detectar las pruebas.

  • NSubstitute: Se trata de una biblioteca de mocking que permite simular las dependencias de las clases a probar.

  • AutoFixture: Esta herramienta permite generar datos falsos de manera rápida para poblar las propiedades de los objetos de prueba.

  • ExpectedObjects: Facilita la comparación de objetos por su valor en lugar de por su referencia, lo cual resulta ser muy útil a la hora de comprobar que los resultados son correctos.

Con estos paquetes en su lugar, podemos comenzar a implementar la prueba de un endpoint básico de nuestra API.

El SUT: el ejemplo de GET /countries

Vamos a tomar como ejemplo el endpoint GET /countries, que hemos aislado en una función estática GetCountries dentro de la clase CountryEndpoints. El propósito de esta prueba es verificar el comportamiento de este endpoint en diversas situaciones, en particular cuando los parámetros de consulta pageIndex y pageSize son nulos.

La función GetCountries se encarga de recibir estos parámetros, realizar una consulta al servicio countryService y devolver los resultados mapeados por el servicio mapper. La idea es realizar varias pruebas para asegurarnos de que la función no solo devuelve los datos correctos, sino que también maneja adecuadamente los valores predeterminados para los parámetros nulos.

Identificando qué probar

Lo primero que debemos hacer es identificar los comportamientos que deseamos comprobar. En este caso, hay cuatro posibles combinaciones para los parámetros pageIndex y pageSize (ambos nulos, solo uno nulo, y ambos con valores asignados). Como ejemplo, nos centraremos en el caso en el que ambos parámetros son nulos.

A continuación, algunas de las comprobaciones clave a realizar durante la prueba de la función GetCountries:

  1. Verificar que la función devuelve un resultado de tipo Ok con una lista de objetos CountryDto devuelta por el método Map.

  2. Comprobar que, cuando los parámetros pageIndex y pageSize son nulos, se asignan correctamente los valores predeterminados (1 y 10, respectivamente).

  3. Asegurarnos de que el método GetAllAsync del servicio countryService recibe un objeto PagingDto con los valores correctos de PageIndex y PageSize.

  4. Comprobar que el método Map recibe la lista de países generada por el método GetAllAsync como parámetro.

El objetivo no es solo comprobar que la función devuelve los datos correctos, sino también asegurar que los parámetros se gestionan correctamente, ya que muchos errores ocurren cuando los valores de los parámetros no se manejan adecuadamente.

Creando la clase de prueba

La clase de prueba debe estar organizada de manera clara, indicando el nombre del SUT (System Under Test) que estamos probando. En este caso, el nombre de la clase será GetCountriesTests, y estará ubicada en una carpeta correspondiente al nombre de la clase que estamos probando: CountriesTests.

El esqueleto de la clase de prueba, usando xUnit, tiene el siguiente aspecto:

csharp
public class GetCountriesTests
{ private readonly ICountryMapper _countryMapper; private readonly ICountryService _countryService; private readonly Fixture _fixture; public GetCountriesTests() { _countryMapper = Substitute.For<ICountryMapper>(); _countryService = Substitute.For<ICountryService>(); _fixture = new Fixture(); } [Fact] public async Task WhenGetCountriesReceivesNullPagingParameters_ShouldReturnDefaultPagingValuesAndCountries() { // Arrange // Act // Assert } }

En este caso, el método de prueba está decorado con el atributo [Fact], que le indica a xUnit que se trata de un test. El nombre del método sigue la convención When{condition}_Should{expectedBehavior}, lo que facilita la comprensión de lo que está probando el test.

Implementación de la prueba

La siguiente implementación muestra cómo podemos estructurar nuestra prueba para el caso donde ambos parámetros (pageIndex y pageSize) son nulos. Primero, definimos los valores esperados para los parámetros de paginación, y luego simulamos los comportamientos de los servicios countryService y countryMapper usando las herramientas adecuadas.

csharp
[Fact]
public async Task WhenGetCountriesReceivesNullPagingParameters_ShouldReturnDefaultPagingValuesAndCountries() { // Arrange int? pageIndex = null; int? pageSize = null; var expectedPaging = new PagingDto { PageIndex = 1, PageSize = 10 }.ToExpectedObject(); var countries = _fixture.CreateMany<CountryDto>(2).ToList(); var expectedCountries = countries.ToExpectedObject(); var mappedCountries = _fixture.CreateMany<CountryDto>(2).ToList(); var expectedMappedCountries = mappedCountries.ToExpectedObject(); _countryService.GetAllAsync(Arg.Any<PagingDto>()).Returns(countries); _countryMapper.Map(Arg.Any<IEnumerable<CountryDto>>()).Returns(mappedCountries); // Act var result = await CountryEndpoints.GetCountries(pageIndex, pageSize, _countryMapper, _countryService); // Assert result.As<IResult>().StatusCode.ShouldEqual(200); result.As<IResult>().Value.ShouldEqual(expectedMappedCountries); expectedPaging.ShouldEqual(result.As<IResult>().Value); }

Aquí, hemos definido las expectativas de comportamiento para los servicios involucrados y luego realizamos las comprobaciones correspondientes en la sección de aserciones. Es importante recordar que las pruebas unitarias deben ser fáciles de leer, por lo que cada parte del proceso (Arrange, Act, Assert) debe estar claramente definida.

Consideraciones adicionales

Además de realizar pruebas sobre el comportamiento esperado, es fundamental tener en cuenta otros aspectos en las pruebas unitarias. Por ejemplo, las pruebas deben asegurar no solo la correcta manipulación de parámetros y resultados, sino también la cobertura de excepciones y la respuesta ante casos inesperados. La correcta configuración de los datos y la validación de que las dependencias se comportan de acuerdo con lo esperado son aspectos igualmente importantes.