CSharpEssentials.Validation 3.0.6

There is a newer version of this package available.
See the version list below for details.
dotnet add package CSharpEssentials.Validation --version 3.0.6
                    
NuGet\Install-Package CSharpEssentials.Validation -Version 3.0.6
                    
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="CSharpEssentials.Validation" Version="3.0.6" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CSharpEssentials.Validation" Version="3.0.6" />
                    
Directory.Packages.props
<PackageReference Include="CSharpEssentials.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 CSharpEssentials.Validation --version 3.0.6
                    
#r "nuget: CSharpEssentials.Validation, 3.0.6"
                    
#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 CSharpEssentials.Validation@3.0.6
                    
#: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=CSharpEssentials.Validation&version=3.0.6
                    
Install as a Cake Addin
#tool nuget:?package=CSharpEssentials.Validation&version=3.0.6
                    
Install as a Cake Tool

CSharpEssentials.Validation

High-performance, model-first validation library for C#. Zero-reflection, eager evaluation — no expression trees, no deferred builds. Returns Result<T> natively.

Installation

dotnet add package CSharpEssentials.Validation

Quick Start

Define a validator by extending Validator<T>:

using CSharpEssentials.Validation;

public class CreateUserCommandValidator : Validator<CreateUserCommand>
{
    protected override ValueTask Configure(CreateUserCommand model, RuleContext<CreateUserCommand> rules, CancellationToken ct = default)
    {
        rules.For(() => model.Email).NotEmpty().EmailAddress();
        rules.For(() => model.Name).NotEmpty().MaxLength(100);
        rules.For(() => model.Age).GreaterThan(0);
        return ValueTask.CompletedTask;
    }
}

Invoke directly or inject via DI:

var validator = new CreateUserCommandValidator();
Result<CreateUserCommand> result = await validator.ValidateAsync(command);
// Sync call site: validator.ValidateAsync(command).GetAwaiter().GetResult()

if (result.IsFailure)
{
    foreach (var error in result.Errors)
        Console.WriteLine($"{error.Code}: {error.Description}");
        // e.g. "Email.NotEmpty: 'Email' must not be empty."
}

Sync call site: validator.ValidateAsync(command).GetAwaiter().GetResult() — safe when the validator has no actual async operations (returns a pre-completed ValueTask).

Built-in Validators

String

Validator Null Description
NotEmpty() FAIL Value must not be null, empty, or whitespace
NotNull() FAIL Value must not be null
MaxLength(n) skip Length ≤ n
MinLength(n) skip Length ≥ n
Length(min, max) skip Length in [min, max]
EmailAddress() skip Valid e-mail format
Matches(pattern) skip Matches regex pattern
Contains(sub) skip Contains substring
StartsWith(prefix) skip Starts with prefix
EndsWith(suffix) skip Ends with suffix

Comparable (int, DateTime, decimal, …)

Validator Null Description
GreaterThan(n) skip Value > n
GreaterThanOrEqualTo(n) skip Value ≥ n
LessThan(n) skip Value < n
LessThanOrEqualTo(n) skip Value ≤ n
InclusiveBetween(min, max) skip min ≤ value ≤ max
ExclusiveBetween(min, max) skip min < value < max
Equal(expected) FAIL Equals expected
NotEqual(forbidden) skip Not equal to forbidden

Collections (IEnumerable<T>)

Validator Null Description
NotEmpty() FAIL Collection must not be null or empty
NotNull() FAIL Collection must not be null
MinCount(n) skip At least n elements
MaxCount(n) skip At most n elements
CountBetween(min, max) skip Element count in [min, max]

Nullable Structs (int?, DateTime?, …)

All comparable validators are available for nullable value types — null is silently skipped.

CascadeMode

By default (Stop), the first failure on a chain stops subsequent rules. Use Continue to accumulate all errors for a property:

rules.For(() => model.Password)
     .Cascade(CascadeMode.Continue)
     .MinLength(8)
     .Matches(@"[A-Z]", message: "Must contain an uppercase letter.");

Custom Predicates

// Sync
rules.For(() => model.Name)
     .Must(name => name != "admin", "Name.Reserved", "The name 'admin' is reserved.");

// Async
await rules.For(() => model.Email)
           .MustAsync(async (email, ct) => await _db.IsUniqueAsync(email, ct),
                      "Email.NotUnique", "Email is already taken.");

Nested Validation

await rules.For(() => model.Address!).SetValidatorAsync(new AddressValidator(), ct);
// Error codes are prefixed: "Address.City.NotEmpty", "Address.ZipCode.Matches"
// Note: must be inside an async Configure to use await.

Collection Validation

rules.ForEach(() => model.Tags, (tag, tagRules) =>
{
    tagRules.For(() => tag.Name).NotEmpty().MaxLength(50);
});
// Error codes: "Tags[0].Name.NotEmpty", "Tags[1].Name.MaxLength"

Native C# Control Flow

Configure receives the fully-resolved model instance, so any C# control flow works inside it — no DSL, no special API.

Conditional Rules

public class OrderValidator : Validator<Order>
{
    protected override ValueTask Configure(Order model, RuleContext<Order> rules, CancellationToken ct = default)
    {
        // Always-on rules
        rules.For(() => model.CustomerId).NotEmpty();

        // if / else branching
        if (model.OrderType == OrderType.Business)
        {
            rules.For(() => model.CompanyName).NotEmpty().MaxLength(200);
            rules.For(() => model.TaxId).NotEmpty().Matches(@"^\d{10}$");
        }
        else
        {
            rules.For(() => model.FirstName).NotEmpty().MaxLength(100);
            rules.For(() => model.LastName).NotEmpty().MaxLength(100);
        }

        // switch expression
        switch (model.Country)
        {
            case "TR":
                rules.For(() => model.NationalId).NotEmpty().MinLength(11);
                break;
            case "US":
                rules.For(() => model.SSN).NotEmpty().Matches(@"^\d{3}-\d{2}-\d{4}$");
                break;
        }

        // Early return — remaining rules skipped when Terms not accepted
        if (!model.AcceptsTerms) return ValueTask.CompletedTask;
        rules.For(() => model.Signature).NotEmpty();
        return ValueTask.CompletedTask;
    }
}

Null-Guard Before Nested Validation

if (model.ShippingAddress is not null)
    await rules.For(() => model.ShippingAddress!).SetValidatorAsync(new AddressValidator(), ct);

CSharpEssentials.Core Integration

CSharpEssentials.Core is transitively available (via CSharpEssentials.Errors) and ships a rich set of conditional helpers that compose naturally with validators.

IfNotNull — run rules only when a property has a value:

// native C#:
if (model.Coupon is not null)
    rules.For(() => model.Coupon.Code).NotEmpty();

// with Core helper — same behaviour, one expression:
model.Coupon.IfNotNull(coupon =>
    rules.For(() => coupon.Code).NotEmpty());

WhereIf — filter a collection before iterating:

// Only validate active items
rules.ForEach(
    () => model.Items.WhereIf(model.ValidateActiveOnly, i => i.IsActive),
    (item, itemRules) => itemRules.For(() => item.Sku).NotEmpty());

WithoutNulls — skip null elements in a collection:

rules.ForEach(
    () => model.Tags.WithoutNulls(),
    (tag, tagRules) => tagRules.For(() => tag).NotEmpty().MaxLength(50));

IsEmpty / IsNotEmpty — readable null/empty guards:

if (model.PromoCode.IsNotEmpty())
    rules.For(() => model.PromoCode).Matches(@"^PROMO-\d{4}$");

IfTrue / IfFalse — boolean action helpers:

model.IsInternational.IfTrue(() =>
    rules.For(() => model.PassportNumber).NotEmpty());

Install CSharpEssentials.Core separately if you are not using the meta-package.

Multiple Validators for One Model

Use Include to compose base validators, or inject several IValidator<T> implementations for the same model type.

Composition with Include

public class BaseOrderValidator : Validator<Order>
{
    protected override ValueTask Configure(Order model, RuleContext<Order> rules, CancellationToken ct = default)
    {
        rules.For(() => model.CustomerId).NotEmpty();
        rules.For(() => model.Items).NotEmpty();
        return ValueTask.CompletedTask;
    }
}

public class PaidOrderValidator : Validator<Order>
{
    protected override async ValueTask Configure(Order model, RuleContext<Order> rules, CancellationToken ct = default)
    {
        await Include(new BaseOrderValidator(), model, rules, ct);
        // additional rules for paid orders
        rules.For(() => model.PaymentReference).NotEmpty();
    }
}

Include runs the included validator in-line and merges all errors into the same Result<T>.

Multiple DI Validators

When several IValidator<Order> implementations are registered, the ValidationBehavior in CSharpEssentials.Mediator aggregates results from all of them, deduplicating identical errors automatically.

services.AddValidator<Order, BaseOrderValidator>();
services.AddValidator<Order, PaidOrderValidator>();
// Both validators run; errors are merged and deduplicated

Validator Ordering

Override Order on Validator<T> to control execution sequence when multiple validators are registered for the same model type.

public class FormatValidator : Validator<CreateUserCommand>
{
    public override int Order => 0;  // default — runs first

    protected override ValueTask Configure(CreateUserCommand model, RuleContext<CreateUserCommand> rules, CancellationToken ct = default)
    {
        rules.For(() => model.Email).NotEmpty().EmailAddress();
        return ValueTask.CompletedTask;
    }
}

public class BusinessRulesValidator : Validator<CreateUserCommand>
{
    public override int Order => 1;  // runs after Order=0 group completes

    protected override ValueTask Configure(CreateUserCommand model, RuleContext<CreateUserCommand> rules, CancellationToken ct = default)
    {
        rules.For(() => model.Email).Must(email => !_blocklist.Contains(email), "Email.Blocked", "Email is blocked.");
        return ValueTask.CompletedTask;
    }
}

Execution rules:

  • Validators sharing the same Order run concurrently within their group.
  • Groups with lower Order run sequentially before groups with higher Order.
  • All groups execute regardless of earlier failures — errors from all groups are accumulated and deduplicated.
  • Default Order is 0 — all validators without an override run concurrently in one group.

DI Registration

// Register a single validator
services.AddValidator<CreateUserCommand, CreateUserCommandValidator>();

// Scan and register all validators from an assembly
services.AddValidatorsFromAssembly(typeof(CreateUserCommandValidator).Assembly);

// Multiple assemblies
services.AddValidatorsFromAssemblies([
    typeof(CreateUserCommandValidator).Assembly,
    typeof(UpdateProductValidator).Assembly
]);

Default lifetime: Scoped. Pass a ServiceLifetime parameter to override.

Validators with no scoped dependencies may be registered as Singleton. Validators that inject scoped services (e.g. DbContext, ICurrentUser) must be Scoped.

Mediator Pipeline Integration

Use with CSharpEssentials.Mediator for automatic validation before handlers run:

services.AddMediatorValidationBehavior();
// or
services.AddMediatorBehaviors(); // registers all behaviors

ValidationBehavior is registered as Scoped. It runs all registered IValidator<TRequest> implementations, groups them by Order, and returns Result.Failure if any errors are found — the handler is never invoked.

Exception isolation: If a validator throws a non-OperationCanceledException exception, ValidationBehavior catches it and converts it to a Result.Failure error (code "Validator.Exception"). OperationCanceledException always propagates — cancellation is never swallowed.

See CSharpEssentials.Mediator for full pipeline documentation.

FluentValidation Comparison

CSharpEssentials.Validation FluentValidation
Rule definition ValueTask Configure(model, rules, ct) — model is the live instance; sync validators return ValueTask.CompletedTask Constructor — RuleFor(x => x.Name) stores expressions
Conditional rules Native if/else/switch — no DSL .When(x => ..., () => { ... }) block or per-rule .When() chain
Conditional style Standard C# — zero extra API to learn Dedicated When, Unless, WhenAsync, UnlessAsync methods
Property name Extracted from CallerArgumentExpression at compile time Extracted from Expression<Func<T,TProperty>> at compile time
Reflection Zero — no expression trees, no reflection at runtime Expression trees at rule-definition time; compiled to delegates
AOT / NativeAOT Fully compatible Expression tree compilation can cause issues
Async rules MustAsync, ForEachAsync, SetValidatorAsync; ValidateAsync returns ValueTask<Result<T>> MustAsync, WhenAsync; ValidateAsync returns Task<ValidationResult>
Result type Returns Result<T> natively (railway-oriented) Returns ValidationResult — separate mapping needed
Error type Structured Error (code, description, type, metadata) ValidationFailure (PropertyName, ErrorMessage, …)
DI lifetime Scoped by default; Singleton-safe Typically Singleton (stateless by design)

Why We Chose This Approach

FluentValidation's constructor-based design forces rule registration without a model instance. This means:

  • You cannot write if (model.X) { ... } in the constructor — the model doesn't exist yet.
  • The When() DSL exists solely to compensate for this limitation.
  • Every developer must learn an extra API layer that simply reimplements what C# if already provides.

By passing model directly into Configure, CSharpEssentials.Validation eliminates the need for When/Unless entirely. Branching logic is written in idiomatic C#, readable by anyone, debuggable with standard tooling, and refactorable without ceremony.

The trade-off: a new RuleContext<T> is created per ValidateAsync call. This is intentional: the context is a small, short-lived object that keeps the design thread-safe and allocation-predictable. Validator<T> instances themselves are stateless and can be registered as Singleton when they have no scoped dependencies. Validators that inject scoped services (e.g. DbContext) should be registered as Scoped.

FluentValidation Advantages

  • Mature ecosystem, wide community adoption
  • Rich .WithMessage, .WithName, .WithErrorCode fluent API
  • Built-in rule sets (RuleSet) for named validation scenarios
  • .Cascade(CascadeMode.StopOnFirstFailure) configurable globally

FluentValidation Disadvantages

  • Cannot use plain if for conditional rules without DSL
  • ValidationResult is not railway-oriented — requires manual Result<T> mapping
  • Expression trees limit Native AOT compatibility
  • When() blocks create implicit coupling between rule registration and condition evaluation

Dependencies

  • CSharpEssentials.Results
  • Microsoft.Extensions.DependencyInjection.Abstractions
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  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 was computed.  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.  net11.0 is compatible. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on CSharpEssentials.Validation:

Package Downloads
CSharpEssentials.Mediator

High-performance CQRS and pipeline behaviors built on the Mediator source-generator library. Provides Result-integrated commands, queries, validation, logging, caching, and transaction behaviors with full Native AOT support.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.0.8 65 5/20/2026
3.0.7 75 5/20/2026
3.0.6 76 5/20/2026