ValidationModules.Runtime
1.0.0-rc1006
dotnet add package ValidationModules.Runtime --version 1.0.0-rc1006
NuGet\Install-Package ValidationModules.Runtime -Version 1.0.0-rc1006
<PackageReference Include="ValidationModules.Runtime" Version="1.0.0-rc1006" />
<PackageVersion Include="ValidationModules.Runtime" Version="1.0.0-rc1006" />
<PackageReference Include="ValidationModules.Runtime" />
paket add ValidationModules.Runtime --version 1.0.0-rc1006
#r "nuget: ValidationModules.Runtime, 1.0.0-rc1006"
#:package ValidationModules.Runtime@1.0.0-rc1006
#addin nuget:?package=ValidationModules.Runtime&version=1.0.0-rc1006&prerelease
#tool nuget:?package=ValidationModules.Runtime&version=1.0.0-rc1006&prerelease
ValidationModules
Compile-time validation for .NET. Rules are declared as attributes and flattened into straight-line C# by a source generator at build time. No reflection, no expression trees, no regex compiled at runtime — Native AOT is a hard requirement rather than a supported configuration.
public record Pet {
[Required]
[StringLength(min: 1, max: 100)]
public string Name { get; init; }
[Pattern("^[a-zA-Z0-9-]*$")]
public string? Sku { get; init; }
[ValidateNested]
public Address? Home { get; init; }
[ItemCount(min: 1, max: 10), ValidateNested]
public IReadOnlyList<Toy> Toys { get; init; } = [];
}
var result = validator.Validate(pet);
foreach (var error in result.Errors) {
Console.WriteLine($"{error.Field}: {error.Code}");
}
// name required
// home.postalCode required
// toys[3].name required
Why
FluentValidation compiles expression trees at runtime. Under Native AOT Expression.Compile()
falls back to the LINQ interpreter rather than throwing, so it works — but property access is
interpreted and you carry IL2026/IL3050 trim warnings. For a workload where AOT is a requirement,
that is the gap this fills.
ValidationModules.Runtime carries IsAotCompatible and escalates IL2026;IL2055;IL2067;IL2072; IL2075;IL2087;IL3050 to errors, so the constraint is enforced by the compiler rather than by
review.
Status
Release candidate for 1.0.0. Built so far:
| Stage | ||
|---|---|---|
| 1 | Runtime — contracts, context, error model, constraint attributes, naming | done |
| 2 | Generator, no profiles | done |
| 3 | Profiles | deferred past 1.0.0 |
| 4 | Impl packaging for framework authors |
done |
| 5 | Hardened integration | substantially done |
| 6 | FluentValidation adapter and conformance suite | not started |
| — | ASP.NET Core integration | done |
Also built since the plan was written, and not in its staging: a declarative rule-class front end
(IValidationRulesFor<T>, API-SURFACE.md §19) and a DataAnnotations front end (§18).
Profiles and overlays are deferred past 1.0.0, and their declaration surfaces have been
withdrawn. Both shipped before the features behind them: using a profile argument was VM0019, an
error, and [ValidationOverlayFor<T>] was read by nothing at all. A 1.0.0 pins the public surface,
and members whose only behaviour is a build failure — or no behaviour whatsoever — are the wrong
thing to pin. Every removal is additively reversible; docs/deferred-features.md records the
analysis, including the one member set that needs default interface methods to come back without
breaking implementers.
Every declared diagnostic now has a report site. DiagnosticCatalogueTests fails in both
directions, so a new descriptor without one cannot ship.
Documentation
The docs site lives in website/ and publishes to
https://ipjohnson.github.io/ValidationModules/:
cd website && npm install && npm run dev
Dead internal links fail the build, so a rename cannot rot a link silently.
Design
IMPLEMENTATION-PLAN.md— what is being built and why. A specification, not a discussion document.API-SURFACE.md— the exact public surface, the reasoning behind each decision, and the verification log behind the claims.
The single most consequential decision is in API-SURFACE.md §13.1: ValidationContext is a
readonly struct rather than a ref struct, carrying its own path rather than indexing into shared
storage. That is what lets IAsyncValidatorFor<T> take the same context as the synchronous side,
and what makes a context safe to hold across an await or hand to a concurrent branch.
Packages
| Package | Ships as | Referenced by |
|---|---|---|
ValidationModules.Runtime |
lib/ |
application code |
ValidationModules.SourceGenerator |
analyzers/dotnet/cs |
application code, PrivateAssets=all |
ValidationModules.AspNetCore |
lib/ |
web applications |
ValidationModules.SourceGenerator.Impl |
source-only | framework authors |
ValidationModules.FluentValidation |
lib/ |
planned adapter |
ValidationModules.Testing |
lib/ |
planned conformance suite |
ValidationModules.Runtime depends only on Microsoft.Extensions.DependencyInjection.Abstractions,
framework-matched per TFM. It does not reference DependencyModules.Runtime — the library is
DependencyModules-shaped in its ergonomics, but only the generated module needs DM types, and that
lands in the consumer's assembly, which already references DM.
Building
dotnet build --configuration Release
dotnet test --configuration Release
dotnet test --configuration Release --collect:"XPlat Code Coverage" --settings coverlet.runsettings
The public API is pinned by a snapshot at
tests/ValidationModules.Runtime.Tests/Snapshots/PublicApiTests.RuntimeApi.verified.txt — one file
listing every public type and member, which is also the quickest way to read the surface. To accept
an intended change:
UPDATE_SNAPSHOTS=1 dotnet test tests/ValidationModules.Runtime.Tests
ASP.NET Core
builder.Services.AddMyAppValidators();
app.MapPost("/orders", (CreateOrder order) => Results.Ok())
.Validate<CreateOrder>();
A failure answers with RFC 9457 before the handler runs, carrying the field paths and the stable
codes. The type argument is named rather than inferred, which is what keeps the request path free of
reflection — website/guide/aspnetcore.md covers why, and what the response looks like.
Benchmarks
./scripts/benchmark.sh # ValidationModules alone, JIT and Native AOT
./scripts/benchmark.sh --quick # the same, fast enough to run after a change
./scripts/benchmark.sh --comparative # against FluentValidation and DataAnnotations
Two suites. The default one measures this library on its own and is what a change should be checked
against; the comparative one is opt-in, because its numbers move when FluentValidation changes as
well as when this does. benchmarks/README.md covers what each measures, and the four choices the
comparative suite makes in FluentValidation's favour so the comparison stays honest.
License
MIT.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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 was computed. 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. |
-
net10.0
-
net8.0
NuGet packages (2)
Showing the top 2 NuGet packages that depend on ValidationModules.Runtime:
| Package | Downloads |
|---|---|
|
Hardened.Requests.Runtime
Execution pipeline implementation for Hardened: filters, serialization, parameter validation and error handling. |
|
|
ValidationModules.AspNetCore
ASP.NET Core integration for ValidationModules: an endpoint filter that validates minimal API arguments before the handler runs, an RFC 9457 ProblemDetails mapping for ValidationResult, and an exception handler for ValidationException. No reflection, Native AOT safe. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0-rc1006 | 162 | 8/16/2026 |
| 1.0.0-rc1005 | 54 | 8/16/2026 |
| 1.0.0-rc1004 | 52 | 8/16/2026 |
| 1.0.0-rc1003 | 188 | 8/14/2026 |
| 1.0.0-rc1002 | 55 | 8/14/2026 |
| 0.1.0-alpha.1 | 106 | 8/13/2026 |