Skip to content
bitzorcas
中EN

Guide

Functional Inbound Validation: IRequestRule and Anti-Corruption

Say goodbye to expensive validation exceptions! Master the BitzOrcas.Modern functional validation engine with IRequestRule declarative rules, pipeline guards, and typed error responses.

Last updated

In traditional API engineering, validation architectures suffer from three critical flaws:

  1. Exceptions as Flow Control: Throwing ValidationException on bad inputs, causing heavy CPU and GC overhead under high concurrency;
  2. Boilerplate in Handlers: Handlers cluttered with defensive if (string.IsNullOrEmpty(...)) checks;
  3. 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

Any Rule FailsAll Rules Pass

1. Inbound Request (Command / Query)

2. ValidationPipelineBehavior

3. Execute Associated IRequestRule Set (Pure Functions)

4. Short-Circuit Result.Failure(ValidationError)

5. Execute Target Handler


Step 1: Writing Pure IRequestRule Validations

Implement IRequestRule<TRequest> for contract validation:

CreateCustomerCommandRules.cs: Inbound Validation Rule
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 names
public 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:

ValidationPipelineBehavior.cs: Interception Logic
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 ErrorCode directly to localized UI forms.

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%