Skip to content

About

Tiny source-generated command validation for .NET.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

94 Commits

Folders and files

Repository files navigation

TinyValidations

Support the Tiny suite on Buy Me a Coffee to help fund testing and ongoing maintenance.

TinyValidations is a small compile-time validation library for application-layer commands.

It was built for TinyDispatcher-style applications first: define a command, define its validation, register services, and let generated code do the boring work before the handler runs. Native ASP.NET integration is planned, but the core package is intentionally host-agnostic.

Status: 1.0 stable. The core package is intentionally small and host-agnostic. Native host integrations may ship separately.

Preview: 1.1.0-beta.3 fixes validation generation safety and supports compile-time constants and named rule arguments. Generator diagnostics TV0001 through TV0006 are now errors. See the changelog.

Contents

Why TinyValidations?

Most validation libraries do their work at runtime.

TinyValidations keeps the developer API small and moves the repetitive validation runner code to a source generator. You declare intent with IValidation<T> and ValidationRules<T>. The generator emits ordinary C# runners that are registered through Microsoft dependency injection.

The goal is not to be a huge validation framework. The goal is calm command validation:

  • no reflection-based rule discovery at runtime
  • no runtime expression parsing for built-in rules
  • direct generated checks for common rules
  • generator-native requirement rules for simple application-specific checks
  • DI-friendly custom async rules for real business checks
  • a small API that fits application-layer code

Quick start

Install the package:

dotnet add package TinyValidations

Then register TinyValidations:

using Microsoft.Extensions.DependencyInjection;
using TinyValidations;

var services = new ServiceCollection();

services.AddScoped<UserStore>();
services.UseTinyValidations();

var provider = services.BuildServiceProvider();
var validator = provider.GetRequiredService<ITinyValidator>();

var result = await validator.ValidateAsync(new CreateUser("bad-email", "J", 17));

Define a validation for your command:

public sealed class CreateUserValidation : IValidation<CreateUser>
{
    public void Define(ValidationRules<CreateUser> rules)
    {
        rules.Required(x => x.Email);
        rules.Email(x => x.Email);
        rules.TextLengthAtLeast(x => x.Name, 2);
        rules.AtLeast(x => x.Age, 18);
        rules.Use<UniqueEmailRule>();
    }
}

public sealed record CreateUser(string Email, string Name, int Age);

Read validation errors:

foreach (var error in result.Errors)
{
    Console.WriteLine($"{error.Member}: {error.Message}");
}

See the full sample in samples/Basic.

Validation declarations

A validation declaration implements IValidation<T>:

public sealed class CreateUserValidation : IValidation<CreateUser>
{
    public void Define(ValidationRules<CreateUser> rules)
    {
        rules.Required(x => x.Email);
    }
}

The Define method is declaration-only. TinyValidations reads it at compile time and emits generated validation runners.

Supported built-in rules are documented in Rules.

Rule values, regex patterns, and supplied messages must be compile-time constants. Literals, const fields and locals, nameof, and constant expressions are supported. Named arguments can be reordered according to C# rules.

Diagnostics TV0001 through TV0006 are compilation errors. The generator reports issues across declarations and generates the valid declarations independently, but the build fails until the errors are resolved. See Declaration diagnostics.

Required treats null, whitespace strings, and default non-nullable value types such as Guid.Empty as missing. A nullable value type is missing only when it is null.

Inspect generated validations

Tooling and adapters can retrieve the generated validation structure without executing validation rules or scanning assemblies:

var validations = TinyValidationBootstrap.GetValidations();

foreach (var validation in validations)
{
    Console.WriteLine(validation.ValidatedType.FullName);
    Console.WriteLine(validation.DeclarationType.FullName);

    foreach (var rule in validation.Rules)
    {
        Console.WriteLine($"{rule.MemberPath}: {rule.Kind}");
    }

    foreach (var customRuleType in validation.CustomRuleTypes)
    {
        Console.WriteLine(customRuleType.FullName);
    }
}

The structure is collected lazily on the first call and then reused. It contains contributions from TinyValidations-enabled assemblies loaded by the application. Calling GetValidations() does not resolve services, construct validators, or run business rules.

Custom rules

Custom rules implement IAsyncValidationRule<T>. They are resolved from dependency injection, so they can use scoped services.

public sealed class UniqueEmailRule : IAsyncValidationRule<CreateUser>
{
    private readonly UserStore _users;

    public UniqueEmailRule(UserStore users)
    {
        _users = users;
    }

    public ValueTask ValidateAsync(
        CreateUser instance,
        ValidationErrorCollection errors,
        CancellationToken cancellationToken)
    {
        if (_users.Exists(instance.Email))
        {
            errors.Add(nameof(CreateUser.Email), "Email is already registered.");
        }

        return ValueTask.CompletedTask;
    }
}

More detail is in Custom rules.

For static generator-native checks, use Requires:

rules.Requires(
    x => x.OrderNumber,
    OrderNumberRequirements.HasOrderPrefix,
    "Order number must start with ORD-.");

Read more in Extending TinyValidations.

TinyDispatcher

TinyValidations is designed to become the validation companion for TinyDispatcher.

The intended flow is:

  1. A command enters the dispatcher.
  2. TinyValidations validates the command.
  3. The dispatcher stops when validation fails.
  4. The command handler runs only for valid commands.

The current package exposes the validation primitives. The TinyDispatcher integration package is planned separately so the core remains useful outside TinyDispatcher.

Read more in TinyDispatcher integration.

See samples/TinyDispatcherAspNetCore for a minimal ASP.NET sample that wires TinyValidations into TinyDispatcher middleware and maps validation failures to problem details.

See samples/MediatRAspNetCore for the same application shape using a MediatR pipeline behavior.

Tiny suite

TinyValidations belongs to the Tiny suite:

Project Kind Responsibility
TinyDispatcher Library Command and query execution
TinyValidations Library Application input validation
TinyEvents Library Reliable application-event handling through the outbox pattern
TheTinyApplicationLayer Example Runnable ASP.NET Core and Blazor application using the complete suite

The three libraries can be adopted independently. Using one does not require referencing the other two.

Design principles

TinyValidations is intentionally boring in the places that matter.

  • Keep the public API small before adding convenience.
  • Prefer source-generated C# over runtime reflection or expression interpretation.
  • Keep the core package host-agnostic.
  • Treat custom rules as the extension point for business behavior.
  • Keep built-in rule declaration shapes narrow and explicit.
  • Protect behavior with tests instead of testing private implementation details.
  • Make dependency injection registration predictable and idempotent.

Documentation

Current limitations

TinyValidations is intentionally small.

  • Declare rule calls directly inside Define; it is read at compile time and is not executed at runtime.
  • Selectors must be simple member paths. Put calculations and conditional checks in Requires or custom rules.
  • Rule values, patterns, and supplied messages must be compile-time constants. static readonly fields, ordinary variables, property reads, and runtime method calls are not supported as these arguments.
  • Comparison values must also satisfy the rule's IComparable<T> constraint. Runtime thresholds, including constructed dates and times, belong in Requires or custom rules.
  • Detected unsupported declarations produce errors TV0001 through TV0006, even without treating warnings as errors.
  • Generated registration uses a bootstrap contribution mechanism that is idempotent per service collection.
  • Native ASP.NET integration is planned but not implemented yet.
  • Tests cover runtime behavior, generated behavior, diagnostics, and multi-assembly contributions.

See Roadmap for the next planned steps.

About

Tiny source-generated command validation for .NET.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages