Introduction
MediatR pipeline behaviors wrap request handlers like onion layers. The registration order determines the execution order: the first registered behavior is the outermost layer. When behaviors are registered in the wrong order, validation may run after the handler executes, logging may not capture the response, or authorization checks may be bypassed. Understanding the wrapping behavior is essential for building a correct CQRS pipeline.
Symptoms
- Validation behavior runs after the handler (too late to prevent execution)
- Logging behavior does not capture handler exceptions
- Caching behavior returns stale data because it runs before validation
- Authorization check runs after expensive handler logic
- Behavior registered but never executes
Debug behavior order: ```csharp public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
public async Task<TResponse> Handle( TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken) { _logger.LogInformation(">>> ENTERING {Behavior} for {Request}", nameof(LoggingBehavior), typeof(TRequest).Name);
var response = await next();
_logger.LogInformation("<<< EXITING {Behavior}", nameof(LoggingBehavior)); return response; } } ```
Common Causes
- Behaviors registered in wrong order in DI container
- Behavior not registered in DI container
next()not called in behavior, breaking the chain- Multiple registrations of the same behavior type
- Behavior uses constructor injection that fails silently
Step-by-Step Fix
- 1.Register behaviors in the correct order:
- 2.```csharp
- 3.// Registration order = outermost to innermost
- 4.// Think of it like an onion: first registered = outermost layer
builder.Services.AddMediatR(cfg => { cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
// Order matters! First registered is outermost: cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); // 1st (outermost) cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); // 2nd cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(AuthorizationBehavior<,>)); // 3rd cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(CachingBehavior<,>)); // 4th // Handler is innermost });
// Execution flow for a request: // Logging -> Validation -> Authorization -> Caching -> Handler // Response flows back: Handler -> Caching -> Authorization -> Validation -> Logging ```
- 1.**Ensure behavior calls next() in the chain":
- 2.```csharp
- 3.// WRONG - forgets to call next, breaks the pipeline
- 4.public class BrokenBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
- 5.{
- 6.public async Task<TResponse> Handle(
- 7.TRequest request,
- 8.RequestHandlerDelegate<TResponse> next,
- 9.CancellationToken cancellationToken)
- 10.{
- 11._logger.LogInformation("Before handler");
- 12.// Missing: return await next(); <-- Handler never runs!
- 13.return default;
- 14.}
- 15.}
// CORRECT - always call next() (unless intentionally short-circuiting) public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse> { private readonly IEnumerable<IValidator<TRequest>> _validators;
public async Task<TResponse> Handle( TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken) { // Short-circuit: do not call next() if validation fails var context = new ValidationContext<TRequest>(request); var failures = _validators .Select(v => v.Validate(context)) .SelectMany(result => result.Errors) .Where(f => f != null) .ToList();
if (failures.Count != 0) { throw new ValidationException(failures); // next() is NOT called - handler is skipped }
// Validation passed - continue the pipeline return await next(); } } ```
- 1.**Conditionally apply behaviors to specific requests":
- 2.```csharp
- 3.public class AuthorizationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
- 4.where TRequest : IRequest<TResponse>
- 5.{
- 6.public async Task<TResponse> Handle(
- 7.TRequest request,
- 8.RequestHandlerDelegate<TResponse> next,
- 9.CancellationToken cancellationToken)
- 10.{
- 11.// Only apply to requests that implement IAuthorizedRequest
- 12.if (request is IAuthorizedRequest authorizedRequest)
- 13.{
- 14.var hasPermission = await _authService.CheckPermissionAsync(
- 15.authorizedRequest.RequiredPermission);
if (!hasPermission) { throw new UnauthorizedAccessException( $"Permission '{authorizedRequest.RequiredPermission}' required"); } }
// For non-IAuthorizedRequest, pass through return await next(); } } ```
Prevention
- Document the behavior registration order with comments
- Name behaviors clearly to indicate their purpose and position
- Write integration tests that verify behavior execution order
- Add logging at the beginning and end of each behavior
- Use behavior constraints (
where TRequest : IValidatable) for conditional application - Test that short-circuiting behaviors (validation, auth) properly skip the handler