Validation
Synapse provides a first-class validation mechanism that runs validators before the handler is invoked. If any validator fails, the handler is never called and the failure is returned to the caller.
Define a validator
Implement IRequestValidator<TRequest> and return Result.Failure(...) when the request is invalid:
using UnambitiousFx.Functional;
using UnambitiousFx.Synapse.Abstractions;
public class CreateTaskCommandValidator : IRequestValidator<CreateTaskCommand>
{
public ValueTask<Result> ValidateAsync(CreateTaskCommand command, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(command.Title))
return ValueTask.FromResult(Result.Failure("Title is required."));
if (command.Title.Length > 200)
return ValueTask.FromResult(Result.Failure("Title must be 200 characters or fewer."));
return ValueTask.FromResult(Result.Success());
}
}
Register the validator and the validation behavior
Validation is implemented as a pipeline behavior, so you register both the validator and the behavior:
services.AddSynapse(cfg =>
{
cfg.AddValidator<CreateTaskCommandValidator, CreateTaskCommand, Guid>();
cfg.RegisterRequestPipelineBehavior<
RequestValidationBehavior<CreateTaskCommand, Guid>,
CreateTaskCommand,
Guid>();
});
AddValidator registers IRequestValidator<CreateTaskCommand> in DI. RequestValidationBehavior resolves all registered validators for the request type at runtime.
Validation flow
Multiple validators
You can register as many validators as you like for the same request type. All validators run and their results are combined via .Combine(). If any fail, all failures are aggregated:
cfg.AddValidator<TitleValidator, CreateTaskCommand, Guid>();
cfg.AddValidator<DuplicateCheckValidator, CreateTaskCommand, Guid>();
// Both run; both failures are reported if both fail
External validation libraries
IRequestValidator<T> is just an interface — you can adapt any validation library. Here is an example adapting FluentValidation:
public class FluentCreateTaskValidator : IRequestValidator<CreateTaskCommand>
{
private readonly IValidator<CreateTaskCommand> _validator;
public FluentCreateTaskValidator(IValidator<CreateTaskCommand> validator)
=> _validator = validator;
public async ValueTask<Result> ValidateAsync(CreateTaskCommand cmd, CancellationToken ct = default)
{
var validation = await _validator.ValidateAsync(cmd, ct);
return validation.IsValid
? Result.Success()
: Result.Failure(string.Join("; ", validation.Errors.Select(e => e.ErrorMessage)));
}
}
See also
- Pipeline Behaviors — validation is a pipeline behavior; understand registration order.
- Error Handling — how validation failures surface as
Resultat the call site.