← maurobernal.com.ar

Etiqueta: CI/CD

  • La CLI de .NET: Guía Completa de sus 7 Capacidades Principales

    Hay algo que aprendí trabajando con desarrolladores que vienen de otros ecosistemas: subestiman la CLI de .NET. La ven como un atajo para no abrir Visual Studio, pero en realidad es la columna vertebral de todo el toolchain de .NET moderno. Los pipelines de CI/CD, los contenedores, los scripts de automatización — todo pasa por acá. En este artículo repaso las siete capacidades principales con ejemplos reales, desde crear una solución hasta monitorear un proceso en producción.

    Un poco de contexto

    Antes de que la CLI existiera en su forma actual, el ecosistema .NET dependía fuertemente de Visual Studio y de MSBuild estructurado para Windows. Eso generaba fragmentación: el desarrollador trabajaba en Windows con el IDE, el servidor de CI corría en Linux, y los scripts de deploy eran una mezcla de PowerShell y batch que nadie quería tocar. La CLI resolvió eso siendo un frontend unificado que orquesta MSBuild, NuGet y VSTest de manera consistente en cualquier plataforma.

    1. Bootstrapping: dotnet new y dotnet sln

    El punto de entrada de cualquier proyecto. dotnet new no es solo un generador de archivos — es un motor de plantillas que elimina el boilerplate inicial y estandariza la estructura de proyectos entre el equipo. dotnet sln maneja la interconexión de proyectos dentro de una solución, algo que antes requería editar XML a mano o depender del IDE.

    # Crear una solución en blanco
    dotnet new sln -n MiSistemaEnterprise
    
    # Crear API y proyecto de pruebas
    dotnet new webapi -n MiSistemaEnterprise.Api
    dotnet new xunit -n MiSistemaEnterprise.Tests
    
    # Vincular ambos a la solución
    dotnet sln add ./MiSistemaEnterprise.Api/MiSistemaEnterprise.Api.csproj
    dotnet sln add ./MiSistemaEnterprise.Tests/MiSistemaEnterprise.Tests.csproj
    
    # Referenciar la API desde el proyecto de pruebas
    dotnet add ./MiSistemaEnterprise.Tests/MiSistemaEnterprise.Tests.csproj \
      reference ./MiSistemaEnterprise.Api/MiSistemaEnterprise.Api.csproj

    Esto es exactamente lo que corro cuando arranco un proyecto nuevo. Cinco comandos y tengo una solución con API, pruebas y referencias configuradas, sin abrir ningún IDE.

    2. Gestión de Dependencias: dotnet add package

    La interfaz de NuGet desde la terminal. Modifica el .csproj, descarga los binarios a la caché global y resuelve el árbol completo de dependencias transitivas. En pipelines de CI/CD esto es fundamental — no hay interfaz gráfica disponible, y este comando funciona igual en cualquier agente.

    cd MiSistemaEnterprise.Api
    
    # Instalar Polly para resiliencia con versión específica
    dotnet add package Polly --version 8.3.0
    
    # Ver todas las dependencias instaladas
    dotnet list package
    
    # Eliminar un paquete
    dotnet remove package Polly

    El --version explícito es importante en proyectos serios: evita que una actualización automática rompa algo en producción sin que nadie lo vea venir.

    3. Compilación: dotnet build

    dotnet build es la abstracción sobre MSBuild. Ejecuta automáticamente el restore de dependencias faltantes y compila el código a IL (Intermediate Language), generando los .dll listos para el runtime. El flag --nologo puede parecer cosmético, pero en logs de CI hace una diferencia real en la legibilidad.

    # Compilar toda la solución en modo Release
    dotnet build ../MiSistemaEnterprise.sln --configuration Release --nologo
    
    # Limpiar artefactos anteriores (bin/ y obj/)
    dotnet clean

    4. Desarrollo Activo: dotnet watch

    Esta es la que más impacto tiene en el día a día. Antes de dotnet watch, el ciclo era: guardar → detener el proceso → recompilar → reiniciar → probar. Repetido decenas de veces por hora. Con dotnet watch y Hot Reload, los cambios se reinyectan directamente en el proceso en ejecución sin perder el estado de la aplicación.

    # Ejecutar la API en modo de observación
    # Cualquier cambio guardado en un .cs se aplica instantáneamente
    dotnet watch run --project ./MiSistemaEnterprise.Api.csproj

    El ahorro de tiempo es real. En una jornada de desarrollo intenso, esos segundos por ciclo se acumulan en horas.

    5. Pruebas Automatizadas: dotnet test

    El runner de pruebas nativo de la CLI. Compatible con xUnit, NUnit y MSTest sin configuración adicional. Lo importante para CI/CD es la combinación de exportación en formato TRX (compatible con Azure DevOps, Jenkins) y la recolección de cobertura con Coverlet, que genera reportes que podés publicar directamente en el pipeline.

    # Ejecutar pruebas con reporte TRX y cobertura de código
    dotnet test ./MiSistemaEnterprise.Tests.csproj \
      --logger "trx;LogFileName=resultados.trx" \
      --collect:"XPlat Code Coverage"

    6. Publicación y Despliegue: dotnet publish

    El comando que resuelve el clásico «funciona en mi máquina». Empaqueta la aplicación, sus dependencias y opcionalmente el runtime de .NET en un artefacto desplegable. Hay dos modos principales:

    • Framework-Dependent: requiere .NET instalado en el servidor. Más liviano.
    • Self-Contained: incluye el runtime. Más pesado, pero sin dependencias externas en el servidor.

    Con AOT (Ahead-of-Time), podés compilar directamente a código nativo de la plataforma, eliminando el runtime por completo.

    # Publicar como ejecutable autocontenido para Linux x64
    # Un único binario, con trimming para reducir el tamaño
    dotnet publish ./MiSistemaEnterprise.Api.csproj \
      --configuration Release \
      --runtime linux-x64 \
      --self-contained true \
      /p:PublishSingleFile=true \
      /p:PublishTrimmed=true

    En proyectos con contenedores, este comando va directo al Dockerfile: compilás con dotnet publish en la etapa de build y copiás el binario resultante a la imagen final. Imagen más pequeña, arranque más rápido.

    7. Diagnóstico en Producción: dotnet tools

    La capacidad menos conocida y la más poderosa cuando algo falla en producción. Las herramientas globales de .NET permiten monitorear y perfilar procesos en vivo, incluso en contenedores Linux, sin necesidad de instalar agentes externos ni reinicios.

    Las tres que más uso:

    • dotnet-counters: métricas en tiempo real (CPU, GC, ThreadPool, peticiones por segundo)
    • dotnet-trace: trazas de CPU para identificar cuellos de botella
    • dotnet-dump: volcados de memoria para analizar fugas o crashes
    # Instalar la herramienta globalmente (una sola vez)
    dotnet tool install --global dotnet-counters
    
    # Listar procesos .NET en ejecución para obtener el PID
    dotnet-counters ps
    
    # Monitorear métricas del runtime en tiempo real para el proceso 1234
    dotnet-counters monitor -p 1234 --counters System.Runtime

    Tuve un caso donde la memoria de un servicio crecía progresivamente en producción sin un patrón claro. Con dotnet-counters identifiqué que las colecciones Gen2 del GC eran anormalmente frecuentes. Con dotnet-dump capturé el heap y encontré el objeto que se estaba reteniendo. Todo sin reiniciar el servicio ni afectar a los usuarios. Eso no tiene precio.

    Resumen

    La CLI de .NET no es un accesorio — es la herramienta central alrededor de la cual gira todo el ciclo de vida de una aplicación .NET moderna. Desde el primer dotnet new hasta el dotnet-counters monitor en producción, cada etapa tiene su comando y su propósito.

    Si venís de un entorno donde todo pasaba por el IDE, mi recomendación es dedicarle una tarde a explorar estos comandos en un proyecto de prueba. La inversión se recupera rápido, especialmente en el momento en que tenés que diagnosticar algo en un servidor Linux sin interfaz gráfica.

    ¿Usás alguna herramienta de diagnóstico de la CLI que no esté en esta lista? Dejalo en los comentarios.

  • Feature Management en .NET (Parte 2): Filtros Dinámicos, Hot Reload y Feature Flags vs. Versionado

    En el post anterior vimos los conceptos base de Feature Flags con Microsoft.FeatureManagement. Acá vamos a profundizar en tres capacidades que marcan la diferencia entre usar flags como simples condicionales y usarlos como una herramienta real de arquitectura: filtros dinámicos con Minimal APIs, recarga en caliente como Kill Switch, y la diferencia conceptual con el versionado de APIs. También incluyo la comparativa completa entre ambos enfoques, que es algo que me preguntan frecuentemente.

    Feature Flags Estáticas en Minimal APIs

    El caso más directo: un interruptor booleano evaluado en tiempo de ejecución. Lo interesante con Minimal APIs es que la integración es aún más limpia que en controllers tradicionales — IFeatureManager se inyecta directamente en el handler del endpoint.

    // appsettings.json
    {
      "FeatureManagement": {
        "NewCheckout": false
      }
    }
    
    // Program.cs
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddFeatureManagement();
    var app = builder.Build();
    
    app.MapGet("/checkout", async (IFeatureManager fm) => 
    {
        if (await fm.IsEnabledAsync("NewCheckout"))
        {
            return Results.Ok("Procesando con el NUEVO flujo de pago.");
        }
        
        return Results.Ok("Procesando con el flujo de pago CLÁSICO.");
    });
    
    app.Run();

    La evaluación ocurre en cada petición. Cambiar false por true en el archivo de configuración actualiza el comportamiento sin reiniciar el proceso.

    Filtros Dinámicos: Canary Releases y Pruebas A/B

    Los flags booleanos globales resuelven muchos casos, pero en producción real necesitás más granularidad. Los Feature Filters permiten activar características basándose en reglas contextuales evaluadas en cada petición. .NET incluye dos filtros nativos listos para usar:

    • PercentageFilter: activa el flag para un porcentaje del tráfico. Ideal para Canary releases.
    • TimeWindowFilter: activa el flag solo durante una ventana temporal. Ideal para lanzamientos programados o mantenimientos.

    Ejemplo con PercentageFilter + FeatureGate en un controller:

    // appsettings.json — 50% del tráfico ve el motor Beta
    {
      "FeatureManagement": {
        "BetaSearch": {
          "EnabledFor": [
            {
              "Name": "Percentage",
              "Parameters": { "Value": 50 }
            }
          ]
        }
      }
    }
    
    // Program.cs — registrar el filtro
    builder.Services.AddFeatureManagement()
        .AddFeatureFilter<PercentageFilter>();
    
    // SearchController.cs
    [ApiController]
    [Route("api/[controller]")]
    public class SearchController : ControllerBase
    {
        [HttpGet]
        [FeatureGate("BetaSearch")] // 404 automático para el 50% que no aplica
        public IActionResult Get()
        {
            return Ok("Resultados del motor de búsqueda Beta.");
        }
    }

    Lo que me gusta de este enfoque es que el [FeatureGate] hace que el endpoint directamente no exista para quien no aplica al filtro. No hay lógica de «si no entra acá, redirigí allá» — el framework lo maneja solo.

    Hot Reload y Kill Switches: El Poder Real de los Feature Flags

    Esta es la capacidad que más valoro en producción. El sistema de Feature Management se integra con IOptionsSnapshot, el mecanismo reactivo de configuración de .NET. Cuando modificás la fuente de configuración — ya sea el appsettings.json, una variable de entorno o Azure App Configuration — el estado del flag se actualiza en memoria automáticamente en la siguiente petición. Sin reinicio, sin downtime.

    El caso de uso más crítico: el Kill Switch. Si una feature nueva empieza a generar errores en producción, en lugar de hacer rollback del deploy entero (proceso que puede tomar minutos), simplemente cambiás el flag a false y la próxima petición ya usa el código anterior.

    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddFeatureManagement();
    var app = builder.Build();
    
    app.MapGet("/health", async (IFeatureManager fm) => 
    {
        // Un operador cambia "MaintenanceMode" a true en appsettings.json.
        // La SIGUIENTE petición refleja el cambio al instante, sin reiniciar el proceso.
        bool isMaintenance = await fm.IsEnabledAsync("MaintenanceMode");
        
        if (isMaintenance)
        {
            return Results.StatusCode(503); // Service Unavailable
        }
        
        return Results.Ok("Sistema Operativo");
    });
    
    app.Run();

    En proyectos donde integramos con Azure App Configuration, esto va un paso más allá: podés cambiar el flag desde un panel web, sin acceso al servidor, y el efecto es inmediato. Para equipos de guardia nocturna o sistemas de alta disponibilidad, esto es invaluable.

    Feature Flags vs. Versionado de API: La Confusión Más Frecuente

    Es la pregunta que más aparece cuando presento este tema. Son herramientas distintas para problemas distintos, aunque superficialmente parecen hacer lo mismo (exponer diferentes comportamientos según contexto).

    CriterioFeature FlagsVersionado de API
    Problema que resuelveExposición controlada: gestiona quién y cuándo accede a un cambioCompatibilidad de contratos: clientes antiguos no se rompen ante cambios destructivos
    Tiempo de vidaEfímero. Una vez estable, la flag y el código viejo deben eliminarse (deuda técnica)Prolongado. v1 y v2 coexisten por años
    Mecanismo de controlConfiguración externa, reglas de negocio, evaluación en runtimeRutas estáticas (/api/v1/), headers HTTP o referencias de ensamblado
    InfraestructuraUn solo binario con todos los caminos lógicosPueden ser múltiples binarios o deploys paralelos

    La regla que uso: si el cambio es interno y temporal (lo vas a limpiar cuando sea estable), usá Feature Flags. Si el cambio rompe el contrato público con consumidores externos que necesitan tiempo para migrar, usá versionado. En proyectos complejos, ambos conviven sin problema.

    Próximos pasos

    Estos tres patrones cubren la gran mayoría de los casos de uso cotidianos. El siguiente nivel son los Custom Feature Filters — filtros personalizados que habilitan flags según el tenant de la base de datos, el rol del usuario, la región geográfica o cualquier lógica de negocio propia. Y después de eso, la integración con Azure App Configuration para centralizar todos los flags en un servicio externo con targeting por usuario o segmento.

    ¿Te interesa alguno de esos dos temas para el próximo artículo? Dejalo en los comentarios.

  • Feature Flags en .NET: Separando el Deploy del Release con Microsoft.FeatureManagement

    Uno de los cambios más importantes que tuve en mi forma de trabajar en los últimos años fue aprender a separar el despliegue del lanzamiento. Hasta ese momento, cada vez que terminaba una feature la subía a producción directamente. Si algo salía mal, había que hacer rollback de todo. Con los Feature Flags, eso cambió completamente: el código puede estar en producción, pero inactivo. Lo activás cuando querés, para quien querés, sin tocar el deploy. En .NET esto se implementa con Microsoft.FeatureManagement, y en este artículo te muestro cómo funciona.

    ¿Qué son los Feature Flags y por qué importan?

    Los Feature Flags (también llamados Feature Toggles) son interruptores que habilitan o deshabilitan funcionalidades en tiempo de ejecución. Su valor principal no es técnico sino estratégico: permiten que el equipo de desarrollo haga merge continuo a la rama principal (Trunk-based development) sin bloquear a nadie, mientras el código de la nueva feature espera desactivado hasta que esté listo para salir.

    Esto los diferencia del versionado de APIs, que apunta a mantener contratos estables para distintos consumidores. Los Feature Flags apuntan a estrategias de CI/CD: lanzamientos progresivos (Canary releases), pruebas A/B y acceso gradual a nuevas funcionalidades. Y lo más importante: su estado se modifica en caliente, cambiando el appsettings.json o variables de entorno, sin recompilar ni redesplegar.

    1. Evaluación Básica: El Interruptor On/Off

    La forma más simple de Feature Management. Un flag booleano que habilita o deshabilita una ruta de código. Existe principalmente para resolver el problema de los feature branches de larga duración: en lugar de mantener una rama separada durante semanas y sufrir un merge doloroso, el código convive en la rama principal desactivado, esperando su momento.

    Gracias a la recarga dinámica de IConfiguration en .NET, cambiar el valor en el archivo de configuración actualiza el comportamiento de la app instantáneamente, sin reiniciarla.

    Configuración en appsettings.json:

    {
      "FeatureManagement": {
        "NewPaymentGateway": false
      }
    }

    Implementación en C#:

    using Microsoft.FeatureManagement;
    
    // Registro en Program.cs:
    // builder.Services.AddFeatureManagement();
    
    public class PaymentService
    {
        private readonly IFeatureManager _featureManager;
    
        public PaymentService(IFeatureManager featureManager)
        {
            _featureManager = featureManager;
        }
    
        public async Task ProcessPaymentAsync(Order order)
        {
            // Se evalúa en tiempo de ejecución.
            // Cambiar el flag en appsettings.json actualiza este comportamiento sin reiniciar la app.
            if (await _featureManager.IsEnabledAsync("NewPaymentGateway"))
            {
                await ProcessWithStripeAsync(order); // Nueva pasarela
            }
            else
            {
                await ProcessWithLegacySystemAsync(order); // Sistema anterior
            }
        }
    }

    La primera vez que usé esto en un proyecto real, cambié el comportamiento de la pasarela de pago en producción cambiando un false por true en un archivo de configuración. Sin deploy, sin downtime, sin nervios.

    2. FeatureGate: Bloqueando Endpoints Completos

    A veces no alcanza con un condicional dentro de la lógica de negocio. Si la feature expone un endpoint nuevo, queremos que ese endpoint directamente no exista mientras el flag esté apagado. El atributo [FeatureGate] intercepta la petición HTTP en el pipeline de ASP.NET Core y devuelve automáticamente un 404 Not Found si el flag está desactivado. La lógica de negocio queda limpia, sin condicionales.

    using Microsoft.AspNetCore.Mvc;
    using Microsoft.FeatureManagement.Mvc;
    
    [ApiController]
    [Route("api/[controller]")]
    public class ReportsController : ControllerBase
    {
        // Este endpoint solo existe si "AdvancedReporting" es true.
        // Si está apagado, responde 404 automáticamente.
        [HttpGet("advanced")]
        [FeatureGate("AdvancedReporting")]
        public IActionResult GetAdvancedReport()
        {
            return Ok(new { Message = "Reporte avanzado generado exitosamente." });
        }
    }

    Útil para exponer funcionalidades premium, features en beta o endpoints que todavía están en desarrollo pero ya mergeados a main.

    3. Feature Filters: Reglas Dinámicas y Canary Releases

    Los flags booleanos globales son el punto de partida, pero las situaciones reales suelen ser más complejas. ¿Qué pasa si querés activar una feature solo para el 10% del tráfico? ¿O solo durante ciertos horarios? ¿O solo para usuarios internos? Ahí entran los Feature Filters.

    El más directo para Canary releases es el filtro de porcentaje. En lugar de un booleano, definís una regla:

    {
      "FeatureManagement": {
        "RedesignedUI": {
          "EnabledFor": [
            {
              "Name": "Percentage",
              "Parameters": {
                "Value": 25
              }
            }
          ]
        }
      }
    }

    El 25% de las peticiones verán la nueva UI. El 75% restante, la versión anterior. Sin tocar el código.

    using Microsoft.FeatureManagement;
    using Microsoft.FeatureManagement.FeatureFilters;
    
    // Registro en Program.cs incluyendo el filtro:
    // builder.Services.AddFeatureManagement().AddFeatureFilter<PercentageFilter>();
    
    public class UIController : Controller
    {
        private readonly IFeatureManager _featureManager;
    
        public UIController(IFeatureManager featureManager)
        {
            _featureManager = featureManager;
        }
    
        public async Task<IActionResult> Index()
        {
            if (await _featureManager.IsEnabledAsync("RedesignedUI"))
            {
                return View("Index_v2"); // 25% del tráfico
            }
    
            return View("Index_v1"); // 75% del tráfico
        }
    }

    Con este patrón podés hacer un lanzamiento progresivo real: empezás con el 5%, observás métricas, subís a 25%, luego a 50%, y cuando estás seguro activás al 100%. Todo sin un solo redeploy.

    ¿Cuándo usar Feature Flags vs. versionado de API?

    Es una confusión frecuente. La regla práctica que uso:

    • Feature Flags → cuando el cambio es interno o progresivo y querés controlar cuándo se activa en producción sin cambiar el contrato público de la API.
    • Versionado de API → cuando el cambio rompe el contrato y tenés consumidores externos que necesitan tiempo para adaptarse.

    No son excluyentes: podés usar ambos en el mismo proyecto para propósitos distintos.

    Próximos pasos

    Lo que vimos acá es la base. El ecosistema de Microsoft.FeatureManagement va más lejos:

    • Custom Feature Filters: habilitar features según el Tenant de la base de datos, el rol del usuario o cualquier lógica de negocio propia.
    • Azure App Configuration: centralizar todos los flags en un servicio externo, modificarlos desde un panel sin tocar archivos de configuración y con soporte para targeting (activar por usuario específico, grupo o región).

    ¿Querés que profundice en alguno de estos dos temas en un próximo artículo? Dejalo en los comentarios.

Tags

tsql (27)mssql (26)devops (21)sql (20)dotnet (18)docker (16)performance (14)contenedores (11)dotnet10 (10)linux (9)csharp (8)microservicios (8)angular (8)angular21 (7)sql server (6)issabel (6)kubernetes (6)docker-compose (6)typescript (6)aot (6)