In traditional API engineering, validation architectures suffer from three critical flaws:
- Exceptions as Flow Control: Throwing
ValidationExceptionon bad inputs, causing heavy CPU and GC overhead under high concurrency; - Boilerplate in Handlers: Handlers cluttered with defensive
if (string.IsNullOrEmpty(...))checks; - Opaque Error Messages: Free-text errors preventing frontends from mapping validation issues to specific form fields.
BitzOrcas.Modern implements a pure-function validation architecture powered by IRequestRule<TRequest>: Validation logic is physically separated from handlers and executed before transactions by ValidationPipelineBehavior, returning structured, typed Result objects.
Functional Validation Pipeline Lifecycle
Step 1: Writing Pure IRequestRule Validations
Implement IRequestRule<TRequest> for contract validation:
using BitzOrcas.Application.Abstractions.Validation;using BitzOrcas.Domain.Results;using BitzOrcas.Customer.Contracts.Commands;
namespace BitzOrcas.Customer.Application.Rules;
public static class CustomerErrors{ public static readonly Error NameRequired = Error.Validation("Customer.NameRequired", "Customer name is required.");
public static readonly Error NameTooLong = Error.Validation("Customer.NameTooLong", "Customer name cannot exceed 100 characters.");}
// Enforces non-empty and bounded length invariants for customer namespublic sealed class CustomerNameMustBeValidRule : IRequestRule<CreateCustomerCommand>{ public ValueTask<Result> ValidateAsync(CreateCustomerCommand request, CancellationToken ct) { // 1. Pure in-memory assertions with zero I/O side-effects if (string.IsNullOrWhiteSpace(request.CustomerName)) { return ValueTask.FromResult(Result.Failure(CustomerErrors.NameRequired)); }
if (request.CustomerName.Length > 100) { return ValueTask.FromResult(Result.Failure(CustomerErrors.NameTooLong)); }
// 2. Validation successful return ValueTask.FromResult(Result.Success()); }}Step 2: Zero-Boilerplate Pipeline Interception
Rules are registered automatically in DI. ValidationPipelineBehavior executes matching rules sequentially before handlers run:
public sealed class ValidationPipelineBehavior<TRequest, TResponse>( IEnumerable<IRequestRule<TRequest>> rules) : IPipelineBehavior<TRequest, TResponse>{ public async ValueTask<TResponse> Handle( TRequest message, CancellationToken cancellationToken, MessageHandlerDelegate<TRequest, TResponse> next) { // 1. Iterate through all registered request rules foreach (var rule in rules) { var result = await rule.ValidateAsync(message, cancellationToken); if (result.IsFailure) { // 2. Short-circuit on first validation failure without throwing exceptions return (TResponse)(object)result; } }
// 3. Proceed to inner handler return await next(message, cancellationToken); }}Summary
Functional validation delivers resilience and clean code:
- Zero Exception Overhead: Microsecond execution with zero GC allocations;
- Clean Handlers: Handlers focus purely on domain logic;
- Typed Error Codes: Frontends map
ErrorCodedirectly to localized UI forms.