Real-world applications combine multiple pipeline behaviors to implement cross-cutting concerns. This guide shows common patterns used in production with DSoftStudio.Mediator.
Log every request with timing information:
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> Handle(
TRequest request,
IRequestHandler<TRequest, TResponse> next,
CancellationToken ct)
{
var name = typeof(TRequest).Name;
_logger.LogInformation("Handling {Request}", name);
var sw = Stopwatch.StartNew();
var response = await next.Handle(request, ct);
sw.Stop();
_logger.LogInformation("Handled {Request} in {Elapsed}ms", name, sw.ElapsedMilliseconds);
return response;
}
}
Wrap commands in a database transaction using the ICommand marker:
public class TransactionBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly AppDbContext _db;
public TransactionBehavior(AppDbContext db) => _db = db;
public async ValueTask<TResponse> Handle(
TRequest request,
IRequestHandler<TRequest, TResponse> next,
CancellationToken ct)
{
if (request is not ICommand)
return await next.Handle(request, ct);
await using var tx = await _db.Database.BeginTransactionAsync(ct);
var response = await next.Handle(request, ct);
await _db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
return response;
}
}
Tip: Use the non-generic
ICommandmarker interface to target only write operations. Queries skip the transaction entirely.
Enforce permissions before the handler executes:
public interface IAuthorizedRequest
{
string RequiredPermission { get; }
}
public class AuthorizationBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly ICurrentUser _user;
public AuthorizationBehavior(ICurrentUser user) => _user = user;
public async ValueTask<TResponse> Handle(
TRequest request,
IRequestHandler<TRequest, TResponse> next,
CancellationToken ct)
{
if (request is IAuthorizedRequest auth
&& !_user.HasPermission(auth.RequiredPermission))
{
throw new UnauthorizedAccessException(
$"Missing permission: {auth.RequiredPermission}");
}
return await next.Handle(request, ct);
}
}
Retry transient failures for read operations:
public class RetryBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
public async ValueTask<TResponse> Handle(
TRequest request,
IRequestHandler<TRequest, TResponse> next,
CancellationToken ct)
{
if (request is not IQuery)
return await next.Handle(request, ct);
const int maxRetries = 3;
for (int i = 0; ; i++)
{
try
{
return await next.Handle(request, ct);
}
catch when (i < maxRetries - 1)
{
await Task.Delay(100 * (i + 1), ct);
}
}
}
}
Warning: Retrying commands can cause duplicate side effects. Only retry idempotent read operations, or use idempotency keys for writes.
Pipeline behaviors execute in registration order. A typical production setup:
services
.AddMediator()
.RegisterMediatorHandlers()
.AddPipelineBehavior(typeof(LoggingBehavior<,>))
.AddPipelineBehavior(typeof(AuthorizationBehavior<,>))
.AddPipelineBehavior(typeof(ValidationBehavior<,>))
.AddPipelineBehavior(typeof(TransactionBehavior<,>))
.PrecompilePipelines();
Execution flow:
Request
→ LoggingBehavior (log entry + timing)
→ AuthorizationBehavior (check permissions)
→ ValidationBehavior (validate input)
→ TransactionBehavior (wrap in tx if command)
→ Handler (execute business logic)
ICommand / IQuery marker interfaces for targeting behaviors