-
Notifications
You must be signed in to change notification settings - Fork 0
Pipeline Hooks
ResultR provides optional lifecycle hooks that run before and after your handler. These are defined as virtual methods on IRequestHandler<TRequest, TResponse> with default implementations, so you only override what you need.
1. ValidateAsync → Return failure to short-circuit
2. BeforeHandleAsync → Pre-processing (logging, setup)
3. HandleAsync → Your business logic (required)
4. AfterHandleAsync → Post-processing (logging, cleanup)
Validates the request before processing. Return Result.Failure() to short-circuit the pipeline.
public class CreateUserHandler : IRequestHandler<CreateUserRequest, User>
{
public ValueTask<Result> ValidateAsync(CreateUserRequest request)
{
if (string.IsNullOrWhiteSpace(request.Email))
return new(Result.Failure("Email is required"));
if (!request.Email.Contains('@'))
return new(Result.Failure("Invalid email format"));
if (string.IsNullOrWhiteSpace(request.Name))
return new(Result.Failure("Name is required"));
return new(Result.Success());
}
public async ValueTask<Result<User>> HandleAsync(
CreateUserRequest request,
CancellationToken cancellationToken)
{
// Only runs if validation passes
var user = new User(request.Email, request.Name);
await _repository.AddAsync(user, cancellationToken);
return Result<User>.Success(user);
}
}You can include an exception in validation failures:
public ValueTask<Result> ValidateAsync(ProcessPaymentRequest request)
{
try
{
// Complex validation that might throw
ValidateCreditCard(request.CardNumber);
return new(Result.Success());
}
catch (ValidationException ex)
{
return new(Result.Failure(ex.Message, ex));
}
}Runs after validation passes, before the main handler. Use for setup, logging, or cross-cutting concerns.
public class AuditedHandler : IRequestHandler<SensitiveRequest, SensitiveData>
{
private readonly ILogger<AuditedHandler> _logger;
private readonly IAuditService _audit;
public ValueTask BeforeHandleAsync(SensitiveRequest request)
{
_logger.LogInformation(
"Processing sensitive request for user {UserId}",
request.UserId);
_audit.LogAccess(request.UserId, "SensitiveData");
return default;
}
public async ValueTask<Result<SensitiveData>> HandleAsync(
SensitiveRequest request,
CancellationToken cancellationToken)
{
// Main logic
}
}Runs after the handler completes, regardless of success or failure. Receives the result for inspection.
public class MetricsHandler : IRequestHandler<OrderRequest, Order>
{
private readonly IMetricsService _metrics;
private readonly ILogger<MetricsHandler> _logger;
private Stopwatch? _stopwatch;
public ValueTask BeforeHandleAsync(OrderRequest request)
{
_stopwatch = Stopwatch.StartNew();
return default;
}
public async ValueTask<Result<Order>> HandleAsync(
OrderRequest request,
CancellationToken cancellationToken)
{
// Main logic
}
public ValueTask AfterHandleAsync(OrderRequest request, Result<Order> result)
{
_stopwatch?.Stop();
_metrics.RecordDuration("order_processing", _stopwatch?.ElapsedMilliseconds ?? 0);
if (result.IsSuccess)
{
_logger.LogInformation(
"Order {OrderId} created in {Duration}ms",
result.Value.Id,
_stopwatch?.ElapsedMilliseconds);
}
else
{
_logger.LogWarning(
"Order creation failed: {Error}",
result.Error);
_metrics.IncrementCounter("order_failures");
}
return default;
}
}A handler using all hooks:
public class CreateOrderHandler : IRequestHandler<CreateOrderRequest, Order>
{
private readonly IOrderRepository _repository;
private readonly ILogger<CreateOrderHandler> _logger;
public CreateOrderHandler(
IOrderRepository repository,
ILogger<CreateOrderHandler> logger)
{
_repository = repository;
_logger = logger;
}
// 1. Validation
public ValueTask<Result> ValidateAsync(CreateOrderRequest request)
{
if (request.Items.Count == 0)
return new(Result.Failure("Order must have at least one item"));
if (request.Items.Any(i => i.Quantity <= 0))
return new(Result.Failure("All items must have positive quantity"));
return new(Result.Success());
}
// 2. Before handling
public ValueTask BeforeHandleAsync(CreateOrderRequest request)
{
_logger.LogInformation(
"Creating order with {ItemCount} items for customer {CustomerId}",
request.Items.Count,
request.CustomerId);
return default;
}
// 3. Main handler
public async ValueTask<Result<Order>> HandleAsync(
CreateOrderRequest request,
CancellationToken cancellationToken)
{
var order = new Order(request.CustomerId, request.Items);
await _repository.AddAsync(order, cancellationToken);
return Result<Order>.Success(order);
}
// 4. After handling
public ValueTask AfterHandleAsync(CreateOrderRequest request, Result<Order> result)
{
if (result.IsSuccess)
{
_logger.LogInformation(
"Order {OrderId} created successfully",
result.Value.Id);
}
else
{
_logger.LogError(
"Failed to create order: {Error}",
result.Error);
}
return default;
}
}All hooks have default implementations, so you only override what you need:
// Interface defaults (you don't write these)
virtual ValueTask<Result> ValidateAsync(TRequest request) => new(Result.Success());
virtual ValueTask BeforeHandleAsync(TRequest request) => default;
virtual ValueTask AfterHandleAsync(TRequest request, Result<TResponse> result) => default;-
Validation failures short-circuit - If
ValidateAsyncreturns failure,BeforeHandleAsync,HandleAsync, andAfterHandleAsyncdo not run. -
AfterHandleAsync always runs - After
HandleAsynccompletes (success or failure),AfterHandleAsyncis called with the result. -
Exceptions are caught - Any exception in the pipeline (except
OperationCanceledException) is caught and converted to a failure result. -
No base class required - Just implement the interface and override the methods you need.
- Error Handling - Advanced error handling patterns
- Best Practices - Recommended patterns
Built with ❤️ for the C# / DotNet community.