Aceptado
2026-05-24
Arquitectura
BeyondNetCode.Shell.AopLos command handlers de UMS necesitan concerns cross-cutting estructurados: logging de entrada/salidacon duración, distributed tracingcon tags de tenant, métricas (señales RED), ycaptura de excepciones. Estos concerns deben ser:
Task es retornado.UMS ya usa MediatR IPipelineBehavior<TRequest, TResponse> para concernsuniformesde pipeline (ValidationBehavior). La pregunta es si extender ese mecanismo para concerns cross-cutting o adoptar un modelo diferente.
| Opción | Mecanismo | Selectivo? | Async-correct? | Dep. externa? | Decisión | |
|---|---|---|---|---|---|---|
| A | MediatR IPipelineBehavior<,> |
all-or-nothing por tipo | Rechazado para cross-cutting | |||
| B | Clases decorator por handler | manual | Rechazado — O(n) boilerplate | |||
| C | Castle.DynamicProxy / Autofac interceptors | nuevo NuGet requerido | Rechazado — stack pollution | |||
| D | BeyondNetCode.Shell.Aop + System.Reflection.DispatchProxy |
attribute-driven | (after fix) | owned shell lib | Adoptado | #### Por qué MediatR IPipelineBehavior no fue suficiente |
IPipelineBehavior<TRequest, TResponse> aplica a cada comando que coincide con su constraint de tipo. Este es el modelo correcto para concernsuniformes (validación, idempotencia) pero crea acoplamiento inaceptable para concernsselectivos:
CreateTenantCommand necesitaría condiciones específicas de tipo o registros de behavior separados por tipo de request.if request is X then log, else skip) es un anti-pattern que defeats el propósito de la abstracción de pipeline.Resolución:Los behaviors de MediatR permanecen como el mecanismo canónico para concerns de pipeline uniformes. BeyondNetCode.Shell.Aop es el mecanismo canónico para decoración selectiva, por-método.
BeyondNetCode.Shell.Aop sobre una nueva dependencia NuGetBeyondNetCode.Shell.Aop es unaownedshell library — sin gestión de paquetes externos, sin cambios rupturistas upstream, sin superficie CVE adicional.DispatchProxy, AspectExecutor, PointCut, IAspect chain, OnMethodBoundaryAspect, LoggerAspect, RetryAspect, y AdviceAspect.AddAop() + AddAopProxy<TService, TImpl>() ya está construida y testeada.BeyondNetCode.Shell.Aop con System.Reflection.DispatchProxy como el mecanismo para concerns cross-cutting selectivos, por-método en los command handlers de UMS.| Concern | Mecanismo | Aplica a | |
|---|---|---|---|
| Validación de input | ValidationBehavior (MediatR) |
Todos los commands uniformemente | |
| Idempotencia | IdempotencyMiddleware (HTTP) |
Todos los endpoints mutantes | |
| Logging (selectivo) | LoggerAspect vía BeyondNetCode.Shell.Aop |
Por-handler, opt-in vía [LoggerAspect] |
|
| Tracing (Fase 2) | TracingAspect vía BeyondNetCode.Shell.Aop |
Por-handler, opt-in vía [Tracing] |
|
| Métricas (Fase 2) | MetricsAspect vía BeyondNetCode.Shell.Aop |
Por-handler, opt-in vía [Metrics] |
|
| Retry (selectivo) | RetryAspect vía BeyondNetCode.Shell.Aop |
Por-método, opt-in vía [RetryAspect] |
### 2. Fix async de proxy — prerrequisito obligatorio |
System.Reflection.DispatchProxy.Invoke es síncrono. Antes de este ADR, OnMethodBoundaryAspect.OnSuccess y OnExit disparaban cuando un Task era retornado, no cuando completaba. Esto causaba que los hooks observaran estado incompleto.
Fix (implementado en BeyondNetCode.Shell.Aop/Impl/OnMethodBoundaryAspect.cs):Después de joinPoint.Proceed(), detectar tipos de retorno Task / Task<TResult> y envolvarlos en continuation tasks vía ConfigureAwait(false). El path original Task<TResult> se maneja vía un MethodInfo genérico cacheado (WrapAsyncOfT<TResult>) para preservar el valor de resultado. El bloque síncrono finally { OnExit() } se skippea para paths async para prevenir double-firing.
[LoggerAspect(Type = typeof(IMelLogger), LogDuration = true, LogException = true)] en CreateTenantCommandHandler.HandleMelLogger registrado como keyed ILogger (AOP) bajo typeof(IMelLogger) en Infrastructure DIIMelLogger marker interface en Ums.Application.Common.Aop — decouple Application layer del tipo concreto de InfrastructureAddAopProxy<IRequestHandler<CreateTenantCommand, Result<CreateTenantResponse>>, CreateTenantCommandHandler>() en Infrastructure.DependencyInjectionTracingAspect implementando ActivitySource.StartActivity() con tag tenant_id (alinea con ADR-0053)MetricsAspect implementando Histogram<long> vía IMeterFactoryAddAopProxy<> a todos los command handlers en los bounded contexts Identity y AuthorizationRetryAspect en servicios de Infrastructure que llaman a endpoints externos de IdPAdviceAspect para hooks de audit específicos de dominio| Logger | Valores de argumentos logueados | Cuando usar | |
|---|---|---|---|
MelLogger |
Nunca — solo nombres/tipos | Default; todos los handlers | |
SerilogLogger |
Destructureados (opt-in) | Solo después de revisión y aprobación de PII explícita | [LoggerAspect(LogArguments = [])] (array vacío) es el default PII-safe y debe setearse en todos los handlers a menos que un argumento específico sea revisado y clearance. |
Ums.Application.csproj
└── BeyondNetCode.Shell.Aspects ← contrato de atributos solo ([LoggerAspect], etc.)
Ums.Infrastructure.csproj
├── BeyondNetCode.Shell.DI ← AddAop(), AddAopProxy<>()
└── BeyondNetCode.Shell.Logger.Serilog ← Adaptador SerilogLogger
Ums.Domain noreferencia ningún proyecto BeyondNetCode.Shell.Aop. La pureza del Domain se preserva.
[LoggerAspect] + [Tracing] hacen la decoración de concern visible y searchable en code review.MelLogger bridgea el ILogger custom de AOP a Microsoft.Extensions.Logging — no se requiere un custom Serilog sink en Application.System.Reflection.DispatchProxy requiere que el servicio sea uninterface (o clase abstracta). El proxying de clase concreta no está soportado — esto es enforce por AddAopProxy<TService, TImpl>().DispatchProxy.Invoke es intrínsecamente síncrono; el wrapper de continuación async añade overhead menor (~1 allocación por llamada de método async).PointCut cachea (MethodInfo, Type) → bool por tipo de proxy — el cache crece proporcionalmente con el número de métodos proxied; insignificante en la práctica.RegisterServicesFromAssembly de MediatR registra handlers antes de AddAopProxy<> — los llamadores deben asegurar que AddAopProxy<> se llamedespuésdel registro de MediatR para que el proxy gane la resolución de último-registro.AddAopProxy lanza ArgumentException para ServiceLifetime.Singleton) porque los aspectos pueden resolver servicios scoped (ej., IUserContext).DispatchProxy se vuelve limitante.Las siguientes verificaciones son obligatorias después de cualquier cambio a un aspecto AOP o registro de proxy AOP:
# Build de la solución completa
dotnet build src/apps/ums.api/Ums.sln
# Ejecutar todos los test suites
dotnet test src/apps/ums.api/Ums.sln --verbosity minimal
dotnet test src/libs/shell/aop/src/BeyondNetCode.Shell.Aop.Tests/BeyondNetCode.Shell.Aop.Tests.csproj --verbosity minimal
# Verificar que no hay violación de pureza del Domain
grep -r "BeyondNetCode.Shell.Aop" src/apps/ums.api/Ums.Domain/ --include="*.csproj"
# Esperado: sin output
| Índice ADR | Guía de Desarrollo AOP | ADR-0053 OpenTelemetry | ADR-0054 Aislamiento de Shell |