MediatR Pipeline Behaviors
Overview
Pipeline Behaviors implement cross-cutting concerns that execute before/after every command or query handler:
- Validation - Validate requests before handler executes
- Logging - Log request/response details
- Exception Handling - Convert exceptions to Results
- Transaction - Wrap handlers in database transactions
- Caching - Cache query results
- Performance - Monitor slow operations
Quick Reference
| Behavior | Purpose | Order |
|---|---|---|
| LoggingBehavior | Log requests | First (outer) |
| ValidationBehavior | Validate input | Second |
| ExceptionHandlingBehavior | Convert exceptions | Third |
| TransactionBehavior | Database transaction | Fourth |
| CachingBehavior | Cache responses | Fifth (inner) |
Behavior Structure
/Application/Abstractions/Behaviors/
├── LoggingBehavior.cs
├── ValidationBehavior.cs
├── ExceptionHandlingBehavior.cs
├── TransactionBehavior.cs
├── QueryCachingBehavior.cs
└── PerformanceBehavior.cs
Template: Logging Behavior
// src/{name}.application/Abstractions/Behaviors/LoggingBehavior.cs
using MediatR;
using Microsoft.Extensions.Logging;
using Serilog.Context;
namespace {name}.application.abstractions.behaviors;
/// <summary>
/// Logs all requests and responses with timing information
/// </summary>
public sealed class LoggingBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
{
_logger = logger;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
var requestName = typeof(TRequest).Name;
var requestId = Guid.NewGuid();
using (LogContext.PushProperty("RequestId", requestId))
using (LogContext.PushProperty("RequestName", requestName))
{
_logger.LogInformation(
"Handling {RequestName} ({RequestId})",
requestName,
requestId);
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
var response = await next();
stopwatch.Stop();
_logger.LogInformation(
"Handled {RequestName} ({RequestId}) in {ElapsedMs}ms",
requestName,
requestId,
stopwatch.ElapsedMilliseconds);
return response;
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(
ex,
"Error handling {RequestName} ({RequestId}) after {ElapsedMs}ms",
requestName,
requestId,
stopwatch.ElapsedMilliseconds);
throw;
}
}
}
}
Template: Validation Behavior
// src/{name}.application/Abstractions/Behaviors/ValidationBehavior.cs
using FluentValidation;
using MediatR;
using {name}.domain.abstractions;
namespace {name}.application.abstractions.behaviors;
/// <summary>
/// Validates requests using FluentValidation validators
/// Returns ValidationResult with errors instead of throwing
/// </summary>
public sealed class ValidationBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
if (!_validators.Any())
{
return await next();
}
var context = new ValidationContext<TRequest>(request);
var validationResults = await Task.WhenAll(
_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
var errors = validationResults
.SelectMany(result => result.Errors)
.Where(failure => failure is not null)
.Select(failure => new Error(
failure.PropertyName,
failure.ErrorMessage))
.Distinct()
.ToArray();
if (errors.Length != 0)
{
return CreateValidationResult<TResponse>(errors);
}
return await next();
}
private static TResponse CreateValidationResult<TResponse>(Error[] errors)
{
// Handle Result type
if (typeof(TResponse) == typeof(Result))
{
return (TResponse)(object)ValidationResult.WithErrors(errors);
}
// Handle Result<T> type
var resultType = typeof(TResponse);
if (resultType.IsGenericType &&
resultType.GetGenericTypeDefinition() == typeof(Result<>))
{
var valueType = resultType.GetGenericArguments()[0];
var validationResultType = typeof(ValidationResult<>).MakeGenericType(valueType);
var validationResult = Activator.CreateInstance(
validationResultType,
BindingFlags.Instance | BindingFlags.NonPublic,
null,
new object[] { errors },
null);
return (TResponse)validationResult!;
}
throw new InvalidOperationException(
$"Cannot create validation result for type {typeof(TResponse).Name}");
}
}
Template: Exception Handling Behavior
// src/{name}.application/Abstractions/Behaviors/ExceptionHandlingBehavior.cs
using MediatR;
using Microsoft.Extensions.Logging;
using {name}.domain.abstractions;
namespace {name}.application.abstractions.behaviors;
/// <summary>
/// Catches unhandled exceptions and converts them to Result.Failure
/// </summary>
public sealed class ExceptionHandlingBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
where TResponse : Result
{
private readonly ILogger<ExceptionHandlingBehavior<TRequest, TResponse>> _logger;
public ExceptionHandlingBehavior(
ILogger<ExceptionHandlingBehavior<TRequest, TResponse>> logger)
{
_logger = logger;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
try
{
return await next();
}
catch (Exception ex)
{
var requestName = typeof(TRequest).Name;
_logger.LogError(
ex,
"Unhandled exception for request {RequestName}",
requestName);
return CreateExceptionResult<TResponse>(ex);
}
}
private static TResponse CreateExceptionResult<TResponse>(Exception exception)
{
var error = new Error(
"Error.Unhandled",
exception.Message);
if (typeof(TResponse) == typeof(Result))
{
return (TResponse)(object)Result.Failure(error);
}
var resultType = typeof(TResponse);
if (resultType.IsGenericType &&
resultType.GetGenericTypeDefinition() == typeof(Result<>))
{
var valueType = resultType.GetGenericArguments()[0];
var failureMethod = typeof(Result)
.GetMethod(nameof(Result.Failure), new[] { typeof(Error) })!
.MakeGenericMethod(valueType);
return (TResponse)failureMethod.Invoke(null, new object[] { error })!;
}