ADR-004: Delegate-Based Pipeline Behaviors over ASP.NET Middleware
Status
Accepted
Context
NimBus needed a middleware pipeline for cross-cutting concerns (logging, metrics, validation). Two patterns were considered:
- ASP.NET Core middleware pattern —
IMiddlewarewithInvokeAsync(HttpContext, RequestDelegate next) - Delegate-based pipeline behavior —
IMessagePipelineBehaviorwithHandle(IMessageContext, MessagePipelineDelegate next, CancellationToken)
MassTransit uses IFilter<T>, NServiceBus uses IBehavior<TContext>, and MediatR uses IPipelineBehavior<TRequest, TResponse>. All follow the delegate/next pattern.
Decision
Use IMessagePipelineBehavior with a MessagePipelineDelegate next parameter. Behaviors are registered via AddNimBus(builder => builder.AddPipelineBehavior<T>()) and resolved from DI.
The pipeline wraps MessageHandler.HandleByMessageType() — meaning behaviors execute around the entire message type dispatch, not just the user event handler. This gives middleware access to all message types (EventRequest, RetryRequest, ResubmissionRequest, etc.).
Three built-in middleware are provided: LoggingMiddleware, MetricsMiddleware, ValidationMiddleware.
A separate IMessageLifecycleObserver interface provides passive observation (OnReceived, OnCompleted, OnFailed, OnDeadLettered) for monitoring without affecting the pipeline flow.
Consequences
Positive
- Familiar pattern for .NET developers (similar to MediatR, MassTransit)
- Full
IMessageContextaccess — middleware can inspect/modify all message metadata - Short-circuit capability — middleware can skip processing without calling
next - Exception handling — middleware can catch, transform, or swallow exceptions
- Composable — multiple behaviors execute in registration order (nesting)
- Lifecycle observers are separate from pipeline (passive vs active)
Negative
- Middleware registered globally — cannot target specific message types (must check
context.MessageTypeinside the behavior) - No attribute-based middleware (must register in DI)
- Pipeline adds overhead for each message (minimal — delegate invocation is fast)