Skip to content

ZenPluS/DevEn.Xrm.Mini.FluentValidator

Repository files navigation

DevEn.Xrm.Mini.FluentValidator

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.

Key Goals

  • 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)

Core Concepts

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.

Built-in Rule Extensions (Illustrative)

  • 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)

Validation Result Model

IValidationResult exposes:

  • IReadOnlyList<ValidationFailure> Errors
  • bool IsValid (true when no failures)

ValidationFailure includes:

  • PropertyName
  • ErrorMessage
  • ErrorCode
  • Severity (e.g. Error / Warning)
  • (Index-aware names for collection elements, e.g. Books[2].Title)

Error Codes

ValidationErrorCode is a lightweight struct:

  • Predefined: Required, Length, Format, Range, CollectionCount
  • Custom codes supported: new ValidationErrorCode("MY_CODE")
  • Implicit conversions to/from string

Basic Example

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.)
        }
    }
}

Cascade Mode

Stops evaluating further rules after the first failure (per whole validator).

Conditional Rules

Nested & Collection Validation

  • SetValidator(new ChildValidator()) for complex members
  • RuleForEach(x => x.Items).SetValidators(new ItemValidator())
  • Failing element rules produce indexed property names.

Date / Time Controls

Time-based rules accept an optional nowProvider and useUtc flag for deterministic testing:

Comparing to Larger Frameworks

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.

Thread-Safety

  • 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)

Testing

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.

Design Principles

  • Predictable execution
  • Minimal abstraction layers
  • Explicit over implicit (no hidden reflection scanning)
  • Composable, chainable fluent API
  • Ergonomic failure aggregation

Roadmap (Potential)

  • Optional async hook wrapper
  • Source generator for rule summaries
  • Extended severity taxonomy
  • NuGet packaging

Build & Integration

  1. Clone
  2. Open solution in Visual Studio 2022
  3. Build (targets .NET Framework 4.7.2)
  4. Reference DevEn.Xrm.Mini.Shared.FluentValidator
  5. Create your validators inheriting Validator<T>

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages