Pipeline Behaviors
Pipeline behaviors allow you to add cross-cutting concerns to your request handling pipeline.
Open vs Closed Behaviors
Open behavior = an open generic type definition (e.g., LoggingBehavior<,>) that can be applied to any request/response pair. It is resolved by DI for each concrete request at runtime.
Closed behavior = a concrete type (non-generic or fully closed generic) that targets a specific request/response pair.
Example request/response:
public sealed record CreateOrder(string ProductId) : IRequest<OrderResult>;
public sealed record OrderResult(Guid OrderId);
Open behavior example
public 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 ValueTask<TResponse> HandleAsync(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken = default)
{
_logger.LogInformation("Handling {RequestType}", typeof(TRequest).Name);
var stopwatch = Stopwatch.StartNew();
var response = await next(); // Call the next behavior or handler
stopwatch.Stop();
_logger.LogInformation("Handled {RequestType} in {ElapsedMs}ms",
typeof(TRequest).Name, stopwatch.ElapsedMilliseconds);
return response;
}
}
Closed behavior example
public sealed class CreateOrderLoggingBehavior
: IPipelineBehavior<CreateOrder, OrderResult>
{
public async ValueTask<OrderResult> HandleAsync(
CreateOrder request,
RequestHandlerDelegate<OrderResult> next,
CancellationToken cancellationToken = default)
{
return await next();
}
}
Registering Behaviors
Source-Generated Registration (Recommended)
When using source-generated registration, behaviors are automatically discovered and registered. Do NOT use MediatorOptions.AddOpenBehavior() - it’s not needed.
If your behaviors are in the same project as the source generator, AddGeneratedHandlers() will discover and register them automatically:
using MediatorLite.Generated;
services
.AddGeneratedHandlers() // Discovers and registers ALL behaviors automatically
.AddMediatorLite(); // No need to call options.AddOpenBehavior()
To register only behaviors from the source generator:
services
.AddGeneratedBehaviors() // Only pipeline behaviors
.AddMediatorLite();
Important: The source generator discovers both open generic behaviors (e.g., LoggingBehavior<,>) and closed behaviors (e.g., CreateOrderValidationBehavior). They are registered directly in DI and will be resolved automatically by the mediator.
Manual Registration (Without Source Generation)
When NOT using source-generated registration, you have two options:
Option 1: Via MediatorOptions (Recommended for manual registration)
Register open generic behaviors through MediatorOptions. This automatically adds them to DI:
services.AddMediatorLite(options =>
{
options.AddOpenBehavior(typeof(LoggingBehavior<,>));
options.AddOpenBehavior(typeof(ValidationBehavior<,>));
});
Register closed behaviors:
services.AddMediatorLite(options =>
{
options.AddBehavior<CreateOrderAuthorizationBehavior>();
});
Option 2: Direct DI Registration
You can also register behaviors directly with the DI container:
// Open generic - applies to all requests
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
// Closed type - applies to specific request only
services.AddTransient<IPipelineBehavior<CreateOrder, OrderResult>, CreateOrderLoggingBehavior>();
services.AddMediatorLite();
Summary: Source-Gen vs Manual Registration
| Method | When to Use | Behavior Registration |
|---|---|---|
| Source-Generated | Recommended for all projects | AddGeneratedHandlers() - behaviors auto-discovered, DO NOT use options.AddOpenBehavior() |
| Manual via Options | When NOT using source-gen | options.AddOpenBehavior() or options.AddBehavior<T>() |
| Manual via DI | When NOT using source-gen | services.AddTransient(typeof(IPipelineBehavior<,>), typeof(Behavior<,>)) |
Key Rule: If you call AddGeneratedHandlers() or AddGeneratedBehaviors(), the source generator handles all behavior registration. Do not mix source-gen with MediatorOptions.AddOpenBehavior() for the same behavior - it will be registered twice.
Execution Order
Behaviors execute in registration order (first registered = first executed):
Request -> LoggingBehavior -> ValidationBehavior -> Handler -> ValidationBehavior -> LoggingBehavior -> Response
Using BehaviorOrderAttribute
[BehaviorOrder(1)] // Executes first
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { }
[BehaviorOrder(2)] // Executes second
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { }
Common Behavior Patterns
Validation Behavior
public 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 ValueTask<TResponse> HandleAsync(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken = default)
{
foreach (var validator in _validators)
{
var result = await validator.ValidateAsync(request, cancellationToken);
if (!result.IsValid)
{
throw new ValidationException(result.Errors);
}
}
return await next();
}
}
Transaction Behavior
public class TransactionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IDbContext _dbContext;
public TransactionBehavior(IDbContext dbContext)
{
_dbContext = dbContext;
}
public async ValueTask<TResponse> HandleAsync(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken = default)
{
await using var transaction = await _dbContext.BeginTransactionAsync(cancellationToken);
try
{
var response = await next();
await transaction.CommitAsync(cancellationToken);
return response;
}
catch
{
await transaction.RollbackAsync(cancellationToken);
throw;
}
}
}
Short-Circuiting
Behaviors can short-circuit the pipeline by not calling next():
```csharp public class CachingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest
public async ValueTask<TResponse> HandleAsync(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken = default)
{
var cacheKey = GenerateCacheKey(request);
if (_cache.TryGet<TResponse>(cacheKey, out var cached))
{
return cached!; // Don't call next(), return cached value
}
var response = await next();
_cache.Set(cacheKey, response);
return response;
} }