ums

ADR-0060: AOP Cross-Cutting Concern Strategy — DispatchProxy over MediatR Behaviors

Status

Accepted

Date

2026-05-24

Decision Owner

Architecture


Context

UMS command handlers need structured cross-cutting concerns: entry/exit logging with duration, distributed tracing with tenant tags, metrics (RED signals), and exception capture. These concerns must be:

  1. Selective — applied per-handler or per-method, not uniformly to all requests.
  2. Non-invasive — zero changes to the handler’s business logic.
  3. Async-correct — fire hooks after the awaited result, not when the Task object is returned.
  4. Testable in isolation — handlers unit-tested without cross-cutting infrastructure.

UMS already uses MediatR IPipelineBehavior<TRequest, TResponse> for uniform pipeline concerns (ValidationBehavior). The question is whether to extend that mechanism for cross-cutting concerns or adopt a different model.

Alternatives considered

Option Mechanism Selective? Async-correct? External dep? Decision
A MediatR IPipelineBehavior<,> ❌ all-or-nothing per type Rejected for cross-cutting
B Decorator classes per handler ✅ manual Rejected — O(n) boilerplate
C Castle.DynamicProxy / Autofac interceptors ❌ new NuGet required Rejected — stack pollution
D Ums.Shell.Aop + System.Reflection.DispatchProxy ✅ attribute-driven ✅ (after fix) ❌ owned shell lib Adopted

Why MediatR IPipelineBehavior was not sufficient

IPipelineBehavior<TRequest, TResponse> applies to every command that matches its type constraint. This is the right model for uniform concerns (validation, idempotency) but creates unacceptable coupling for selective concerns:

Resolution: MediatR behaviors remain the canonical mechanism for uniform pipeline concerns. Ums.Shell.Aop is the canonical mechanism for selective, per-method decoration.

Why Ums.Shell.Aop was chosen over a new NuGet dependency


Decision

Adopt Ums.Shell.Aop with System.Reflection.DispatchProxy as the mechanism for selective, per-method cross-cutting concerns in UMS command handlers.

1. Separation of responsibilities

Concern Mechanism Applies to
Input validation ValidationBehavior (MediatR) All commands uniformly
Idempotency IdempotencyMiddleware (HTTP) All mutating endpoints
Logging (selective) LoggerAspect via Ums.Shell.Aop Per-handler, opt-in via [LoggerAspect]
Tracing (Phase 2) TracingAspect via Ums.Shell.Aop Per-handler, opt-in via [Tracing]
Metrics (Phase 2) MetricsAspect via Ums.Shell.Aop Per-handler, opt-in via [Metrics]
Retry (selective) RetryAspect via Ums.Shell.Aop Per-method, opt-in via [RetryAspect]

2. Async proxy fix — mandatory prerequisite

System.Reflection.DispatchProxy.Invoke is synchronous. Prior to this ADR, OnMethodBoundaryAspect.OnSuccess and OnExit fired when a Task was returned, not when it completed. This caused hooks to observe incomplete state.

Fix (implemented in Ums.Shell.Aop/Impl/OnMethodBoundaryAspect.cs): After joinPoint.Proceed(), detect Task / Task<TResult> return types and wrap them in continuation tasks via ConfigureAwait(false). The original Task<TResult> path is handled via a cached generic MethodInfo (WrapAsyncOfT<TResult>) to preserve the result value. The synchronous finally { OnExit() } block is skipped for async paths to prevent double-firing.

3. Adoption scope

Phase 1 (implemented — 2026-05-24)

Phase 2 (planned)

Phase 3 (future consideration)

4. PII policy for logging aspects

Logger Argument values logged When to use
MelLogger ❌ Never — names/types only Default; all handlers
SerilogLogger ✅ Destructured (opt-in) Only after explicit PII review and approval

[LoggerAspect(LogArguments = [])] (empty array) is the PII-safe default and must be set on all handlers unless a specific argument is reviewed and cleared.

5. Layer references introduced

Ums.Application.csproj
  └── Ums.Shell.Aop.Aspects   ← attribute contract only ([LoggerAspect], etc.)

Ums.Infrastructure.csproj
  ├── Ums.Shell.Aop.Microsoft.Extensions.DependencyInjection.Aspects.Installer ← AddAop(), AddAopProxy<>()
  └── Ums.Shell.Aop.Aspects.Logger.Serilog ← SerilogLogger adapter

Ums.Domain does not reference any Ums.Shell.Aop project. Domain purity is preserved.


Consequences

Positive

Trade-offs

Non-decisions


Compliance

The following checks are mandatory after any change to an AOP aspect or AOP proxy registration:

# Build the full solution
dotnet build src/apps/ums.api/Ums.sln

# Run all test suites
dotnet test src/apps/ums.api/Ums.sln --verbosity minimal
dotnet test src/libs/shell/aop/src/Ums.Shell.Aop.Tests/Ums.Shell.Aop.Tests.csproj --verbosity minimal

# Verify no Domain purity violation
grep -r "Ums.Shell.Aop" src/apps/ums.api/Ums.Domain/ --include="*.csproj"
# Expected: no output

ADR Registry AOP Developer Guide ADR-0053 OpenTelemetry ADR-0054 Shell Isolation