FluentValidation
Run a FluentValidation validator and get a Result back, with the failures attached to the error.
Waystone.Monads.FluentValidation — a validator that returns a Result.
What it adds
Two extension methods, Validate and ValidateAsync, on any value. Each takes an IValidator<T> you already wrote and hands back a Result<TValue, Error>.
using FluentValidation;
using FluentValidation.Extensions;
record UserInput(int Range, string Search);
class UserInputValidator : AbstractValidator<UserInput>
{
public UserInputValidator()
{
RuleFor(x => x.Range).GreaterThan(0);
RuleFor(x => x.Search).NotEmpty();
}
}
UserInput input = new(1, "bob");
Result<UserInput, Error> result = input.Validate(new UserInputValidator());A value that passes comes back as an Ok holding the value you gave it. A value that fails comes back as an Err holding a ValidationError.
The async form is the same shape, and takes a cancellation token:
When to reach for it
Reach for it where a validation failure is an ordinary outcome your caller handles, and you are already writing FluentValidation validators. Validate gives you a Result that chains with everything else in the library.
Skip it if you throw on invalid input by design, or if you do not use FluentValidation. Nothing else here depends on it.
Install it
You also need FluentValidation itself, which you almost certainly already have — you have to write the validator.
The package supports FluentValidation >= 11.1.0 && < 13.0.0. Bring your own version inside that range.
Where the types live
The package shadows FluentValidation's own namespaces. Its types sit beside IValidator and ValidationFailure rather than under a parallel Waystone tree, so the validator file you already wrote usually needs no new using at all.
ValidationError
FluentValidation
Validate, ValidateAsync
FluentValidation.Extensions
UseValidationErrorCode
FluentValidation.Configs
The package and assembly are still called Waystone.Monads.FluentValidation. Only the namespaces shadow.
Before 7.0.0 these lived under Waystone.Monads.FluentValidation.*. Every type and member name is unchanged — only the using directives move. See Every v7 break.
It errs with Error, so it chains
This is the point of the package. Validate errs with Error, the same type the rest of Waystone.Monads uses, so a validation step drops into a chain without a conversion at the seam.
The async form composes the same way:
ValidateAsync returns a ValueTask, which is what lets a chain ending in it be passed as a step to AndThenAsync. See Async.
ValidationError
ValidationError is a sealed record that derives from Error. It is an error, not something you convert into one.
Code
The configured validation error code. Default validation.failed
Message
Every failure message joined with "; "
Failures
The ValidationFailure list the validator reported, never empty
ToDictionary()
Those messages grouped by property name
You reach the detail by pattern matching, at the one place you need it:
ToDictionary() builds a fresh dictionary each call, so hold the result if you need it twice.
You cannot build an empty one
The constructor is internal, and only the failure branch inside Validate and ValidateAsync reaches it. So a ValidationError always carries at least one failure, and there is no "no failures" case for you to handle.
Two of them compare on code and message
Failures takes no part in equality. Message is rendered from it, so comparing both would only add reference equality over a list — two errors describing the same failures would come out unequal.
A ValidationError never equals a plain Error, even with the same code and message. Records compare their type as well as their values.
Configure the error code
Configure this through MonadOptions, alongside the core settings. There is no separate options class.
The default is validation.failed. The code cannot be null or whitespace — passing either throws an ArgumentException.
UseValidationErrorCode returns MonadValidationOptionsBuilder, not MonadOptionsBuilder, so set the core options first if you need both:
Scopes work, and the code is read once
Validation options honour MonadOptions.BeginScope. One scope covers this package and the core together.
The code is read when the validation runs, not when you later read the error. An error created inside a scope keeps that scope's code after the scope closes. See Configuration for the full scope semantics.
Exceptions still throw
Only validation failures become an Err.
An exception thrown by your validator propagates to the caller.
Validatethrows if the validator declares asynchronous rules. CallValidateAsyncfor those.A cancelled token surfaces as an
OperationCanceledException, not as anErr.
It changed a lot in 7.0.0
If you are coming from 6.x, this package no longer has a ValidationErr type and the extension methods return something different. See Every v7 break.
What it does not do
It does not turn exceptions into an
Err. Only validation failures become one.It does not run asynchronous rules from
Validate. That call throws; useValidateAsync.It does not register your validators. That is FluentValidation's own container wiring, unchanged.
Last updated
Was this helpful?