For cross-cutting concerns that only need a “before” or “after” hook, pre/post processors are simpler than full pipeline behaviors — no next parameter, no chain responsibility.
PreProcessor₁ → PreProcessor₂ → Handler → PostProcessor₁ → PostProcessor₂
Run before the handler. If a pre-processor throws, the handler is not invoked.
public class ValidationPreProcessor<TRequest> : IRequestPreProcessor<TRequest>
{
public ValueTask Process(TRequest request, CancellationToken ct)
{
// Validate before the handler runs — throw to short-circuit
if (request is ICommand command)
Console.WriteLine($"Validating {typeof(TRequest).Name}");
return ValueTask.CompletedTask;
}
}
Run after the handler completes successfully. Not invoked if the handler throws.
public class AuditPostProcessor<TRequest, TResponse>
: IRequestPostProcessor<TRequest, TResponse>
{
public ValueTask Process(TRequest request, TResponse response, CancellationToken ct)
{
Console.WriteLine($"{typeof(TRequest).Name} → {response}");
return ValueTask.CompletedTask;
}
}
Register as open generics:
services.AddTransient(typeof(IRequestPreProcessor<>), typeof(ValidationPreProcessor<>));
services.AddTransient(typeof(IRequestPostProcessor<,>), typeof(AuditPostProcessor<,>));