| name | pipeline-behaviors |
| description | Use when adding cross-cutting MediatR pipeline behaviors for validation, logging, or transactions.
|
| metadata | {"category":"cqrs","agent":"ef-specialist","when-to-use":"When creating MediatR pipeline behaviors for validation, logging, or transactions"} |
Pipeline Behaviors
Core Principles
- Pipeline behaviors wrap every MediatR request like middleware
- Registration order matters: first registered = outermost in the pipeline
- Use open generic registration so behaviors apply to all requests
- Constrain behaviors with marker interfaces when needed
- Common behaviors: validation, logging, performance, transaction
Patterns
Validation Behavior
public sealed class ValidationBehavior<TRequest, TResponse>(
IEnumerable<IValidator<TRequest>> validators)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
if (!validators.Any())
return await next();
var context = new ValidationContext<TRequest>(request);
var results = await Task.WhenAll(
validators.Select(v => v.ValidateAsync(context, ct)));
var failures = results
.SelectMany(r => r.Errors)
.Where(f => f is not null)
.ToList();
if (failures.Count != 0)
throw new ValidationException(failures);
return await next();
}
}
Logging Behavior
public sealed class LoggingBehavior<TRequest, TResponse>(
ILogger<LoggingBehavior<TRequest, TResponse>> logger)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
var requestName = typeof(TRequest).Name;
logger.LogInformation(
"Handling {RequestName}: {@Request}",
requestName, request);
var sw = Stopwatch.StartNew();
var response = await next();
sw.Stop();
if (sw.ElapsedMilliseconds > 500)
{
logger.LogWarning(
"Long-running request {RequestName}: {ElapsedMs}ms",
requestName, sw.ElapsedMilliseconds);
}
else
{
logger.LogInformation(
"Handled {RequestName} in {ElapsedMs}ms",
requestName, sw.ElapsedMilliseconds);
}
return response;
}
}
Performance Behavior
public sealed class PerformanceBehavior<TRequest, TResponse>(
ILogger<PerformanceBehavior<TRequest, TResponse>> logger)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
private const int ThresholdMs = 500;
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
var sw = Stopwatch.StartNew();
var response = await next();
sw.Stop();
if (sw.ElapsedMilliseconds > ThresholdMs)
{
logger.LogWarning(
"Slow request detected: {RequestName} took {ElapsedMs}ms. " +
"Request: {@Request}",
typeof(TRequest).Name,
sw.ElapsedMilliseconds,
request);
}
return response;
}
}
Transaction Behavior
public interface ITransactionalRequest { }
public sealed class TransactionBehavior<TRequest, TResponse>(
AppDbContext db,
ILogger<TransactionBehavior<TRequest, TResponse>> logger)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : ITransactionalRequest
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
var strategy = db.Database.CreateExecutionStrategy();
return await strategy.ExecuteAsync(async () =>
{
await using var transaction =
await db.Database.BeginTransactionAsync(ct);
logger.LogInformation(
"Begin transaction for {RequestName}",
typeof(TRequest).Name);
var response = await next();
await transaction.CommitAsync(ct);
logger.LogInformation(
"Committed transaction for {RequestName}",
typeof(TRequest).Name);
return response;
});
}
}
Unhandled Exception Behavior
public sealed class UnhandledExceptionBehavior<TRequest, TResponse>(
ILogger<UnhandledExceptionBehavior<TRequest, TResponse>> logger)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
try
{
return await next();
}
catch (Exception ex)
{
logger.LogError(ex,
"Unhandled exception for {RequestName}: {@Request}",
typeof(TRequest).Name, request);
throw;
}
}
}
Registration (Order Matters)
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
cfg.AddBehavior(typeof(IPipelineBehavior<,>),
typeof(UnhandledExceptionBehavior<,>));
cfg.AddBehavior(typeof(IPipelineBehavior<,>),
typeof(LoggingBehavior<,>));
cfg.AddBehavior(typeof(IPipelineBehavior<,>),
typeof(ValidationBehavior<,>));
cfg.AddBehavior(typeof(IPipelineBehavior<,>),
typeof(PerformanceBehavior<,>));
cfg.AddBehavior(typeof(IPipelineBehavior<,>),
typeof(TransactionBehavior<,>));
});
builder.Services.AddValidatorsFromAssembly(
typeof(Program).Assembly);
Pipeline Flow
Request arrives
→ UnhandledExceptionBehavior (catch + log)
→ LoggingBehavior (log entry/exit + timing)
→ ValidationBehavior (FluentValidation)
→ PerformanceBehavior (warn if slow)
→ TransactionBehavior (if ITransactionalRequest)
→ Handler (actual business logic)
Anti-Patterns
- Registering behaviors in wrong order (logging should be before validation)
- Behavior that modifies the request (behaviors are for cross-cutting, not logic)
- Transaction behavior on all requests (use marker interface for opt-in)
- Swallowing exceptions in behaviors (always re-throw)
- Heavy computation in behaviors (keep them lightweight)
Detect Existing Patterns
- Search for
IPipelineBehavior< implementations
- Look for
AddBehavior calls in MediatR registration
- Check for
Behaviors/ folder in the project
- Look for
ITransactionalRequest or similar marker interfaces
- Search for
ValidationBehavior or LoggingBehavior classes
Adding to Existing Project
- Create behaviors — start with validation and logging
- Register in correct order — exception → logging → validation → transaction
- Add marker interfaces for opt-in behaviors (e.g.,
ITransactionalRequest)
- Install FluentValidation for the validation behavior
- Test behavior ordering — verify logging captures validation failures
References