Skip to main content
Pipeline behaviors in MediatR provide a way to add cross-cutting concerns to your command and query handlers. The BuildingBlocks library includes two essential behaviors: ValidationBehavior and LoggingBehavior.

Overview

Behaviors wrap around handler execution, forming a pipeline where each behavior can execute logic before and after the handler runs. They are similar to middleware in ASP.NET Core.
Request → ValidationBehavior → LoggingBehavior → Handler → Response

ValidationBehavior

The ValidationBehavior automatically validates commands using FluentValidation before they reach the handler.

Implementation

BuildingBlocks/Behaviors/ValidationBehavior.cs
using BuildingBlocks.CQRS;
using FluentValidation;
using MediatR;

namespace BuildingBlocks.Behaviors;

public class ValidationBehavior<TRequest, TResponse>
    (IEnumerable<IValidator<TRequest>> validators)
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : ICommand<TResponse>
{
    public async Task<TResponse> Handle(
        TRequest request, 
        RequestHandlerDelegate<TResponse> next, 
        CancellationToken cancellationToken)
    {
        var context = new ValidationContext<TRequest>(request);

        var validationResults =
            await Task.WhenAll(
                validators.Select(v => v.ValidateAsync(context, cancellationToken)));

        var failures =
            validationResults
            .Where(r => r.Errors.Any())
            .SelectMany(r => r.Errors)
            .ToList();

        if (failures.Any())
            throw new ValidationException(failures);

        return await next();
    }
}

Key Features

  • Automatic Validation: Runs all registered validators for the command
  • Parallel Execution: Validates using Task.WhenAll for performance
  • Early Exit: Throws ValidationException before handler executes if validation fails
  • Commands Only: Only applies to ICommand<TResponse> types, not queries

Creating Validators

Create validators using FluentValidation:
public record CreateOrderCommand(
    Guid CustomerId,
    List<OrderItemDto> Items
) : ICommand<CreateOrderResult>;

public class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
{
    public CreateOrderCommandValidator()
    {
        RuleFor(x => x.CustomerId)
            .NotEmpty()
            .WithMessage("Customer ID is required");
        
        RuleFor(x => x.Items)
            .NotEmpty()
            .WithMessage("Order must contain at least one item");
        
        RuleForEach(x => x.Items)
            .SetValidator(new OrderItemDtoValidator());
    }
}

public class OrderItemDtoValidator : AbstractValidator<OrderItemDto>
{
    public OrderItemDtoValidator()
    {
        RuleFor(x => x.ProductId)
            .NotEmpty()
            .WithMessage("Product ID is required");
        
        RuleFor(x => x.Quantity)
            .GreaterThan(0)
            .WithMessage("Quantity must be greater than 0");
        
        RuleFor(x => x.Price)
            .GreaterThanOrEqualTo(0)
            .WithMessage("Price cannot be negative");
    }
}

Registration

Register validators and the validation behavior:
public static IServiceCollection AddApplicationServices(
    this IServiceCollection services)
{
    // Register MediatR
    services.AddMediatR(config =>
    {
        config.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
        
        // Register validation behavior
        config.AddOpenBehavior(typeof(ValidationBehavior<,>));
    });
    
    // Register all validators in the assembly
    services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
    
    return services;
}

LoggingBehavior

The LoggingBehavior logs request execution, measures performance, and warns about slow operations.

Implementation

BuildingBlocks/Behaviors/LoggingBehavior.cs
using MediatR;
using Microsoft.Extensions.Logging;
using System.Diagnostics;

namespace BuildingBlocks.Behaviors;

public class LoggingBehavior<TRequest, TResponse>
    (ILogger<LoggingBehavior<TRequest, TResponse>> logger)
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull, IRequest<TResponse>
    where TResponse : notnull
{
    public async Task<TResponse> Handle(
        TRequest request, 
        RequestHandlerDelegate<TResponse> next, 
        CancellationToken cancellationToken)
    {
        logger.LogInformation(
            "[START] Handle request={Request} - Response={Response} - RequestData={RequestData}",
            typeof(TRequest).Name, typeof(TResponse).Name, request);

        var timer = new Stopwatch();
        timer.Start();

        var response = await next();

        timer.Stop();
        var timeTaken = timer.Elapsed;
        
        if (timeTaken.Seconds > 3) // if the request is greater than 3 seconds, then log the warnings
            logger.LogWarning(
                "[PERFORMANCE] The request {Request} took {TimeTaken} seconds.",
                typeof(TRequest).Name, timeTaken.Seconds);

        logger.LogInformation(
            "[END] Handled {Request} with {Response}", 
            typeof(TRequest).Name, typeof(TResponse).Name);
        
        return response;
    }
}

Key Features

  • Request Logging: Logs the start of request processing with request data
  • Performance Monitoring: Measures execution time using Stopwatch
  • Performance Warnings: Logs warnings for requests taking more than 3 seconds
  • Completion Logging: Logs when request handling completes
  • Universal: Applies to both commands and queries

Log Output Examples

Normal Request:
[11:23:45 INF] [START] Handle request=CreateOrderCommand - Response=CreateOrderResult - RequestData=CreateOrderCommand { CustomerId = a1b2c3d4, Items = [...] }
[11:23:45 INF] [END] Handled CreateOrderCommand with CreateOrderResult
Slow Request:
[11:23:45 INF] [START] Handle request=GenerateReportQuery - Response=ReportDto - RequestData=GenerateReportQuery { StartDate = 2024-01-01, EndDate = 2024-12-31 }
[11:23:49 WRN] [PERFORMANCE] The request GenerateReportQuery took 4 seconds.
[11:23:49 INF] [END] Handled GenerateReportQuery with ReportDto

Registration

Register the logging behavior in your dependency injection:
public static IServiceCollection AddApplicationServices(
    this IServiceCollection services)
{
    services.AddMediatR(config =>
    {
        config.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
        
        // Register behaviors in order
        config.AddOpenBehavior(typeof(ValidationBehavior<,>));
        config.AddOpenBehavior(typeof(LoggingBehavior<,>));
    });
    
    return services;
}

Behavior Order

The order in which behaviors are registered matters. They form a pipeline:
Request

1. LoggingBehavior (logs start)

2. ValidationBehavior (validates, may throw)

3. Handler (processes request)

4. ValidationBehavior (returns)

5. LoggingBehavior (logs end, measures time)

Response
Typical Order:
config.AddOpenBehavior(typeof(LoggingBehavior<,>));     // Outer - logs everything
config.AddOpenBehavior(typeof(ValidationBehavior<,>));  // Inner - validates before handler
This ensures that validation errors are logged by the LoggingBehavior.

Creating Custom Behaviors

You can create additional behaviors for other cross-cutting concerns:

Authorization Behavior Example

public class AuthorizationBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly ICurrentUserService _currentUserService;
    private readonly IAuthorizationService _authorizationService;
    
    public AuthorizationBehavior(
        ICurrentUserService currentUserService,
        IAuthorizationService authorizationService)
    {
        _currentUserService = currentUserService;
        _authorizationService = authorizationService;
    }
    
    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        var authorizeAttributes = request.GetType()
            .GetCustomAttributes<AuthorizeAttribute>()
            .ToList();
        
        if (authorizeAttributes.Any())
        {
            var user = _currentUserService.User;
            
            if (user == null)
                throw new UnauthorizedAccessException();
            
            foreach (var attribute in authorizeAttributes)
            {
                var authorized = await _authorizationService
                    .AuthorizeAsync(user, attribute.Policy);
                
                if (!authorized)
                    throw new ForbiddenAccessException();
            }
        }
        
        return await next();
    }
}

Caching Behavior Example

public class CachingBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IQuery<TResponse>
{
    private readonly IDistributedCache _cache;
    
    public CachingBehavior(IDistributedCache cache)
    {
        _cache = cache;
    }
    
    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        var cacheKey = $"{typeof(TRequest).Name}:{JsonSerializer.Serialize(request)}";
        
        var cachedResponse = await _cache.GetStringAsync(cacheKey, cancellationToken);
        
        if (cachedResponse != null)
        {
            return JsonSerializer.Deserialize<TResponse>(cachedResponse)!;
        }
        
        var response = await next();
        
        await _cache.SetStringAsync(
            cacheKey,
            JsonSerializer.Serialize(response),
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
            },
            cancellationToken);
        
        return response;
    }
}

Best Practices

Behaviors should handle cross-cutting concerns that apply to many requests, not specific business logic.
// Good - Generic concern
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>

// Avoid - Specific business logic
public class OrderProcessingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
Behaviors execute for every request. Keep them lightweight and efficient.
// Good - Fast validation
var failures = validationResults
    .Where(r => r.Errors.Any())
    .SelectMany(r => r.Errors)
    .ToList();

// Avoid - Slow operations
await Task.Delay(1000); // Don't add artificial delays
var data = await _externalApi.Call(); // Don't call external APIs
Exceptions thrown in behaviors prevent the handler from executing.
// Good - Throw for validation failures
if (failures.Any())
    throw new ValidationException(failures);

// Avoid - Swallowing exceptions
try { await next(); }
catch { return default; } // Don't hide errors

CQRS Pattern

Learn about commands and queries that behaviors wrap

Exception Handling

See how validation exceptions are handled globally

Build docs developers (and LLMs) love