Vali-Validation
3.0.0
dotnet add package Vali-Validation --version 3.0.0
NuGet\Install-Package Vali-Validation -Version 3.0.0
<PackageReference Include="Vali-Validation" Version="3.0.0" />
<PackageVersion Include="Vali-Validation" Version="3.0.0" />
<PackageReference Include="Vali-Validation" />
paket add Vali-Validation --version 3.0.0
#r "nuget: Vali-Validation, 3.0.0"
#:package Vali-Validation@3.0.0
#addin nuget:?package=Vali-Validation&version=3.0.0
#tool nuget:?package=Vali-Validation&version=3.0.0
Vali-Validation
Vali-Validation is a lightweight, zero-dependency fluent validation library for .NET 7, 8, 9, and 10. It provides a clean, expressive API for defining validation rules on your models, with full support for async validation, conditional rules, nested object validation, collection validation, cascade mode, error codes, custom rules, and seamless dependency injection — all without requiring any external dependencies beyond Microsoft.Extensions.DependencyInjection.Abstractions.
Installation
dotnet add package Vali-Validation
Quick Start
Define a validator by subclassing AbstractValidator<T> and configuring rules in the constructor:
using Vali_Validation.Core.Validators;
public class CreateUserDto
{
public string? Name { get; set; }
public string? Email { get; set; }
public string? Password { get; set; }
public int Age { get; set; }
}
public class CreateUserValidator : AbstractValidator<CreateUserDto>
{
public CreateUserValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.MinimumLength(2)
.MaximumLength(100);
RuleFor(x => x.Email)
.NotEmpty()
.Email();
RuleFor(x => x.Password)
.NotEmpty()
.MinimumLength(8)
.HasUppercase()
.HasLowercase()
.HasDigit()
.HasSpecialChar();
RuleFor(x => x.Age)
.GreaterThanOrEqualTo(18)
.LessThan(120);
}
}
Use the validator:
var validator = new CreateUserValidator();
var result = validator.Validate(dto);
if (!result.IsValid)
{
foreach (var error in result.ToFlatList())
Console.WriteLine(error);
}
// Async
var result = await validator.ValidateAsync(dto, cancellationToken);
// Throw on failure
validator.ValidateAndThrow(dto);
await validator.ValidateAndThrowAsync(dto, cancellationToken);
Available Rules
| Rule | Description |
|---|---|
NotEmpty() |
Value must not be null or whitespace |
NotNull() |
Value must not be null |
Null() |
Value must be null |
Empty() |
Value must be null or empty string |
Must(predicate) |
Custom sync predicate |
MustAsync(predicate) |
Custom async predicate |
MustAsync(predicate, ct) |
Custom async predicate with CancellationToken |
Custom(action) |
Custom validation action with full context |
MinimumLength(n) |
String length >= n |
MaximumLength(n) |
String length ⇐ n |
LengthBetween(min, max) |
String length between min and max |
Matches(pattern) |
Must match regex pattern |
Email() |
Must be a valid email address |
Url() |
Must be a valid HTTP/HTTPS URL |
PhoneNumber() |
Must be a valid E.164 phone number |
IPv4() |
Must be a valid IPv4 address |
CreditCard() |
Must pass Luhn check |
Guid() |
Must be a valid GUID string |
NotEmptyGuid() |
Must be a non-empty GUID |
IsAlpha() |
Only alphabetic characters |
IsAlphanumeric() |
Only alphanumeric characters |
IsNumeric() |
Only numeric characters |
NoWhitespace() |
No whitespace characters |
EqualTo(value) |
Must equal the given value |
NotEqual(value) |
Must not equal the given value |
EqualToProperty(expr) |
Must equal another property |
GreaterThan(n) |
Must be greater than n |
LessThan(n) |
Must be less than n |
GreaterThanOrEqualTo(n) |
Must be >= n |
LessThanOrEqualTo(n) |
Must be ⇐ n |
Between(min, max) |
Inclusive range |
ExclusiveBetween(min, max) |
Exclusive range |
Positive() |
Must be > 0 |
Negative() |
Must be < 0 |
NotZero() |
Must not be 0 |
Odd() |
Must be an odd integer |
Even() |
Must be an even integer |
MultipleOf(factor) |
Must be a multiple of factor |
MaxDecimalPlaces(n) |
At most n decimal places |
In(values) |
Must be in the allowed list |
NotIn(values) |
Must not be in the disallowed list |
StartsWith(prefix) |
String must start with prefix |
EndsWith(suffix) |
String must end with suffix |
MustContain(substring) |
String must contain substring |
NotContains(substring) |
String must not contain substring |
FutureDate() |
DateTime must be in the future |
PastDate() |
DateTime must be in the past |
Today() |
DateTime must be today |
IsEnum<TEnum>() |
Must be a valid enum value |
HasCount(n) |
Collection must have exactly n items |
MinCount(n) |
Collection must have at least n items |
MaxCount(n) |
Collection must have at most n items |
NotEmptyCollection() |
Collection must not be empty |
Unique() |
Collection must have no duplicates |
AllSatisfy(predicate) |
All collection items must satisfy predicate |
AnySatisfy(predicate) |
At least one item must satisfy predicate |
HasUppercase() |
String must contain an uppercase letter |
HasLowercase() |
String must contain a lowercase letter |
HasDigit() |
String must contain a digit |
HasSpecialChar() |
String must contain a special character |
Lowercase() |
String must be all lowercase |
Uppercase() |
String must be all uppercase |
MinWords(n) |
String must have at least n words |
MaxWords(n) |
String must have at most n words |
Modifiers
| Modifier | Description |
|---|---|
WithMessage(msg) |
Override the error message; supports {PropertyName} and {PropertyValue} |
WithErrorCode(code) |
Attach an error code to the rule |
WithSeverity(severity) |
Mark the rule Severity.Warning/Severity.Info instead of the default Severity.Error |
OverridePropertyName(name) |
Use a custom key in error dictionaries |
StopOnFirstFailure() |
Stop evaluating rules for this property after first failure |
When(condition) |
Only run preceding rules when condition is true |
Unless(condition) |
Only run preceding rules when condition is false |
WhenAsync(condition) |
Async version of When |
UnlessAsync(condition) |
Async version of Unless |
Message Templates
Error messages support {PropertyName} and {PropertyValue} placeholders:
RuleFor(x => x.Age)
.GreaterThan(0)
.WithMessage("'{PropertyName}' value '{PropertyValue}' must be positive.");
Severity (Warnings vs Errors)
By default, every rule failure is Severity.Error and makes IsValid false. Mark a rule as
non-blocking with .WithSeverity(Severity.Warning):
public class OrderValidator : AbstractValidator<Order>
{
public OrderValidator()
{
RuleFor(x => x.Email).NotEmpty(); // Severity.Error (default)
RuleFor(x => x.Discount)
.LessThanOrEqualTo(50)
.WithSeverity(Severity.Warning)
.WithMessage("Discount above 50% requires manager approval.");
}
}
A Warning (or Info) failure still appears in result.Failures, but never makes IsValid
false and never appears in the legacy Errors/ErrorCodes/ErrorsFor/HasErrorFor surface —
those only ever reflect Severity.Error failures, so existing code that doesn't know about
severity keeps working unchanged.
result.Failures serializes (via System.Text.Json, default options — no naming policy or
converter required) as a single structured array:
{
"isValid": false,
"failures": [
{ "property": "Email", "message": "The Email field must be a valid email address.", "severity": "Error" },
{ "property": "Discount", "message": "Discount above 50% requires manager approval.", "severity": "Warning" }
]
}
.WithSeverity() scope: like .WithMessage()/.WithErrorCode(), it only affects the last
rule added — including .MustAsync(...)/.DependentRuleAsync(...), which it now targets
correctly. It has no effect on RequiredIf/EqualToProperty/other cross-property rules.
Migrating from v2.x
- If you never need
Warning/Infoseverities, no code changes are required —Errors/ErrorCodes/ErrorsFor(...)/HasErrorFor(...)/FirstError(...)/IsValidall behave exactly as before. - If you directly mutated
result.Errors/result.ErrorCodesor assigned them to a variable typed asDictionary<string, List<string>>, that will now fail to compile — both are nowIReadOnlyDictionary<string, List<string>>. Read-only usage (indexer reads, iteration,ContainsKey) is unaffected. - To read the new severity-aware data:
// Old (v2.x): result.Errors["Email"] // New (v3.x): result.Failures.Where(f => f.PropertyName == "Email").Select(f => f.Message);
Localization
Built-in rule messages are available in English (default) and Spanish out of the box, resolved
automatically from CultureInfo.CurrentUICulture — no configuration needed in an ASP.NET Core
app with standard request localization.
// Automatic: resolves from CultureInfo.CurrentUICulture
var result = validator.Validate(dto);
// Explicit override for one call
var result = validator.Validate(dto, opts => opts.WithLanguage("es"));
// App-wide default (used when CurrentUICulture has no catalog entry)
ValiValidationOptions.Global.DefaultLanguage = "es";
Add your own language by registering a catalog at startup — a partial catalog is fine, any missing key falls back to English:
LanguageManager.RegisterLanguage("pt", new Dictionary<MessageKey, string>
{
[MessageKey.NotEmpty] = "O campo {PropertyName} não pode estar vazio.",
// ... remaining keys fall back to English until added
});
Known limitation: nested validators attached via SetValidator do not inherit an outer call's
explicit .WithLanguage(...) override — each resolves independently from CultureInfo.CurrentUICulture,
which is already consistent across nesting without any forwarding needed for the common case.
Global Configuration
ValiValidationOptions.Global configures app-wide defaults — set once at startup, before any
concurrent validation runs:
ValiValidationOptions.Global.DefaultCascadeMode = CascadeMode.StopOnFirstFailure;
ValiValidationOptions.Global.DefaultLanguage = "es";
ValiValidationOptions.Global.DisplayNameResolver = name => name; // e.g. PascalCase → "Display Name"
ValiValidationOptions.Global.PropertyNameResolver = name => name; // e.g. PascalCase → snake_case
DefaultCascadeMode: applies to validators that don't overrideGlobalCascadeModethemselves.DisplayNameResolver: transforms the text shown in{PropertyName}inside messages — does NOT change the key used inValidationResult.Errors.PropertyNameResolver: transforms the actual key used inValidationResult.Errors/Failures.
Reusable Custom Rules
For a custom rule you want to unit-test in isolation or share across validators, implement
IPropertyValidator<TProperty> (or extend the PropertyValidator<TProperty> convenience base)
instead of an inline .Must(...) predicate:
public class EvenNumberValidator : IPropertyValidator<int>
{
public bool IsValid(int value) => value % 2 == 0;
public IReadOnlyDictionary<string, string> Messages { get; } = new Dictionary<string, string>
{
["en"] = "The {PropertyName} field must be an even number.",
["es"] = "El campo {PropertyName} debe ser un número par."
};
}
RuleFor(x => x.Age).SetPropertyValidator(new EvenNumberValidator());
.SetPropertyValidator(...) integrates like every other rule — .WithSeverity(...),
.WithErrorCode(...), .When(...), .Unless(...), and .WithMessage(...) all work on it.
CascadeMode
Stop validation after the first property failure across all properties:
public class MyValidator : AbstractValidator<MyDto>
{
protected override CascadeMode GlobalCascadeMode => CascadeMode.StopOnFirstFailure;
public MyValidator()
{
RuleFor(x => x.Name).NotEmpty();
RuleFor(x => x.Email).Email(); // Skipped if Name already fails
}
}
RuleSets
Tag rules with .InRuleSet(...) and restrict a single Validate/ValidateAsync call to only the
rule sets you name via IncludeRuleSets(...). Rules with no InRuleSet call carry the implicit
tag "default" and are skipped unless "default" is itself included:
public class OrderValidator : AbstractValidator<Order>
{
public OrderValidator()
{
RuleFor(x => x.CustomerName).NotEmpty(); // implicit "default" rule set
RuleFor(x => x.PaymentMethod)
.NotEmpty()
.InRuleSet("checkout");
}
}
// Runs only rules tagged "checkout"
var result = validator.Validate(order, o => o.IncludeRuleSets("checkout"));
// Runs every rule regardless of tag (same as always)
var result = validator.Validate(order);
InRuleSet(...) is additive — call it multiple times (or pass multiple names) to tag a rule with
more than one rule set. ValidateAsync supports the same Action<ValidationOptions> overload.
Nested Validators
RuleFor(x => x.Address).SetValidator(new AddressValidator());
Resolve the nested validator from DI instead of constructing it yourself with
.InjectValidator<T, TProperty>(IServiceProvider) — useful when nested validators are registered
via AddValidationsFromAssembly and the root validator receives an IServiceProvider:
public class OrderValidator : AbstractValidator<Order>
{
public OrderValidator(IServiceProvider serviceProvider)
{
RuleFor(x => x.Address).InjectValidator<Order, Address>(serviceProvider);
}
}
Throws InvalidOperationException if no IValidator<TProperty> is registered, or if the registered
implementation isn't an AbstractValidator<TProperty>.
Collection Validation
RuleForEach(x => x.Tags).NotEmpty().MinimumLength(2);
DI Registration
// Register all validators from an assembly
services.AddValidationsFromAssembly(typeof(CreateUserValidator).Assembly);
// Or register individually
services.AddScoped<IValidator<CreateUserDto>, CreateUserValidator>();
Links
Donations
If Vali-Validation is useful to you, consider supporting its development:
- Latin America — MercadoPago
- International — PayPal
License
Contributions
Issues and pull requests are welcome on GitHub.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
-
net7.0
-
net8.0
-
net9.0
NuGet packages (4)
Showing the top 4 NuGet packages that depend on Vali-Validation:
| Package | Downloads |
|---|---|
|
Vali-Validation.MediatR
MediatR integration for Vali-Validation. Adds a ValidationBehavior<TRequest, TResponse> pipeline behavior that automatically validates IRequest objects using Vali-Validation before they reach their handler. Throws ValidationException on failure. Use Vali-Mediator.Validation instead if you use Vali-Mediator — it provides richer integration with Result<T> support (no exceptions needed). |
|
|
Vali-Validation.ValiMediator
Vali-Mediator integration for Vali-Validation. Targets net7.0/net8.0/net9.0/net10.0. Adds a ValidationBehavior<TRequest, TResponse> that validates IRequest objects before they reach their handler. When TResponse is Result<T>, validation failures are returned as Result.Fail(errors, ErrorType.Validation) — no exceptions needed. For other response types, throws ValidationException. Package name: Vali-Validation.ValiMediator. Requires Vali-Mediator. |
|
|
Vali-Validation.AspNetCore
ASP.NET Core integration for Vali-Validation. Targets net7.0/net8.0/net9.0/net10.0. Provides: - ValiValidationMiddleware: catches ValidationException and returns HTTP 400 problem+json. - ValiValidationFilter<T>: Minimal API endpoint filter for automatic request validation. - ValiValidateAttribute: MVC action filter for automatic controller action validation. - UseValiValidationExceptionHandler() and WithValiValidation<T>() extension methods. |
|
|
Vali-Validation.TestHelpers
Test-assertion helpers for Vali-Validation. Targets net7.0/net8.0/net9.0/net10.0. Adds fluent ShouldHaveValidationErrorFor / ShouldNotHaveValidationErrorFor extensions on ValidationResult. Framework-agnostic — throws ValidationAssertionException on failure, which any test runner (xUnit, NUnit, MSTest) fails the test on as an unhandled exception, without requiring a dependency on that runner from this package. |
GitHub repositories
This package is not used by any popular GitHub repositories.
v3.0.0 — BREAKING CHANGE: Severity-aware validation results.
ValidationResult now tracks failures with a severity (Error/Warning/Info) instead of
a flat pass/fail per property. The new ValidationResult.Failures (List<ValidationFailure>)
is the source of truth; Errors/ErrorCodes/ErrorsFor/HasErrorFor/FirstError/ToFlatList/
ErrorCount/PropertyNames all still work exactly as before, but now only reflect
Severity.Error failures — a rule marked .WithSeverity(Severity.Warning) never appears
in them and never makes IsValid false.
Migration:
- No code changes required if you only ever used the old Errors/ErrorCodes/ErrorsFor/
HasErrorFor/IsValid surface and never need Warning/Info severities — behavior is
identical for validators that never call .WithSeverity().
- If you directly mutated result.Errors/result.ErrorCodes or assigned them to a
variable typed as Dictionary<string, List<string>>, that will now fail to
compile — both are now IReadOnlyDictionary<string, List<string>>. Read-only
usage (indexer reads, iteration, ContainsKey) is unaffected.
- To read the new severity-aware data: use result.Failures directly, or
result.Failures.Where(f => f.PropertyName == "Email").Select(f => f.Message)
instead of result.Errors["Email"].
- New fluent modifier: .WithSeverity(Severity.Warning) on any property rule (same
placement rules as WithMessage/WithErrorCode — only affects the last rule in the
chain, and only rules added via the standard property-rule chain, not RequiredIf/
EqualToProperty and other cross-property rules).
- ValidationResult now serializes (System.Text.Json, default options) as a single
structured array: { "isValid": bool, "failures": [{ "property", "message",
"severity", "errorCode"? }] } — see README for the full example.
New features:
- Severity enum (Error/Warning/Info) in Vali_Validation.Core.Results.
- ValidationResult.Failures — single structured list, replaces the implicit
two-collection (Errors/ErrorCodes) model as the source of truth.
- ValidationResult.AddFailure(property, message, severity, errorCode) — general-purpose
failure-adding method; AddError is now a Severity.Error-only shorthand for it.
- IRuleBuilder<T,TProperty>.WithSeverity(Severity) — mark a rule as non-blocking.
- CustomValidationContext<T>.AddFailure(..., Severity, ...) overloads — Custom()
rules can now raise warnings too.
RuleSets (new, non-breaking):
- IRuleBuilder<T,TProperty>.InRuleSet(params string[]) tags a rule; additive across
calls. Rules with no InRuleSet call carry the implicit tag "default".
- Validate(instance, o => o.IncludeRuleSets("name")) / ValidateAsync(...) restrict a
single call to rules tagged with any of the named rule sets. Plain Validate(instance)/
ValidateAsync(instance) with no options still runs every rule regardless of tag.
Block-level When/Unless (new, non-breaking):
- Protected When(Func<T,bool>, Action)/Unless(Func<T,bool>, Action) on
AbstractValidator<T> wrap multiple RuleFor/RuleForEach calls in the constructor
under a single shared condition, instead of repeating .When(...)/.Unless(...) on each
rule. Nested blocks compose with AND.
InjectValidator (new, non-breaking):
- IRuleBuilder<T,TProperty>.InjectValidator<T,TProperty>(IServiceProvider)
resolves a nested IValidator<TProperty> from DI instead of requiring an
already-constructed instance, then delegates to SetValidator. Throws
InvalidOperationException if no IValidator<TProperty> is registered, or the
registered implementation isn't an AbstractValidator<TProperty>.
Localization (new, non-breaking):
- Built-in rule messages ship in English (default) and Spanish, resolved automatically
from CultureInfo.CurrentUICulture. Override per call with
Validate(instance, o => o.WithLanguage("es")), or set an app-wide default via
ValiValidationOptions.Global.DefaultLanguage.
- LanguageManager.RegisterLanguage("code", dictionary) adds a new language at startup;
partial catalogs are fine — any missing key falls back to English.
Global Configuration (new, non-breaking):
- ValiValidationOptions.Global exposes DefaultCascadeMode, DefaultLanguage,
DisplayNameResolver, and PropertyNameResolver as app-wide defaults set once at
startup, before any concurrent validation runs.
PropertyValidator (new, non-breaking):
- IPropertyValidator<TProperty> (and the PropertyValidator<TProperty>
convenience base) let you implement a custom rule as a reusable, unit-testable class
instead of an inline .Must(...) predicate. Attach with
RuleFor(x => x.Prop).SetPropertyValidator(new MyValidator()) — integrates with
WithSeverity/WithErrorCode/When/Unless/WithMessage like any other rule.
.NET 10 support:
- Multi-targeting extended to net7.0;net8.0;net9.0;net10.0.
Fixes:
- .WithMessage()/.WithErrorCode()/.WithSeverity()/.When()/.Unless() chained after
.MustAsync(...)/.DependentRuleAsync(...) now correctly apply to that async rule
instead of silently being dropped or reassigned to an earlier synchronous rule.