Introduction
MediatR is a widely used library in .NET applications that follows the mediator pattern, helping to decouple requests from handlers. While it simplifies CQRS (Command Query Responsibility Segregation), it also offers a robust feature called Pipeline Behaviors.
These behaviors enable developers to intercept requests and implement cross-cutting concerns such as logging, validation , performance tracking , transaction management , and error handling in a structured and reusable manner.
Source Code
You can find the complete source code for this tutorial at:
👉 GitHub Repository
Understanding MediatR Behaviors and the Decorator Pattern
MediatR Behaviors function as middleware, allowing developers to execute logic before and after request handlers.
The Decorator Pattern in MediatR Behaviors
MediatR Behaviors follow the Decorator Pattern, a structural design pattern that allows additional responsibilities to be dynamically added to an object without modifying its code.
Instead of modifying request handlers directly, behaviors act as layers around the handler execution, applying cross-cutting concerns transparently.
How MediatR Implements the Decorator Pattern
Each MediatR behavior wraps the handler, decorating it with additional functionality. This follows the Open-Closed Principle (OCP), as we can extend behavior without modifying the existing handler.
Example: Applying the Decorator Pattern in MediatR Behaviors
When MediatR processes a request, it applies behaviors in order, wrapping the original handler:
1️⃣ ValidationBehavior runs first and validates the request.
2️⃣ LoggingBehavior logs the request before and after execution.
3️⃣ Request Handler executes after all decorators have run.
Each behavior decorates the request handler, adding functionality without modifying it.
Why Use Behaviors?
Separation of concerns: Keep handlers focused on business logic.
Code reusability: Apply behaviors to multiple handlers.
Maintainability: Centralized logic makes updates easier.
Consistent processing: Ensure common concerns (e.g., logging, validation) are always executed.
Implementing a MediatR Behavior
To create a MediatR behavior, you implement IPipelineBehavior.
Example: Logging Behavior
using MediatR;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, 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)
{
_logger.LogInformation("Handling {RequestName}", typeof(TRequest).Name);
var response = await next(); // Call the next behavior or handler
_logger.LogInformation("Handled {RequestName}", typeof(TRequest).Name);
return response;
}
}
Registering the Behavior in DI
To make behaviors work, register them in the DI container:
builder.Services.AddMediatR(options =>
{
options.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
options.AddOpenBehavior(typeof(ValidationBehaviour<,>));
options.AddOpenBehavior(typeof(PerformanceBehaviour<,>));
options.AddOpenBehavior(typeof(UnhandledExceptionBehaviour<,>));
options.AddOpenBehavior(typeof(LoggingBehavior<,>));
});
More Use Cases for MediatR Behaviors :
Validation
Integrate FluentValidation to validate requests before handling.
using FluentValidation;
using MediatR;
public class ValidationBehaviour<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators) : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators = validators;
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
if (_validators.Any())
{
var context = new ValidationContext<TRequest>(request);
var validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
var failures = validationResults.SelectMany(r => r.Errors).Where(f => f != null).ToList();
if (failures.Any())
{
throw new ValidationException(failures);
}
}
return await next();
}
}
Performance Monitoring
Measure execution time using Stopwatch.
using MediatR;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
public class PerformanceBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
private readonly ILogger<PerformanceBehavior<TRequest, TResponse>> _logger;
public PerformanceBehavior(ILogger<PerformanceBehavior<TRequest, TResponse>> logger)
{
_logger = logger;
}
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
var stopwatch = Stopwatch.StartNew();
var response = await next();
stopwatch.Stop();
_logger.LogInformation("Request {Request} executed in {ElapsedMilliseconds}ms", typeof(TRequest).Name, stopwatch.ElapsedMilliseconds);
return response;
}
}
Transaction Handling
Wrap requests in a database transaction scope.
using MediatR;
using Microsoft.EntityFrameworkCore;
using System.Threading;
using System.Threading.Tasks;
public class TransactionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
private readonly DbContext _dbContext;
public TransactionBehavior(DbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
await using var transaction = await _dbContext.Database.BeginTransactionAsync(cancellationToken);
try
{
var response = await next();
await _dbContext.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
return response;
}
catch
{
await transaction.RollbackAsync(cancellationToken);
throw;
}
}
}
Exception Handling
Catch and log exceptions globally.
using MediatR;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
public class ExceptionHandlingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
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)
{
_logger.LogError(ex, "Request {Request} failed with exception {Message}", typeof(TRequest).Name, ex.Message);
throw;
}
}
}
**
Final Thoughts : **
MediatR Behaviors, leveraging the Decorator Pattern, provide an elegant solution for handling cross-cutting concerns in .NET applications. By combining multiple behaviors, developers can ensure cleaner, more maintainable, and efficient request processing
References
FluentValidation Documentation
Top comments (0)