Estado: Aceptado
Fecha: 2026-05-24
Responsable: Arquitectura
Disposición Evolith: Propuesto para adopción en Evolith — sin dependencias específicas de UMS; aplicable a cualquier satélite .NET
Relacionados:
Los command handlers, aspectos AOP, servicios de background y dispatchers de outbox necesitan acceso a las mismas señales de observabilidad con scope de request: CorrelationId, SessionTrackingId, TraceId y SpanId.
Antes de este ADR, cada componente las resolvía de forma independiente:
MelLogger leía directamente desde Activity.CurrentCorrelationIdMiddleware escribía en HttpContext.TraceIdentifierSerilogLogger usaba el estático Log.ForContext() sin estado de requestEsto generaba tres problemas:
IHttpContextAccessor, filtrando infraestructura de Presentación hacia las capas de Aplicación o Infraestructura.Activity.Current es null en dispatchers de outbox y servicios de background; no había forma de propagar el contexto del request originador.| Opción | Problema |
|---|---|
IHttpContextAccessor en todas partes |
Acopla Aplicación/Infraestructura a HTTP |
AsyncLocal<T> flow |
Se rompe en límites de Task.Run y ConfigureAwait(false) |
Solo Activity.Current estático |
Null en servicios de background; sin SessionTrackingId |
RequestContextAccessor con scope |
Escribible por middleware, legible por cualquier servicio con scope — sin dependencia HTTP |
Introducir un RequestContextAccessor con scope que el middleware escribe una vez y cualquier componente del scope del request puede leer a través de un puerto de solo lectura IRequestContext (Aplicación) o un puerto de escritura IExecutionContextAccessor (Infraestructura/AOP), sin acoplarse a HTTP.
BeyondNetCode.Shell.Logger.Serilog (shell library — genérica, sin dependencia de UMS)
├── ExecutionContextSnapshot record(CorrelationId, SessionTrackingId, TraceId, SpanId)
├── IExecutionContextAccessor interface { Current; Set(snapshot) }
├── ObservabilityHeaders clase estática — constantes de nombres de headers HTTP
│ CorrelationId = "X-Correlation-Id"
│ SessionTrackingId = "X-Session-Tracking-Id"
└── ObservabilityKeys clase estática — constantes de claves baggage/tag OTel
CorrelationId = "correlation.id"
SessionTrackingId = "session.tracking_id"
Ums.Application.Common.Interfaces
└── IRequestContext puerto de solo lectura para la capa de Aplicación
SessionTrackingId, CorrelationId, TraceId, SpanId (todos string?)
Ums.Infrastructure.Services
└── RequestContextAccessor implementa IRequestContext + IExecutionContextAccessor
registrado como IRequestContext (solo lectura) e IExecutionContextAccessor (escritura)
[Llega HTTP Request]
│
▼
CorrelationIdMiddleware
– lee / genera header X-Correlation-Id
– escribe en baggage Activity.Current ("correlation.id")
– escribe en scope ILogger ("CorrelationId")
│
▼
SessionTrackingMiddleware
– lee / genera header X-Session-Tracking-Id
– escribe en baggage Activity.Current ("session.tracking_id")
– llama RequestContextAccessor.Set(new ExecutionContextSnapshot(...))
– escribe en scope ILogger ("SessionTrackingId")
│
▼
RequestContextAccessor (scoped)
– mantiene el snapshot durante el resto del request
– legible por aspectos AOP, handlers, handoffs a background
│
▼
UmsSerilogLogger / StructuredAopLoggerBase
– llama ResolveExecutionContext() → lee RequestContextAccessor.Current
– cae a Activity.Current si el snapshot está vacío (ruta de servicio background)
StructuredAopLoggerBase.ResolveExecutionContext()1. RequestContextAccessor.Current (establecido por SessionTrackingMiddleware)
2. Baggage de Activity.Current (fallback para contextos no-HTTP)
3. Parámetro requestId del atributo [LoggerAspect]
4. Cadena vacía
// Ums.Infrastructure/DependencyInjection.cs
services.AddScoped<RequestContextAccessor>();
services.AddScoped<IRequestContext>(sp => sp.GetRequiredService<RequestContextAccessor>());
services.AddScoped<IExecutionContextAccessor>(sp => sp.GetRequiredService<RequestContextAccessor>());
| Capa | Puede usar | NO puede usar |
|---|---|---|
Ums.Domain |
— (no necesita contexto) | IRequestContext, IExecutionContextAccessor |
Ums.Application |
IRequestContext (puerto solo lectura) |
IExecutionContextAccessor, RequestContextAccessor |
Ums.Infrastructure |
IExecutionContextAccessor (adaptadores AOP) |
RequestContextAccessor directo (inyectar por interfaz) |
Ums.Presentation |
Ambas interfaces vía DI; RequestContextAccessor en middleware |
— |
ExecutionContextSnapshot en el momento del handoffStructuredAopLoggerBase en la shell library usa este patrón sin ningún import específico de UMSObservabilityHeaders y ObservabilityKeys evitan la proliferación de literales de cadena en los middlewaresRequestContextAccessor es escribible por cualquier código con IExecutionContextAccessor — el contrato es por convención, no forzado. El middleware debe ser el único escritor.SessionTrackingMiddleware; los spans que se inician después en el pipeline tienen un SpanId obsoleto en el snapshot. Los aspectos AOP compensan leyendo Activity.Current.SpanId como fallback.Los siguientes tipos están en BeyondNetCode.Shell.Logger.Serilog sin ningún import específico de UMS:
ExecutionContextSnapshot — record genérico, sin referencias de productoIExecutionContextAccessor — interfaz genéricaObservabilityHeaders — constantes, renombrar prefijo según correspondaObservabilityKeys — constantes, renombrar prefijo según correspondaStructuredAopLoggerBase — depende solo de IExecutionContextAccessor e IJoinPointIRequestContext y RequestContextAccessor tienen namespace de UMS pero son trivialmente portables a cualquier satélite.
| Registro ADR | CP-05 Contexto de Ejecución | ADR-0053 OTel |