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.