A lightweight, allocation‑aware, dependency‑free fluent validation mini-library targeting .NET Framework 4.7.2.
Designed for Dynamics 365 / Dataverse plugin scenarios and general domain model validation where low overhead, clarity, and composability matter.
- Small surface area (easy to read, reason about, and extend)
- No external dependencies
- Allocation conscious (single validator instances, delegate caching pattern)
- Deterministic rule execution with optional cascade short‑circuiting
- Support for nested object graphs and collection item validation
- Explicit error metadata (message, code, severity)
- Simple integration into existing solutions (plugins, services, tests)
| Concept | Description |
|---|---|
Validator<T> |
Base class you inherit to define rules for a model. |
RuleFor(expr) |
Start a rule chain for a single property or value projection. |
RuleForEach(expr) |
Start a rule chain for a collection (then validate the collection itself or its elements). |
| Chaining | Each rule supports chaining constraints; each failing constraint produces a ValidationFailure. |
CascadeMode |
Continue (default) or StopOnFirstFailure. |
| Nested validation | SetValidator(childValidator) or SetValidators(itemValidator) for collections. |
| Conditional activation | .When(predicate) / .Unless(predicate) |
| Null handling | .AllowNull() to gracefully skip further predicates if null. |
| Error metadata | .WithMessage(...), .WithErrorCode(...), .WithSeverity(...) |
| Execution styles | Validate, TryValidate(out result), ValidateOrThrow. |
- Null / emptiness:
NotNullAndNotDefault(),NotEmpty(),NotEmptyString(),NotNullOrWhiteSpace() - Strings:
LengthBetween(min,max),MaxLength(n),Match(regex),EmailAddress() - Guids:
NotEmptyGuid() - Numerics / comparables:
Between(min,max,inclusive),GreaterThan(v),LessOrEqual(v) - Date/Time:
InPast(),InFuture(),NotMinValue() - Collections:
HavingCount(n),HavingGreaterEqualItemOf(n),HavingGreaterItemOf(n),HavingLessItemOf(n),HavingLessEqualItemOf(n) - General predicate:
Must(predicate)(overloads with message, code, severity)
IValidationResult exposes:
IReadOnlyList<ValidationFailure> Errorsbool IsValid(true when no failures)
ValidationFailure includes:
PropertyNameErrorMessageErrorCodeSeverity(e.g. Error / Warning)- (Index-aware names for collection elements, e.g.
Books[2].Title)
ValidationErrorCode is a lightweight struct:
- Predefined:
Required,Length,Format,Range,CollectionCount - Custom codes supported:
new ValidationErrorCode("MY_CODE") - Implicit conversions to/from string
public class Book
{
public string Title { get; set; } = null!;
public string Author { get; set; } = null!;
public DateTime Published { get; set; }
public Guid Id { get; set; }
}
public class BookValidator : Validator<Book>
{
public BookValidator()
{
RuleFor(x => x.Title).NotEmpty().MaxLength(200);
RuleFor(x => x.Author).NotEmpty().MaxLength(100);
RuleFor(x => x.Published).NotEqual(DateTime.MinValue);
RuleFor(x => x.Id).NotEmptyGuid();
}
}
// Usage:
var validator = new BookValidator();
var result = validator.Validate(new Book { /*...*/ });
if (!result.IsValid)
{
foreach (var error in result.Errors)
{
Console.WriteLine($"{error.PropertyName}: {error.ErrorMessage}");
}
}Consumption:
// In a plugin or service:
public void Execute(IServiceProvider serviceProvider)
{
var context = serviceProvider.GetService<IPluginExecutionContext>();
var entity = context.InputParameters["Target"] as Entity;
var validator = new BookValidator();
var validationResult = validator.Validate(entity);
if (!validationResult.IsValid)
{
foreach (var error in validationResult.Errors)
{
// Handle validation errors (e.g., add to plugin context output, throw exception, etc.)
}
}
}Stops evaluating further rules after the first failure (per whole validator).
SetValidator(new ChildValidator())for complex membersRuleForEach(x => x.Items).SetValidators(new ItemValidator())- Failing element rules produce indexed property names.
Time-based rules accept an optional nowProvider and useUtc flag for deterministic testing:
| This Library | Heavier Alternatives |
|---|---|
| Minimal API surface | Rich but larger surface |
| No dependencies | Extra assemblies |
| Explicit/simple | More abstraction |
| Easy to audit | Harder to trim |
Use when you need clarity + performance without adopting a full ecosystem.
- Validator instances are stateless after configuration
- Delegate caching relies on CLR-guaranteed thread-safe static initialization
- All validation methods are safe for concurrent calls (assuming custom predicates are pure)
MSTest project includes:
- Edge cases (nulls, empties, default values)
- Cascade behavior
- Conditional activation (
When/Unless) - Date/time rule determinism
- Collection size variants
- Severity / error code metadata
Run via Test Explorer or vstest.console.exe.
- Predictable execution
- Minimal abstraction layers
- Explicit over implicit (no hidden reflection scanning)
- Composable, chainable fluent API
- Ergonomic failure aggregation
- Optional async hook wrapper
- Source generator for rule summaries
- Extended severity taxonomy
- NuGet packaging
- Clone
- Open solution in Visual Studio 2022
- Build (targets .NET Framework 4.7.2)
- Reference
DevEn.Xrm.Mini.Shared.FluentValidator - Create your validators inheriting
Validator<T>