Status: Accepted
Date: 2026-05-24
Decision Owner: Architecture
Evolith disposition: Proposed for Evolith adoption — zero UMS-specific dependencies; applicable to any .NET satellite
Related:
Command handlers, AOP aspects, background services, and outbox dispatchers all need access to the same request-scoped observability signals: CorrelationId, SessionTrackingId, TraceId, and SpanId.
Before this ADR, each component resolved these independently:
MelLogger read from Activity.Current directlyCorrelationIdMiddleware wrote to HttpContext.TraceIdentifierSerilogLogger used the static Log.ForContext() without request stateThis created three problems:
IHttpContextAccessor, leaking Presentation infrastructure into Application or Infrastructure layers.Activity.Current is null in outbox dispatchers and background services; there was no way to carry forward the originating request context.| Option | Problem |
|---|---|
IHttpContextAccessor everywhere |
Couples Application/Infrastructure to HTTP |
AsyncLocal<T> flow |
Breaks across Task.Run boundaries and ConfigureAwait(false) |
Static Activity.Current only |
Null in background services; no SessionTrackingId |
Scoped RequestContextAccessor |
Writable by middleware, readable by any scoped service — no HTTP dependency |
Introduce a scoped RequestContextAccessor that is written by middleware and read by any component in the request scope without coupling to HTTP.
Ums.Shell.Aop.Aspects.Logger.Serilog (shell library — generic, no UMS dependency)
├── ExecutionContextSnapshot record(CorrelationId, SessionTrackingId, TraceId, SpanId)
├── IExecutionContextAccessor interface { Current; Set(snapshot) }
├── ObservabilityHeaders static class — HTTP header name constants
│ CorrelationId = "X-Correlation-Id"
│ SessionTrackingId = "X-Session-Tracking-Id"
└── ObservabilityKeys static class — OTel baggage/tag key constants
CorrelationId = "correlation.id"
SessionTrackingId = "session.tracking_id"
Ums.Application.Common.Interfaces
└── IRequestContext read-only port for Application layer
SessionTrackingId, CorrelationId, TraceId, SpanId (all string?)
Ums.Infrastructure.Services
└── RequestContextAccessor implements IRequestContext + IExecutionContextAccessor
registered as IRequestContext (read-only) and IExecutionContextAccessor (writable)
[HTTP Request arrives]
│
▼
CorrelationIdMiddleware
– reads / generates X-Correlation-Id header
– writes to Activity.Current baggage ("correlation.id")
– writes to ILogger scope ("CorrelationId")
│
▼
SessionTrackingMiddleware
– reads / generates X-Session-Tracking-Id header
– writes to Activity.Current baggage ("session.tracking_id")
– calls RequestContextAccessor.Set(new ExecutionContextSnapshot(...))
– writes to ILogger scope ("SessionTrackingId")
│
▼
RequestContextAccessor (scoped)
– holds snapshot for remainder of request
– readable by AOP aspects, handlers, background handoffs
│
▼
UmsSerilogLogger / StructuredAopLoggerBase
– calls ResolveExecutionContext() → reads RequestContextAccessor.Current
– falls back to Activity.Current if snapshot is empty (background service path)
StructuredAopLoggerBase.ResolveExecutionContext()1. RequestContextAccessor.Current (set by SessionTrackingMiddleware)
2. Activity.Current baggage (fallback for non-HTTP contexts)
3. requestId parameter from [LoggerAspect] attribute
4. Empty string
// Ums.Infrastructure/DependencyInjection.cs
services.AddScoped<RequestContextAccessor>();
services.AddScoped<IRequestContext>(sp => sp.GetRequiredService<RequestContextAccessor>());
services.AddScoped<IExecutionContextAccessor>(sp => sp.GetRequiredService<RequestContextAccessor>());
| Layer | May use | May NOT use |
|---|---|---|
Ums.Domain |
— (no context needed) | IRequestContext, IExecutionContextAccessor |
Ums.Application |
IRequestContext (read-only port) |
IExecutionContextAccessor, RequestContextAccessor |
Ums.Infrastructure |
IExecutionContextAccessor (AOP adapters) |
direct RequestContextAccessor (inject via interface) |
Ums.Presentation |
Both interfaces via DI; RequestContextAccessor in middleware |
— |
ExecutionContextSnapshot at handoff timeStructuredAopLoggerBase in the shell library uses this pattern without any UMS-specific importObservabilityHeaders and ObservabilityKeys constants prevent string-literal proliferation across middlewareRequestContextAccessor is writable by any code with IExecutionContextAccessor — the contract is by convention, not enforced. Middleware should be the only writer.SessionTrackingMiddleware position in the pipeline; spans that start later in the pipeline get a stale SpanId in the snapshot. AOP aspects compensate by reading Activity.Current.SpanId as fallback.The following types are in Ums.Shell.Aop.Aspects.Logger.Serilog with no UMS-specific import:
ExecutionContextSnapshot — generic record, no product referencesIExecutionContextAccessor — generic interfaceObservabilityHeaders — constants, rename prefix as appropriateObservabilityKeys — constants, rename prefix as appropriateStructuredAopLoggerBase — depends only on IExecutionContextAccessor and IJoinPointIRequestContext and RequestContextAccessor are UMS-namespaced but trivially portable to any satellite.
| ADR Registry | CP-05 Execution Context | ADR-0053 OTel |