FluentValidation
dotnet add package Nullean.Entities.FluentValidation
For a team that already validates with FluentValidation. The package works in both directions: your constraints run inside FluentValidation, and your
FluentValidation validators run as constraints. Every snippet on this page is taken from
tests/Nullean.Entities.FluentValidation.Tests.
A constraint set becomes an IValidator<T>. Register it where the pipeline (ASP.NET Core, a mediator pipeline behaviour, your own) resolves validators, and it is called through ValidateAsync like any
other:
services.AddSingleton<IValidator<Customer>>(CustomerRules.All.ToValidator());
Add the constraints to a validator you already have, or check one property or each item of a collection against its own set:
public sealed class SignUpValidator : AbstractValidator<Customer>
{
public SignUpValidator()
{
RuleFor(c => c.Name).NotEmpty();
Include(CustomerRules.All.ToValidator());
}
}
validator.RuleFor(c => c.Address).SatisfiesConstraints(AddressRules.All); // a failure is keyed "Address.City"
validator.RuleForEach(c => c.Lines).SatisfiesConstraints(LineRules.All); // "Lines[1].Quantity"
ConstraintValidator<T> is the validator class itself: derive from it to add FluentValidation rules next to the constraints (: base(CustomerRules.All)).
Each violation becomes one ValidationFailure:
ValidationFailure |
From the Violation |
|---|---|
PropertyName |
Member under the rule's property path (Address.City, Lines[1].Quantity). Without a member: the path, or at the root the rule's id |
ErrorMessage |
Message, as written (FluentValidation's {placeholders} are not expanded) |
ErrorCode |
ConstraintId, the rule's name |
Severity |
Error, Warning and Hint become Error, Warning and Info |
AttemptedValue |
Evidence |
CustomState |
The Violation itself |
Only the point-in-time rules run, as in Validate: a validator sees one value, not a change, so a rule about a transition is not checked here. A null property is skipped, as FluentValidation
skips a child validator; add NotNull() when null is wrong.
Tested in ConstraintValidatorTests.cs.
An existing validator becomes a named constraint, and from there everything that takes a constraint set takes it, including the property tests:
var rules = ConstraintSet.From(("customer-validator", Constraints<Customer>.FromValidator(new CustomerValidator())));
// or: new CustomerValidator().ToConstraintSet() (named after the validator type)
rules.Validate(customer with { Age = 7 });
// customer-validator Member "Age" 'Age' must be between 18 and 120. You entered 7.
Each failure keeps its PropertyName as Member (Address.City, Lines[1].Quantity from RuleForEach), its AttemptedValue as Evidence, and Info maps back to Hint. Only errors
reject a change in Apply. Tested in ValidatorConstraintsTests.cs.
This is the reason to do it. With Nullean.Entities.CsCheck in the test project, generated values are thrown at the validator:
var rules = new CustomerValidator().ToConstraintSet();
rules.Probe(samples: 500);
// CustomerValidator held 62 violated 438 (one run; both must be above zero)
GenAuto.Of<Customer>().ShouldSatisfy(rules);
// Set seed: "..." to reproduce (3 shrinks, ...)
// Customer { Name = "Anna Douglas", Email = "-", Age = 1, ... } violates: CustomerValidator: 'Email' is not a valid email address.; CustomerValidator: 'Age' must be between 18 and 120. You entered 1.
Probe tells you the validator both holds and fails on generated data (a validator that never fails is accepting everything); ShouldSatisfy against the output of your own factory or
mapper finds a value it builds that the validator rejects, shrunk and with a seed to replay. Tested in ProbeTests.cs.
Severityis in both namespaces. A file withusing FluentValidation;andusing Nullean.Entities;must say which one it means:using FvSeverity = FluentValidation.Severity;.- The validator is run synchronously. Constraints are synchronous and pure, so
FromValidatorcallsValidate; a validator withMustAsyncorCustomAsyncrules throws FluentValidation'sAsyncValidatorInvokedSynchronouslyExceptionwhen the constraint is checked. Keep database lookups in the application service. - One validator is one constraint.
Probecounts the whole validator, not eachRuleFor; the violation'sMembersays which property failed. - FluentValidation's
ErrorCodeis not kept on the way in: a violation'sConstraintIdis the constraint's name. - Not trim or NativeAOT clean: FluentValidation compiles expressions and is not annotated for trimming.
- Built and tested against FluentValidation 12.2.0 (Apache-2.0).