← maurobernal.com.ar

Etiqueta: sse

  • Arquitectura de Web APIs en .NET 10 — Parte 3: Observabilidad, Resiliencia y Tiempo Real

    Llegamos a la tercera y última parte de la serie Arquitectura Esencial de Web APIs en .NET 10. En la Parte 1 vimos los fundamentos, en la Parte 2 los componentes de performance y escalabilidad. Ahora cerramos con los cuatro que elevan una API de «funcional» a «profesional»: observabilidad, resiliencia, despliegue progresivo y comunicación en tiempo real.

    8. OpenTelemetry: ver lo que pasa adentro

    Tuve años en los que mi «monitoreo» consistía en revisar logs manualmente y rezar para encontrar el error antes de que me llamara el cliente. Hoy, con OpenTelemetry integrado nativamente en .NET 10, no hay excusa para no tener observabilidad completa desde el día uno.

    OpenTelemetry unifica tres pilares de observabilidad:

    • Trazas distribuidas: seguir el flujo de una request a través de múltiples servicios
    • Métricas: CPU, memoria, requests por segundo, latencia
    • Logs: correlacionados automáticamente con las trazas
    builder.Services.AddOpenTelemetry()
        .WithMetrics(metrics => metrics
            .AddAspNetCoreInstrumentation()   // Métricas de HTTP
            .AddRuntimeInstrumentation()      // GC, threadpool, memoria
            .AddPrometheusExporter())         // Expone /metrics para Prometheus
        .WithTracing(tracing => tracing
            .AddAspNetCoreInstrumentation()   // Trazas de requests HTTP
            .AddHttpClientInstrumentation()   // Trazas de llamadas salientes
            .AddEntityFrameworkCoreInstrumentation() // Trazas de queries SQL
            .AddOtlpExporter(otlp =>          // Exporta a Jaeger, Grafana Tempo, etc.
            {
                otlp.Endpoint = new Uri("http://localhost:4317");
            }));
    
    // Opcional: exponer el endpoint de métricas de Prometheus
    app.MapPrometheusScrapingEndpoint("/metrics");

    Una vez que tenés OpenTelemetry configurado, herramientas como Grafana + Prometheus + Tempo te dan dashboards completos, alertas automáticas y trazas distribuidas entre microservicios. Todo lo que necesitás para diagnosticar cualquier problema en producción en minutos en lugar de horas.

    9. Resiliencia con Polly: sobrevivir a las caídas externas

    En arquitecturas de microservicios hay una certeza: el servicio que llamás va a fallar en algún momento. No es una posibilidad, es una garantía estadística. La pregunta es si tu API se cae con él o lo maneja con elegancia.

    Polly —integrado de forma estándar en .NET desde la versión 8— implementa patrones de resiliencia: reintentos con backoff exponencial, timeouts y circuit breakers.

    // Opción 1: Pipeline estándar preconfigurado (la manera más rápida)
    // Incluye: reintentos exponenciales + circuit breaker + timeout + hedging
    builder.Services.AddHttpClient("ExternalAPI", client =>
        client.BaseAddress = new Uri("https://api.externa.com"))
        .AddStandardResilienceHandler();
    
    // Opción 2: Pipeline personalizado para control fino
    builder.Services.AddHttpClient("CriticalService", client =>
        client.BaseAddress = new Uri("https://servicio-critico.com"))
        .AddResilienceHandler("custom", pipeline =>
        {
            // Timeout total de la operación
            pipeline.AddTimeout(TimeSpan.FromSeconds(10));
    
            // Reintentos: 3 intentos con backoff exponencial
            pipeline.AddRetry(new HttpRetryStrategyOptions
            {
                MaxRetryAttempts = 3,
                Delay = TimeSpan.FromMilliseconds(500),
                BackoffType = DelayBackoffType.Exponential,
                UseJitter = true, // Evita thundering herd
                ShouldHandle = args => args.Outcome switch
                {
                    { Exception: HttpRequestException } => PredicateResult.True(),
                    { Result.StatusCode: HttpStatusCode.ServiceUnavailable } => PredicateResult.True(),
                    _ => PredicateResult.False()
                }
            });
    
            // Circuit Breaker: abre después de 50% de fallos en 30 segundos
            pipeline.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
            {
                FailureRatio = 0.5,
                SamplingDuration = TimeSpan.FromSeconds(30),
                MinimumThroughput = 10,
                BreakDuration = TimeSpan.FromSeconds(15)
            });
        });

    El Circuit Breaker es especialmente importante: cuando un servicio externo falla repetidamente, el circuit breaker «abre» y deja de intentar llamarlo durante un período, evitando que los timeouts se acumulen y degraden toda tu API.

    10. Feature Flags: deployar sin activar

    Los feature flags cambiaron mi forma de deployar. Antes, cada feature nueva era un riesgo: si algo fallaba en producción, había que hacer rollback, lo cual es lento y estresante. Hoy puedo subir código a producción con la funcionalidad apagada, activarla para el 5% de los usuarios, monitorear, y si todo va bien, activarla para todos. Sin rollback, sin estrés.

    dotnet add package Microsoft.FeatureManagement.AspNetCore
    builder.Services.AddFeatureManagement();
    
    app.MapGet("/api/beta-feature", async (IFeatureManager featureManager) =>
    {
        if (await featureManager.IsEnabledAsync("BetaFeatureX"))
            return Results.Ok("¡Nueva funcionalidad activada!");
    
        return Results.StatusCode(StatusCodes.Status404NotFound);
    });
    
    // También podés proteger endpoints completos con filtros de acción
    app.MapGet("/api/new-algorithm", [FeatureGate("NewAlgorithm")] () =>
        Results.Ok(RunNewAlgorithm()));

    Los flags se configuran en appsettings.json (para desarrollo) o en Azure App Configuration (para producción con control en tiempo real sin redeploy):

    {
      "FeatureManagement": {
        "BetaFeatureX": false,
        "NewAlgorithm": {
          "EnabledFor": [
            {
              "Name": "Percentage",
              "Parameters": { "Value": 10 }
            }
          ]
        }
      }
    }

    Podés activar features para un porcentaje de usuarios, para usuarios específicos, por fecha de activación, o con cualquier lógica custom. Ideal para Canary Releases y pruebas A/B.

    11. Server-Sent Events (SSE): tiempo real sin la complejidad de WebSockets

    WebSockets es la primera opción que se te ocurre para «tiempo real», pero tiene overhead de setup y complejidad de manejo de conexiones bidireccionales. Si solo necesitás enviar datos del servidor al cliente —notificaciones, progreso de tareas, feeds de datos— SSE es mucho más simple y funciona sobre HTTP estándar.

    En .NET 10, implementarlo con IAsyncEnumerable es elegante y eficiente:

    // Ejemplo 1: Stream simple de notificaciones
    app.MapGet("/api/notifications", async IAsyncEnumerable<string> (CancellationToken ct) =>
    {
        for (int i = 0; i < 5 && !ct.IsCancellationRequested; i++)
        {
            await Task.Delay(1000, ct);
            yield return $"data: Notificación {i + 1}\n\n";
        }
    });
    
    // Ejemplo 2: Progreso de una tarea larga
    app.MapPost("/api/export", async IAsyncEnumerable<ProgressUpdate> (ExportRequest request, CancellationToken ct) =>
    {
        var items = await GetItemsToExport(request);
        int total = items.Count;
    
        for (int i = 0; i < total && !ct.IsCancellationRequested; i++)
        {
            await ProcessItem(items[i]);
            yield return new ProgressUpdate
            {
                Current = i + 1,
                Total = total,
                Percentage = (int)((i + 1.0) / total * 100)
            };
        }
    });
    
    record ProgressUpdate(int Current, int Total, int Percentage);

    El cliente recibe un flujo continuo de datos sin necesidad de polling. Perfecto para dashboards en tiempo real, barras de progreso de procesos largos, o feeds de eventos de sistema.

    El checklist completo de producción

    Antes de llevar cualquier Web API a producción, repaso esta lista de los 11 componentes que cubrimos en la serie:

    • Health Checks — Liveness y Readiness configurados
    • Exception Handler global — ProblemDetails para todos los errores
    • Validación — Endpoint Filters antes de la lógica de negocio
    • OpenAPI nativo — Documentación sin Swashbuckle
    • Rate Limiting — Protección en endpoints públicos
    • Output Cache — En endpoints de lectura costosos
    • API Versioning — Desde el primer endpoint
    • OpenTelemetry — Métricas, trazas y logs exportando
    • Polly — Resiliencia en todas las llamadas externas
    • Feature Flags — Para funcionalidades en desarrollo activo
    • SSE — En lugar de polling para actualizaciones en tiempo real

    No hace falta implementarlos todos desde el día uno, pero sí conviene tenerlos en el radar desde que empezás. Cuanto más temprano los incorporés, menos dolores de cabeza cuando la API escale.

    ¿Hay algún componente que uses habitualmente y no esté en la lista? Dejamelo en los comentarios.

    Parte 2: Rate Limiting, Output Cache y API Versioning

  • Arquitectura de Web APIs en .NET 10 — Parte 2: Performance y Escalabilidad

    Esta es la segunda parte de la serie Arquitectura Esencial de Web APIs en .NET 10. En la Parte 1 cubrimos los fundamentos: Health Checks, Exception Handling, Validación y OpenAPI nativo. Ahora le toca el turno a los componentes que marcan la diferencia cuando tu API empieza a recibir carga real.

    Estos tres los ignoré durante demasiado tiempo, hasta que los problemas aparecieron solos en producción.

    5. Rate Limiting: protegé tu API del abuso

    Una API pública sin rate limiting es un blanco fácil. Me tocó ver picos de miles de requests por minuto desde una sola IP dejando el servidor de rodillas. Desde .NET 7 el middleware de rate limiting viene integrado en el framework, y en .NET 10 está completamente maduro.

    .NET ofrece cuatro estrategias distintas según el caso de uso:

    • Fixed Window: X peticiones por ventana de tiempo fija
    • Sliding Window: igual pero la ventana se mueve (más preciso)
    • Token Bucket: permite bursts controlados
    • Concurrency: limita las peticiones simultáneas, no el rate
    builder.Services.AddRateLimiter(options =>
    {
        // Política general: 100 requests por minuto por IP
        options.AddFixedWindowLimiter("General", opt =>
        {
            opt.Window = TimeSpan.FromMinutes(1);
            opt.PermitLimit = 100;
            opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
            opt.QueueLimit = 10; // Cuántas requests pueden esperar en cola
        });
    
        // Política estricta para endpoints sensibles
        options.AddSlidingWindowLimiter("Strict", opt =>
        {
            opt.Window = TimeSpan.FromMinutes(1);
            opt.SegmentsPerWindow = 6; // Segmentos de 10 segundos
            opt.PermitLimit = 20;
        });
    
        // Respuesta personalizada cuando se supera el límite
        options.OnRejected = async (context, cancellationToken) =>
        {
            context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
            await context.HttpContext.Response.WriteAsJsonAsync(
                new ProblemDetails { Title = "Too Many Requests", Status = 429 },
                cancellationToken);
        };
    });
    
    var app = builder.Build();
    app.UseRateLimiter();
    
    // Aplicar a endpoints específicos
    app.MapGet("/api/data", () => "Datos")
       .RequireRateLimiting("General");
    
    app.MapPost("/api/auth/login", (LoginDto dto) => Results.Ok())
       .RequireRateLimiting("Strict");

    Para APIs que manejan usuarios autenticados, podés segmentar el rate limiting por user ID en lugar de IP, así los usuarios legítimos no se ven afectados por el comportamiento de otros.

    6. Output Cache: no calcules lo mismo dos veces

    En un proyecto de reportes que tenía consultas de 2 segundos contra SQL Server, implementar Output Caching llevó el tiempo de respuesta promedio a menos de 10ms. Sin tocar una sola línea de lógica de negocio.

    Para endpoints que devuelven datos que no cambian constantemente, el Output Cache guarda la respuesta completa en memoria (o Redis) y la sirve directamente sin ejecutar nada de la lógica de negocio.

    builder.Services.AddOutputCache(options =>
    {
        // Política base: cache de 60 segundos
        options.AddBasePolicy(builder => builder.Expire(TimeSpan.FromSeconds(60)));
    
        // Política nombrada para datos de referencia
        options.AddPolicy("ReferenceData", builder =>
            builder.Expire(TimeSpan.FromMinutes(10))
                   .Tag("reference")); // Tag para invalidación selectiva
    });
    
    var app = builder.Build();
    app.UseOutputCache();
    
    // Cache por 10 segundos (override de la política base)
    app.MapGet("/api/stats", () => GetExpensiveStats())
       .CacheOutput(c => c.Expire(TimeSpan.FromSeconds(10)));
    
    // Usando política nombrada
    app.MapGet("/api/products", () => GetProducts())
       .CacheOutput("ReferenceData");
    
    // Invalidar el cache cuando cambian los datos
    app.MapPost("/api/products", async (ProductDto product, IOutputCacheStore cache) =>
    {
        await SaveProduct(product);
        await cache.EvictByTagAsync("reference", CancellationToken.None); // Invalida el cache
        return Results.Created($"/api/products/{product.Id}", product);
    });

    La invalidación por tags es la clave para usar Output Cache sin miedo a servir datos stale. Cuando mutás datos, invalidás el tag correspondiente y la próxima request regenera el cache.

    Para ambientes distribuidos (múltiples instancias), podés configurar Redis como backing store con AddStackExchangeRedisOutputCache().

    7. API Versioning: cambiar sin romper a tus clientes

    Esta es una de las lecciones más caras que aprendí: si tu API tiene clientes externos y no versiona desde el principio, el día que necesités hacer un cambio incompatible vas a tener un problema enorme. Agregarle versionado a una API ya desplegada es doloroso. Hacerlo desde el inicio es trivial.

    En Minimal APIs el versionado se maneja mediante Version Sets. Primero instalá el paquete Asp.Versioning.Http:

    dotnet add package Asp.Versioning.Http
    // Program.cs
    builder.Services.AddApiVersioning(options =>
    {
        options.DefaultApiVersion = new ApiVersion(1, 0);
        options.AssumeDefaultVersionWhenUnspecified = true;
        options.ReportApiVersions = true; // Devuelve las versiones disponibles en headers
    });
    
    var app = builder.Build();
    
    var apiVersionSet = app.NewApiVersionSet()
        .HasApiVersion(new ApiVersion(1, 0))
        .HasApiVersion(new ApiVersion(2, 0))
        .ReportApiVersions()
        .Build();
    
    // V1: respuesta simple
    app.MapGet("/api/v{version:apiVersion}/users", () =>
        new[] { new { Id = 1, Name = "Juan" } })
       .WithApiVersionSet(apiVersionSet)
       .MapToApiVersion(1, 0);
    
    // V2: respuesta enriquecida con datos extra
    app.MapGet("/api/v{version:apiVersion}/users", () =>
        new[] { new { Id = 1, Name = "Juan", Email = "juan@ejemplo.com", Role = "Admin" } })
       .WithApiVersionSet(apiVersionSet)
       .MapToApiVersion(2, 0);

    Los clientes existentes siguen usando /api/v1/users sin ningún cambio. Los nuevos pueden adoptar /api/v2/users con el formato enriquecido. Convivencia perfecta.

    Además del versionado por URL, podés usar query string (?api-version=2.0) o header (X-API-Version: 2.0) según las necesidades de tus clientes.

    Resumen de la Parte 2

    Tres componentes que hacen la diferencia cuando la API escala:

    • Rate Limiting → protección contra abuso, con estrategias flexibles por caso de uso
    • Output Cache → performance brutal en endpoints de lectura, con invalidación inteligente
    • API Versioning → libertad para evolucionar sin romper contratos existentes

    En la Parte 3 cerramos la serie con los componentes más avanzados: OpenTelemetry, Polly, Feature Flags y Server-Sent Events.

  • Arquitectura de Web APIs en .NET 10 — Parte 1: Los fundamentos que no pueden faltar

    Cuando empecé a trabajar con .NET, mi idea de una «API lista para producción» era que compilara y los endpoints respondieran correctamente. Con el tiempo aprendí que hay una diferencia enorme entre una API que funciona en tu máquina y una que sobrevive el mundo real.

    Esta es la primera parte de una serie de tres artículos donde te cuento los 11 componentes que hoy considero esenciales en cualquier Web API con .NET 10. Empezamos por los fundamentos: los cuatro que deberías tener desde el primer día, sin importar el tamaño del proyecto.

    1. Health Checks: que el orquestador sepa si tu API está viva

    La primera vez que deployé en Kubernetes sin health checks fue un desastre silencioso. El pod figuraba como «Running», pero la base de datos no conectaba y las requests simplemente fallaban sin que el orquestador se enterara.

    Los Health Checks exponen endpoints que le dicen a Kubernetes (o a cualquier balanceador de carga) dos cosas fundamentales:

    • Liveness: ¿el proceso sigue vivo?
    • Readiness: ¿está listo para recibir tráfico? (sus dependencias —base de datos, Redis, etc.— están funcionando)
    // Program.cs
    builder.Services.AddHealthChecks()
        .AddNpgSql(builder.Configuration.GetConnectionString("Default")); // Verifica PostgreSQL
    
    var app = builder.Build();
    
    app.MapHealthChecks("/health");          // Endpoint general
    app.MapHealthChecks("/health/ready",    // Solo readiness
        new HealthCheckOptions { Predicate = check => check.Tags.Contains("ready") });
    app.MapHealthChecks("/health/live",     // Solo liveness
        new HealthCheckOptions { Predicate = _ => false });

    Con el paquete AspNetCore.HealthChecks.* podés agregar chequeos para SQL Server, Redis, servicios HTTP externos y más. Una línea por dependencia crítica.

    2. Exception Handling Global con IExceptionHandler

    Nada peor que una API que le devuelve un stack trace de C# al cliente, o un error 500 genérico sin información útil. Durante mucho tiempo usé try/catch en cada endpoint, hasta que descubrí IExceptionHandler.

    Con esta interfaz centralizás el manejo de errores en un solo lugar y respondés siempre con el formato estándar ProblemDetails (RFC 7807), que es lo que los clientes modernos esperan.

    public class GlobalExceptionHandler : IExceptionHandler
    {
        private readonly ILogger<GlobalExceptionHandler> _logger;
    
        public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
        {
            _logger = logger;
        }
    
        public async ValueTask<bool> TryHandleAsync(
            HttpContext context, Exception exception, CancellationToken cancellationToken)
        {
            _logger.LogError(exception, "Error no controlado: {Message}", exception.Message);
    
            var problemDetails = new ProblemDetails
            {
                Status = StatusCodes.Status500InternalServerError,
                Title = "Ocurrió un error inesperado",
                Detail = exception.Message
            };
    
            context.Response.StatusCode = problemDetails.Status.Value;
            await context.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
            return true; // Excepción manejada, detiene la cadena
        }
    }
    
    // Program.cs
    builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
    builder.Services.AddProblemDetails();
    app.UseExceptionHandler();

    Podés crear múltiples handlers y registrarlos en orden: el primero que devuelva true corta la cadena. Útil para manejar ValidationException, NotFoundException, etc., con respuestas específicas.

    3. Validación Nativa con Endpoint Filters

    Validar el payload antes de que llegue a la lógica de negocio es crítico. En .NET 10, las Minimal APIs tienen Endpoint Filters que actúan como un pipeline de validación declarativo, sin necesidad de librerías externas para los casos más comunes.

    app.MapPost("/api/users", (UserDto user) =>
    {
        return Results.Created($"/api/users/{user.Id}", user);
    })
    .AddEndpointFilter(async (context, next) =>
    {
        var user = context.GetArgument<UserDto>(0);
    
        var errors = new Dictionary<string, string[]>();
    
        if (string.IsNullOrEmpty(user.Name))
            errors["Name"] = new[] { "El nombre es obligatorio" };
    
        if (user.Age < 0 || user.Age > 120)
            errors["Age"] = new[] { "La edad debe estar entre 0 y 120" };
    
        if (errors.Any())
            return Results.ValidationProblem(errors);
    
        return await next(context);
    });

    Para proyectos más grandes donde necesitás validaciones complejas, FluentValidation sigue siendo una excelente opción que se integra perfectamente con este patrón.

    4. OpenAPI Integrado: adiós Swashbuckle

    Durante años, generar documentación Swagger significaba instalar Swashbuckle y rezar para que no conflictuara con otras dependencias. En .NET 10, la generación de especificación OpenAPI es nativa, sin dependencias externas, con mejor performance de arranque y mantenimiento garantizado por Microsoft.

    builder.Services.AddOpenApi(); // Generación nativa
    
    var app = builder.Build();
    
    if (app.Environment.IsDevelopment())
    {
        app.MapOpenApi(); // Expone la spec en /openapi/v1.json
    
        // Opción A: Scalar (UI moderna, recomendada)
        app.MapScalarApiReference();
    
        // Opción B: Swagger UI clásico
        // app.UseSwaggerUI(c => c.SwaggerEndpoint("/openapi/v1.json", "API v1"));
    }

    Para la UI de documentación, te recomiendo probar Scalar: tiene mucho mejor UX que Swagger UI clásico y es trivial de integrar.

    Resumen de la Parte 1

    Estos cuatro componentes son los que agrego en cualquier proyecto nuevo desde el día uno:

    • Health Checks → para que Kubernetes y el balanceador sepan el estado real de tu API
    • Exception Handler global → respuestas de error consistentes y sin leakear internos
    • Validación con Endpoint Filters → datos limpios antes de tocar la lógica de negocio
    • OpenAPI nativo → documentación siempre actualizada sin overhead

    En la Parte 2 vemos los tres componentes que marcan la diferencia cuando la API empieza a escalar: Rate Limiting, Output Cache y API Versioning.

Tags

tsql (27)mssql (26)sql (20)devops (20)dotnet (18)docker (15)performance (14)contenedores (11)dotnet10 (10)linux (9)csharp (8)microservicios (7)angular (7)angular21 (7)sql server (6)issabel (6)docker-compose (6)typescript (6)mysql (5).NET (5)