Pipeline Behaviors
Pipeline behaviors allow you to add cross-cutting concerns to your request handling pipeline.
v2 Changes
In v2, behavior execution order is controlled by the [BehaviorOrder] attribute at compile time:
[BehaviorOrder(1)] // Executes first
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { }
[BehaviorOrder(2)] // Executes second
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { }
⚠️ v2 Change: DI registration order no longer determines behavior execution order. Use
[BehaviorOrder]instead.
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 (Required for v2)
When using source-generated registration, behaviors are automatically discovered and registered — no manual AddOpenBehavior(...) call is required or supported.
If your behaviors are in the same project as the source generator, AddGeneratedHandlers() will discover and register them automatically with ordering from [BehaviorOrder]:
using MediatorLite.Generated;
services
.AddGeneratedHandlers() // Discovers and registers ALL behaviors with [BehaviorOrder] ordering
.AddMediatorLite(); // Takes no arguments; mediator is always registered as Transient
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 ordered by [BehaviorOrder].
Manual Registration (Not Supported)
⚠️ Not supported in v2: There is no reflection fallback, so manually registered behaviors are never dispatched without
AddGeneratedHandlers(). Worse, hand-registering behaviors alongside source generation double-registers them — the generator already discovered and registered every open-generic and closed behavior. Never callservices.AddTransient(typeof(IPipelineBehavior<,>), ...)in a source-generated consumer.
Key Rule: AddGeneratedHandlers() is the only registration path in v2; behavior ordering is controlled exclusively by [BehaviorOrder].
Execution Order (v2)
In v2, behaviors execute in [BehaviorOrder] attribute order (lowest number first):
Request -> [BehaviorOrder(1)] LoggingBehavior -> [BehaviorOrder(2)] ValidationBehavior -> Handler -> ValidationBehavior -> LoggingBehavior -> Response
Using BehaviorOrderAttribute
[BehaviorOrder(1)] // Executes first
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
// ...
}
[BehaviorOrder(2)] // Executes second
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
// ...
}
[BehaviorOrder(100)] // Executes last (just before handler)
public class TransactionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
// ...
}
Behaviors without [BehaviorOrder] default to order 0.
Tip: Use gaps in ordering (1, 10, 100) to allow inserting new behaviors without renumbering. ```
Common Behavior Patterns
Validation Behavior
You do not need to write your own validation behavior. MediatorLite provides FluentValidation integration out of the box: add the MediatorLite.FluentValidation package, write AbstractValidator<T> validators, and the source generator wires FluentValidationBehavior<,> as the outermost behavior automatically. See Validation.
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;
} }