Vali-Validation 3.0.0

dotnet add package Vali-Validation --version 3.0.0
                    
NuGet\Install-Package Vali-Validation -Version 3.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Vali-Validation" Version="3.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Vali-Validation" Version="3.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Vali-Validation" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Vali-Validation --version 3.0.0
                    
#r "nuget: Vali-Validation, 3.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Vali-Validation@3.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Vali-Validation&version=3.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Vali-Validation&version=3.0.0
                    
Install as a Cake Tool

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/Info severities, no code changes are required — Errors/ErrorCodes/ErrorsFor(...)/HasErrorFor(...)/FirstError(...)/IsValid all behave exactly as before.
  • 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:
    // 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 override GlobalCascadeMode themselves.
  • DisplayNameResolver: transforms the text shown in {PropertyName} inside messages — does NOT change the key used in ValidationResult.Errors.
  • PropertyNameResolver: transforms the actual key used in ValidationResult.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>();

Donations

If Vali-Validation is useful to you, consider supporting its development:


License

Apache License 2.0

Contributions

Issues and pull requests are welcome on GitHub.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.

Version Downloads Last Updated
3.0.0 27 9/29/2026
2.0.2 223 4/14/2026
2.0.1 264 3/18/2026
2.0.0 214 3/16/2026
1.0.0 306 4/16/2025

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.