From d5dd86f19c9c5cfecf16c7ef9cc9427e6a4d4ca2 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 30 Dec 2025 10:06:07 +0200 Subject: [PATCH 01/18] Add second impl for netstandard2 --- src/Verifast/IValidator.cs | 60 +++++++++++++++++- src/Verifast/ValidationResult.cs | 11 +++- src/Verifast/Validator.cs | 62 +++++++++++++++++++ src/Verifast/Verifast.csproj | 9 ++- .../Verifast.Tests.Unit.csproj | 2 +- 5 files changed, 137 insertions(+), 7 deletions(-) diff --git a/src/Verifast/IValidator.cs b/src/Verifast/IValidator.cs index f837c4b..e2ed935 100644 --- a/src/Verifast/IValidator.cs +++ b/src/Verifast/IValidator.cs @@ -1,5 +1,6 @@ namespace Verifast; +#if NET9_0_OR_GREATER /// /// Interface for implementing a synchronous validator for with message type /// @@ -54,4 +55,61 @@ public interface IAsyncValidator { /// /// ValueTask> ValidateAsync(T instance, CancellationToken ct = default); -} \ No newline at end of file +} +#else +/// +/// Interface for implementing a synchronous validator for with message type +/// +/// +/// +public interface IValidator { + /// + /// Validate method + /// + /// + /// + void Validate(in T instance, ref ValidationResult result); +} + +/// +/// Interface for implementing a synchronous validator for +/// +/// +public interface IValidator { + /// + /// Validate method + /// + /// + /// + void Validate(in T instance, ref ValidationResult result); +} + +/// +/// Interface for implementing an asynchronous validator for with message type +/// +/// +/// +public interface IAsyncValidator { + /// + /// ValidateAsync method + /// + /// + /// + /// + Task> ValidateAsync(T instance, CancellationToken ct = default); +} + +/// +/// Interface for implementing an asynchronous validator for +/// +/// +public interface IAsyncValidator { + /// + /// ValidateAsync method + /// + /// + /// + /// + Task> ValidateAsync(T instance, CancellationToken ct = default); +} +#endif \ No newline at end of file diff --git a/src/Verifast/ValidationResult.cs b/src/Verifast/ValidationResult.cs index 56cbca5..3486abd 100644 --- a/src/Verifast/ValidationResult.cs +++ b/src/Verifast/ValidationResult.cs @@ -9,6 +9,13 @@ public struct ValidationResult { private List? _errors; private List? _warnings; + private static readonly ReadOnlyCollection Empty = +#if NET + ReadOnlyCollection.Empty; +#else + new(Array.Empty()); +#endif + /// /// The result is valid if no errors were found. /// @@ -20,7 +27,7 @@ public struct ValidationResult { public readonly ReadOnlyCollection Errors => _errors is not null ? new ReadOnlyCollection(_errors) - : ReadOnlyCollection.Empty; + : Empty; /// /// A of the @@ -28,7 +35,7 @@ public readonly ReadOnlyCollection Errors public readonly ReadOnlyCollection Warnings => _warnings is not null ? new ReadOnlyCollection(_warnings) - : ReadOnlyCollection.Empty; + : Empty; /// /// Adds an error to the validation result diff --git a/src/Verifast/Validator.cs b/src/Verifast/Validator.cs index 4127f4a..1039b80 100644 --- a/src/Verifast/Validator.cs +++ b/src/Verifast/Validator.cs @@ -4,6 +4,7 @@ /// A static class providing the main APIs for validation /// public static class Validator { +#if NET9_0_OR_GREATER /// /// Validate /// @@ -71,4 +72,65 @@ public static bool TryValidate(this TValidator validator, in T in validator.Validate(in instance, ref result); return result.IsValid; } +#else + /// + /// Validate + /// + /// + /// + /// + /// + /// + /// + public static ValidationResult Validate(this TValidator validator, in T instance) { + ValidationResult result = default; + validator.Validate(in instance, ref result); + return result; + } + + /// + /// Validate + /// + /// + /// + /// + /// + /// + public static ValidationResult Validate(this TValidator validator, in T instance) { + ValidationResult result = default; + validator.Validate(in instance, ref result); + return result; + } + + /// + /// Try Validate + /// + /// + /// + /// + /// + /// + /// + /// True if is valid. + public static bool TryValidate(this TValidator validator, in T instance, out ValidationResult result) { + result = default; + validator.Validate(in instance, ref result); + return result.IsValid; + } + + /// + /// Try Validate + /// + /// + /// + /// + /// + /// + /// True if is valid. + public static bool TryValidate(this TValidator validator, in T instance, out ValidationResult result) { + result = default; + validator.Validate(in instance, ref result); + return result.IsValid; + } +#endif } \ No newline at end of file diff --git a/src/Verifast/Verifast.csproj b/src/Verifast/Verifast.csproj index f8df1d7..8fa2306 100644 --- a/src/Verifast/Verifast.csproj +++ b/src/Verifast/Verifast.csproj @@ -3,16 +3,19 @@ 0.1.0.0 - net9.0 + net9.0;netstandard2.0 enable enable + latest true true true true latest-recommended - true - true + true + true David Shnayder David Shnayder diff --git a/tests/Verifast.Tests.Unit/Verifast.Tests.Unit.csproj b/tests/Verifast.Tests.Unit/Verifast.Tests.Unit.csproj index 1c67b6b..01951d2 100644 --- a/tests/Verifast.Tests.Unit/Verifast.Tests.Unit.csproj +++ b/tests/Verifast.Tests.Unit/Verifast.Tests.Unit.csproj @@ -5,7 +5,7 @@ enable Exe Verifast.Tests.Unit - net9.0 + net9.0;netstandard2.0 @@ -27,9 +16,12 @@ - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + From c14619669ce6514653eb89c5f6d3aaa60414e80b Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 30 Dec 2025 10:57:41 +0200 Subject: [PATCH 06/18] Use ValueTask by conditionally add dependency --- AGENTS.md | 2 +- src/Verifast/IAsyncValidator.cs | 30 ++++++++++ src/Verifast/IValidator.cs | 58 ------------------- src/Verifast/Verifast.csproj | 8 ++- .../Verifast.Tests.Unit.Standard/AsyncUser.cs | 2 +- 5 files changed, 38 insertions(+), 62 deletions(-) create mode 100644 src/Verifast/IAsyncValidator.cs diff --git a/AGENTS.md b/AGENTS.md index 728cab9..a3b05df 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,7 +9,7 @@ This file provides guidance to agents when working with code in this repository. ## Big‑Picture Architecture - Validation contracts: - `IValidator` and `IValidator`: synchronous validators. On `net9.0`, both use `where T : allows ref struct` so validators can be used with stack‑only types without forcing `ref struct` everywhere. On `netstandard2.0`, the constraint is omitted. - - `IAsyncValidator` and `IAsyncValidator`: asynchronous validators. On `net9.0` they return `ValueTask>`; on `netstandard2.0` they return `Task>` and are declared with `in T` variance. + - `IAsyncValidator` and `IAsyncValidator`: asynchronous validators returning `ValueTask>` across targets. The `netstandard2.0` forms are declared with `in T` variance. - Orchestrator APIs: - Static `Validator` class: extension methods for synchronous validation and `TryValidate` overloads. Only sync helpers exist (no async orchestrator today). On `net9.0` the extensions include `allows ref struct` constraints. - Result type: diff --git a/src/Verifast/IAsyncValidator.cs b/src/Verifast/IAsyncValidator.cs new file mode 100644 index 0000000..7c97afa --- /dev/null +++ b/src/Verifast/IAsyncValidator.cs @@ -0,0 +1,30 @@ +namespace Verifast; + +/// +/// Interface for implementing an asynchronous validator for with message type +/// +/// +/// +public interface IAsyncValidator { + /// + /// ValidateAsync method + /// + /// + /// + /// + ValueTask> ValidateAsync(T instance, CancellationToken ct = default); +} + +/// +/// Interface for implementing an asynchronous validator for +/// +/// +public interface IAsyncValidator { + /// + /// ValidateAsync method + /// + /// + /// + /// + ValueTask> ValidateAsync(T instance, CancellationToken ct = default); +} \ No newline at end of file diff --git a/src/Verifast/IValidator.cs b/src/Verifast/IValidator.cs index 73656d4..6c8959a 100644 --- a/src/Verifast/IValidator.cs +++ b/src/Verifast/IValidator.cs @@ -27,35 +27,6 @@ public interface IValidator where T : allows ref struct { /// void Validate(in T instance, ref ValidationResult result); } - -/// -/// Interface for implementing an asynchronous validator for with message type -/// -/// -/// -public interface IAsyncValidator { - /// - /// ValidateAsync method - /// - /// - /// - /// - ValueTask> ValidateAsync(T instance, CancellationToken ct = default); -} - -/// -/// Interface for implementing an asynchronous validator for -/// -/// -public interface IAsyncValidator { - /// - /// ValidateAsync method - /// - /// - /// - /// - ValueTask> ValidateAsync(T instance, CancellationToken ct = default); -} #else /// /// Interface for implementing a synchronous validator for with message type @@ -83,33 +54,4 @@ public interface IValidator { /// void Validate(in T instance, ref ValidationResult result); } - -/// -/// Interface for implementing an asynchronous validator for with message type -/// -/// -/// -public interface IAsyncValidator { - /// - /// ValidateAsync method - /// - /// - /// - /// - Task> ValidateAsync(T instance, CancellationToken ct = default); -} - -/// -/// Interface for implementing an asynchronous validator for -/// -/// -public interface IAsyncValidator { - /// - /// ValidateAsync method - /// - /// - /// - /// - Task> ValidateAsync(T instance, CancellationToken ct = default); -} #endif \ No newline at end of file diff --git a/src/Verifast/Verifast.csproj b/src/Verifast/Verifast.csproj index 8fa2306..0c6634b 100644 --- a/src/Verifast/Verifast.csproj +++ b/src/Verifast/Verifast.csproj @@ -1,7 +1,7 @@  - 0.1.0.0 + 1.0.0.0 net9.0;netstandard2.0 enable @@ -37,6 +37,10 @@ + + + + portable true @@ -54,4 +58,4 @@ - \ No newline at end of file + diff --git a/tests/Verifast.Tests.Unit.Standard/AsyncUser.cs b/tests/Verifast.Tests.Unit.Standard/AsyncUser.cs index 6ccf64a..d46cb36 100644 --- a/tests/Verifast.Tests.Unit.Standard/AsyncUser.cs +++ b/tests/Verifast.Tests.Unit.Standard/AsyncUser.cs @@ -7,7 +7,7 @@ public class AsyncUserDto : IAsyncValidator { public string? Password { get; set; } public string? Phone { get; set; } - public async Task> ValidateAsync(AsyncUserDto instance, CancellationToken ct = default) { + public async ValueTask> ValidateAsync(AsyncUserDto instance, CancellationToken ct = default) { // Emulate asynchronous work so tests exercise the async path await Task.Yield(); ct.ThrowIfCancellationRequested(); From a039a077919cd95c488878fa63936ae8db2c9fe3 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 30 Dec 2025 11:02:09 +0200 Subject: [PATCH 07/18] Make T contravarient --- src/Verifast/IAsyncValidator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Verifast/IAsyncValidator.cs b/src/Verifast/IAsyncValidator.cs index 7c97afa..aad2e0a 100644 --- a/src/Verifast/IAsyncValidator.cs +++ b/src/Verifast/IAsyncValidator.cs @@ -5,7 +5,7 @@ /// /// /// -public interface IAsyncValidator { +public interface IAsyncValidator { /// /// ValidateAsync method /// @@ -19,7 +19,7 @@ public interface IAsyncValidator { /// Interface for implementing an asynchronous validator for /// /// -public interface IAsyncValidator { +public interface IAsyncValidator { /// /// ValidateAsync method /// From d8a1b009b96ec32700174df6c640bf3a660973f0 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 30 Dec 2025 11:11:18 +0200 Subject: [PATCH 08/18] Adjust dependencies for netcore --- AGENTS.md | 4 ++-- src/Verifast/Verifast.csproj | 2 +- .../Verifast.Tests.Unit.Standard.csproj | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a3b05df..fb74b74 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,12 +3,12 @@ This file provides guidance to agents when working with code in this repository. ## Project Overview -- Tech: Multi‑targeted .NET library (`src/Verifast`) targeting `net9.0` and `netstandard2.0`, focused on fast, allocation‑aware validation. Unit tests live in `tests/Verifast.Tests.Unit` (net9.0) using xUnit v3 with the Microsoft Testing Platform, and `tests/Verifast.Tests.Unit.Standard` (net8.0) using xUnit v2 to exercise the `netstandard2.0` target. A minimal benchmarks project exists under `benchmarks/Verifast.Benchmarks` (net9.0). +- Tech: Multi‑targeted .NET library (`src/Verifast`) targeting `net9.0`, `netstandard2.1`, and `netstandard2.0`, focused on fast, allocation‑aware validation. Unit tests live in `tests/Verifast.Tests.Unit` (net9.0) using xUnit v3 with the Microsoft Testing Platform, and `tests/Verifast.Tests.Unit.Standard` (net8.0) using xUnit v2 to exercise the `netstandard2.0` target. A minimal benchmarks project exists under `benchmarks/Verifast.Benchmarks` (net9.0). - Design: The library centers on simple, interface‑driven validators and a lightweight result type that captures errors and warnings only when needed. ## Big‑Picture Architecture - Validation contracts: - - `IValidator` and `IValidator`: synchronous validators. On `net9.0`, both use `where T : allows ref struct` so validators can be used with stack‑only types without forcing `ref struct` everywhere. On `netstandard2.0`, the constraint is omitted. + - `IValidator` and `IValidator`: synchronous validators. On `net9.0`, both use `where T : allows ref struct` so validators can be used with stack‑only types without forcing `ref struct` everywhere. On `netstandard2.1`/`netstandard2.0`, the constraint is omitted. - `IAsyncValidator` and `IAsyncValidator`: asynchronous validators returning `ValueTask>` across targets. The `netstandard2.0` forms are declared with `in T` variance. - Orchestrator APIs: - Static `Validator` class: extension methods for synchronous validation and `TryValidate` overloads. Only sync helpers exist (no async orchestrator today). On `net9.0` the extensions include `allows ref struct` constraints. diff --git a/src/Verifast/Verifast.csproj b/src/Verifast/Verifast.csproj index 0c6634b..edc6e57 100644 --- a/src/Verifast/Verifast.csproj +++ b/src/Verifast/Verifast.csproj @@ -3,7 +3,7 @@ 1.0.0.0 - net9.0;netstandard2.0 + net9.0;netstandard2.1;netstandard2.0 enable enable latest diff --git a/tests/Verifast.Tests.Unit.Standard/Verifast.Tests.Unit.Standard.csproj b/tests/Verifast.Tests.Unit.Standard/Verifast.Tests.Unit.Standard.csproj index eb7e254..4853328 100644 --- a/tests/Verifast.Tests.Unit.Standard/Verifast.Tests.Unit.Standard.csproj +++ b/tests/Verifast.Tests.Unit.Standard/Verifast.Tests.Unit.Standard.csproj @@ -25,7 +25,8 @@ - + From 072930cb728dddc56d05691aaaee234aa96143d0 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 30 Dec 2025 11:13:00 +0200 Subject: [PATCH 09/18] Update dependencies --- benchmarks/Verifast.Benchmarks/Verifast.Benchmarks.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/benchmarks/Verifast.Benchmarks/Verifast.Benchmarks.csproj b/benchmarks/Verifast.Benchmarks/Verifast.Benchmarks.csproj index cc0fe65..e41379e 100644 --- a/benchmarks/Verifast.Benchmarks/Verifast.Benchmarks.csproj +++ b/benchmarks/Verifast.Benchmarks/Verifast.Benchmarks.csproj @@ -8,9 +8,9 @@ - - - + + + From c609c60896891f9bbdaf4e44b176d709cd2ef954 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 30 Dec 2025 11:19:18 +0200 Subject: [PATCH 10/18] Lower trimming compatibility --- src/Verifast/Verifast.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Verifast/Verifast.csproj b/src/Verifast/Verifast.csproj index edc6e57..419e632 100644 --- a/src/Verifast/Verifast.csproj +++ b/src/Verifast/Verifast.csproj @@ -13,9 +13,9 @@ true latest-recommended true + Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true true + Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true David Shnayder David Shnayder From 4cdc707e8bc3cd1008e49e4c260e2a85bbbb6602 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 30 Dec 2025 11:21:57 +0200 Subject: [PATCH 11/18] Add standard2.0 tests to actions --- .github/workflows/unit-tests.yaml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml index 9bc8a46..123d9f7 100644 --- a/.github/workflows/unit-tests.yaml +++ b/.github/workflows/unit-tests.yaml @@ -14,4 +14,14 @@ jobs: with: platform: ${{ matrix.platform }} dotnet-version: 9.0.x - test-project-path: tests/Verifast.Tests.Unit/Verifast.Tests.Unit.csproj \ No newline at end of file + test-project-path: tests/Verifast.Tests.Unit/Verifast.Tests.Unit.csproj + unit-tests-standard: + strategy: + fail-fast: false + matrix: + platform: [ubuntu-latest, windows-latest, macos-latest] + uses: dusrdev/actions/.github/workflows/reusable-dotnet-test.yaml@main + with: + platform: ${{ matrix.platform }} + dotnet-version: 8.0.x + test-project-path: tests/Verifast.Tests.Unit.Standard/Verifast.Tests.Unit.Standard.csproj \ No newline at end of file From c90a17f8bf1f75584df20d384d1c5e6acbc607de Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 30 Dec 2025 11:40:06 +0200 Subject: [PATCH 12/18] Update docs --- README.md | 12 ++++-------- src/Verifast/Verifast.csproj | 3 +-- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 88cc841..24fbc9a 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ [![NuGet](https://img.shields.io/nuget/v/Verifast.svg?style=flat-square)](https://www.nuget.org/packages/Verifast) [![NuGet Downloads](https://img.shields.io/nuget/dt/Verifast?style=flat&label=Downloads)](https://www.nuget.org/packages/Verifast) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE) -[![.NET](https://img.shields.io/badge/.NET-9.0-512BD4?style=flat-square)](#) +[![.NET](https://img.shields.io/badge/.NET-net9.0%20%7C%20netstandard2.1%20%7C%20netstandard2.0-512BD4?style=flat-square)](#) -High‑performance, allocation‑friendly validation for .NET 9 and above. No complicated APIs, no expression trees - just plain C#. Implement a tiny interface, add errors or warnings, and you’re done. +High‑performance, allocation‑friendly validation for .NET 9 and netstandard (2.1/2.0). No complicated APIs, no expression trees - just plain C#. Implement a tiny interface, add errors or warnings, and you’re done. ## Why Verifast @@ -21,10 +21,6 @@ High‑performance, allocation‑friendly validation for .NET 9 and above. No co dotnet add package Verifast ``` -## Versioning - -While the version is below `1.0.0.0` minor versions can change the public API without warning (SemVer will not be followed until `1.0.0.0` is reached). - ## Quick Start Define your model and implement its validator. Then call the extension helpers. @@ -34,7 +30,7 @@ using Verifast; public readonly record struct User(string? Name, int Age); -public readonly ref struct UserValidator : IValidator { +public readonly struct UserValidator : IValidator { public void Validate(in User instance, ref ValidationResult result) { if (string.IsNullOrWhiteSpace(instance.Name)) result.AddError("'Name' must be non-empty"); @@ -95,7 +91,7 @@ using Verifast; public readonly record struct Msg(string Code, string Text); -public readonly ref struct EvenValidator : IValidator { +public readonly struct EvenValidator : IValidator { public void Validate(in int value, ref ValidationResult result) { if ((value & 1) != 0) result.AddError(new Msg("NotEven", "Value must be even")); diff --git a/src/Verifast/Verifast.csproj b/src/Verifast/Verifast.csproj index 419e632..4202034 100644 --- a/src/Verifast/Verifast.csproj +++ b/src/Verifast/Verifast.csproj @@ -54,8 +54,7 @@ - - + Added support for netstandard2.0 and netstandard2.1. From d64a1cc7a6bac1e3883ed85948bdfa1c0e09ae03 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 30 Dec 2025 11:53:00 +0200 Subject: [PATCH 13/18] Use net10 for benchmarks --- benchmarks/Verifast.Benchmarks/Verifast.Benchmarks.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/Verifast.Benchmarks/Verifast.Benchmarks.csproj b/benchmarks/Verifast.Benchmarks/Verifast.Benchmarks.csproj index e41379e..e8c5c9c 100644 --- a/benchmarks/Verifast.Benchmarks/Verifast.Benchmarks.csproj +++ b/benchmarks/Verifast.Benchmarks/Verifast.Benchmarks.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable From 187a1d0025b64f6dbd859e072cee0f62a92d6ec1 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 30 Dec 2025 12:09:20 +0200 Subject: [PATCH 14/18] Add benchmark results --- .../BenchmarkRun-joined.md | 23 +++++++++++++++++++ .../Benchmarks/AsyncValidation.cs | 8 +++---- .../Benchmarks/SyncValidation.cs | 8 +++---- .../Models/BenchmarkConfig.cs | 7 ++++-- 4 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 benchmarks/Verifast.Benchmarks/BenchmarkRun-joined.md diff --git a/benchmarks/Verifast.Benchmarks/BenchmarkRun-joined.md b/benchmarks/Verifast.Benchmarks/BenchmarkRun-joined.md new file mode 100644 index 0000000..a04e927 --- /dev/null +++ b/benchmarks/Verifast.Benchmarks/BenchmarkRun-joined.md @@ -0,0 +1,23 @@ +``` + +BenchmarkDotNet v0.15.8, macOS Tahoe 26.1 (25B78) [Darwin 25.1.0] +Apple M2 Pro, 1 CPU, 10 logical and 10 physical cores +.NET SDK 10.0.101 + [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a + MediumRun : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a + +Job=MediumRun OutlierMode=RemoveAll IterationCount=15 +IterationTime=100ms LaunchCount=2 WarmupCount=10 + +``` +| Type | Method | ValidDto | Mean | Ratio | Rank | Allocated | Alloc Ratio | +|---------------- |----------------- |--------- |------------:|----------------:|-----:|----------:|--------------:| +| AsyncValidation | FluentValidation | False | 10,935.2 ns | baseline | 4 | 14104 B | | +| AsyncValidation | Verifast | False | 2,203.2 ns | 4.96x faster | 2 | 1096 B | 12.87x less | +| AsyncValidation | FluentValidation | True | 8,470.8 ns | baseline | 3 | 9808 B | | +| AsyncValidation | Verifast | True | 2,101.9 ns | 5.20x faster | 1 | 1016 B | 13.88x less | +| | | | | | | | | +| SyncValidation | FluentValidation | False | 35,687.7 ns | baseline | 4 | 63112 B | | +| SyncValidation | Verifast | False | 141.4 ns | 252.377x faster | 2 | 328 B | 192.415x less | +| SyncValidation | FluentValidation | True | 26,091.2 ns | baseline | 3 | 50063 B | | +| SyncValidation | Verifast | True | 112.3 ns | 320.264x faster | 1 | - | NA | diff --git a/benchmarks/Verifast.Benchmarks/Benchmarks/AsyncValidation.cs b/benchmarks/Verifast.Benchmarks/Benchmarks/AsyncValidation.cs index 3b48622..5af8321 100644 --- a/benchmarks/Verifast.Benchmarks/Benchmarks/AsyncValidation.cs +++ b/benchmarks/Verifast.Benchmarks/Benchmarks/AsyncValidation.cs @@ -6,7 +6,7 @@ namespace Verifast.Benchmarks.Benchmarks; public class AsyncValidation { [Params(true, false)] - public bool DtoValid { get; set; } + public bool ValidDto { get; set; } private UserProfile? _dto; private FakeUserRepository _repo = null!; @@ -18,19 +18,19 @@ public async Task Setup() { // Simulate seeding work await _repo.AddAsync("taken@spam.com"); - _dto = DtoValid + _dto = ValidDto ? UserProfileFactory.CreateValid("valid@example.com") : UserProfileFactory.CreateInvalid("taken@spam.com"); } - [Benchmark(Baseline = true)] + [Benchmark(Baseline = true, Description = "FluentValidation")] public async Task FluentValidation_Async() { var validator = new UserProfileFluentAsyncValidator(_repo); var result = await validator.ValidateAsync(_dto!); return result.Errors.Count; } - [Benchmark] + [Benchmark(Description = "Verifast")] public async Task Verifast_Async() { var validator = new AsyncUserProfileVerifastValidator(_repo); var result = await validator.ValidateAsync(_dto!); diff --git a/benchmarks/Verifast.Benchmarks/Benchmarks/SyncValidation.cs b/benchmarks/Verifast.Benchmarks/Benchmarks/SyncValidation.cs index dc9951a..0529dd5 100644 --- a/benchmarks/Verifast.Benchmarks/Benchmarks/SyncValidation.cs +++ b/benchmarks/Verifast.Benchmarks/Benchmarks/SyncValidation.cs @@ -7,25 +7,25 @@ namespace Verifast.Benchmarks.Benchmarks; [ReturnValueValidator] public class SyncValidation { [Params(true, false)] - public bool DtoValid { get; set; } + public bool ValidDto { get; set; } private UserProfile? _dto; [GlobalSetup] public void Setup() { - _dto = DtoValid + _dto = ValidDto ? UserProfileFactory.CreateValid() : UserProfileFactory.CreateInvalid(); } - [Benchmark(Baseline = true)] + [Benchmark(Baseline = true, Description = "FluentValidation")] public int FluentValidation() { var validator = new UserProfileFluentValidator(); var result = validator.Validate(_dto!); return result.Errors.Count; } - [Benchmark] + [Benchmark(Description = "Verifast")] public int Verifast() { var validator = new UserProfileVerifastValidator(); var result = validator.Validate(_dto!); diff --git a/benchmarks/Verifast.Benchmarks/Models/BenchmarkConfig.cs b/benchmarks/Verifast.Benchmarks/Models/BenchmarkConfig.cs index 56006ab..6694d2b 100644 --- a/benchmarks/Verifast.Benchmarks/Models/BenchmarkConfig.cs +++ b/benchmarks/Verifast.Benchmarks/Models/BenchmarkConfig.cs @@ -3,12 +3,14 @@ using BenchmarkDotNet.Columns; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Exporters; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Order; using BenchmarkDotNet.Reports; using BenchmarkDotNet.Running; +using Perfolizer.Horology; using Perfolizer.Mathematics.OutlierDetection; namespace Verifast.Benchmarks.Models; @@ -17,14 +19,15 @@ public class BenchmarkConfig : ManualConfig { public BenchmarkConfig() { SummaryStyle = SummaryStyle.Default.WithRatioStyle(RatioStyle.Trend); AddDiagnoser(MemoryDiagnoser.Default); - AddJob(Job.MediumRun.WithOutlierMode(OutlierMode.RemoveAll)); + AddJob(Job.MediumRun.WithOutlierMode(OutlierMode.RemoveAll).WithIterationTime(TimeInterval.FromMilliseconds(100))); AddColumnProvider(DefaultColumnProviders.Instance); AddColumn(RankColumn.Arabic); - HideColumns(Column.Error, Column.StdDev, Column.Median, Column.RatioSD); + HideColumns(Column.Error, Column.StdDev, Column.Median, Column.RatioSD, Column.Gen0, Column.Gen1, Column.Gen2); WithOrderer(new GroupByTypeOrderer()); WithOptions(ConfigOptions.JoinSummary); WithOptions(ConfigOptions.StopOnFirstError); WithOptions(ConfigOptions.DisableLogFile); + AddExporter(MarkdownExporter.GitHub); AddLogger(ConsoleLogger.Default); } } From 2e110c5b3e868bee7facec8a41dc1375f8087aaf Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 30 Dec 2025 12:21:45 +0200 Subject: [PATCH 15/18] Show benchmarks in readme --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 24fbc9a..f613b2b 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,22 @@ High‑performance, allocation‑friendly validation for .NET 9 and netstandard - Plays well with `ref struct`: APIs are designed to enable stack‑only scenarios. - Your messages, your way: use `string` or a custom `TMessage` for richer metadata. +## Benchmarks + +[BenchmarkDotNet](https://benchmarkdotnet.org/) results comparing `Verifast` to [FluentValidation](https://github.com/FluentValidation/FluentValidation) (baseline, industry standard). Full report: [BenchmarkRun-joined.md](benchmarks/Verifast.Benchmarks/BenchmarkRun-joined.md). + +| Type | Method | ValidDto | Mean | Ratio | Rank | Allocated | Alloc Ratio | +|---------------- |----------------- |--------- |------------:|----------------:|-----:|----------:|--------------:| +| AsyncValidation | FluentValidation | False | 10,935.2 ns | baseline | 4 | 14104 B | | +| AsyncValidation | Verifast | False | 2,203.2 ns | 4.96x faster | 2 | 1096 B | 12.87x less | +| AsyncValidation | FluentValidation | True | 8,470.8 ns | baseline | 3 | 9808 B | | +| AsyncValidation | Verifast | True | 2,101.9 ns | 5.20x faster | 1 | 1016 B | 13.88x less | +| | | | | | | | | +| SyncValidation | FluentValidation | False | 35,687.7 ns | baseline | 4 | 63112 B | | +| SyncValidation | Verifast | False | 141.4 ns | 252.377x faster | 2 | 328 B | 192.415x less | +| SyncValidation | FluentValidation | True | 26,091.2 ns | baseline | 3 | 50063 B | | +| SyncValidation | Verifast | True | 112.3 ns | 320.264x faster | 1 | - | NA | + ## Install ```bash From 9c0f27e8f5f0023ae0ca43c79776266364d51231 Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 30 Dec 2025 12:24:08 +0200 Subject: [PATCH 16/18] Remove rank from results --- README.md | 22 +++++++++---------- .../BenchmarkRun-joined.md | 22 +++++++++---------- .../Models/BenchmarkConfig.cs | 2 +- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index f613b2b..ca3c92d 100644 --- a/README.md +++ b/README.md @@ -19,17 +19,17 @@ High‑performance, allocation‑friendly validation for .NET 9 and netstandard [BenchmarkDotNet](https://benchmarkdotnet.org/) results comparing `Verifast` to [FluentValidation](https://github.com/FluentValidation/FluentValidation) (baseline, industry standard). Full report: [BenchmarkRun-joined.md](benchmarks/Verifast.Benchmarks/BenchmarkRun-joined.md). -| Type | Method | ValidDto | Mean | Ratio | Rank | Allocated | Alloc Ratio | -|---------------- |----------------- |--------- |------------:|----------------:|-----:|----------:|--------------:| -| AsyncValidation | FluentValidation | False | 10,935.2 ns | baseline | 4 | 14104 B | | -| AsyncValidation | Verifast | False | 2,203.2 ns | 4.96x faster | 2 | 1096 B | 12.87x less | -| AsyncValidation | FluentValidation | True | 8,470.8 ns | baseline | 3 | 9808 B | | -| AsyncValidation | Verifast | True | 2,101.9 ns | 5.20x faster | 1 | 1016 B | 13.88x less | -| | | | | | | | | -| SyncValidation | FluentValidation | False | 35,687.7 ns | baseline | 4 | 63112 B | | -| SyncValidation | Verifast | False | 141.4 ns | 252.377x faster | 2 | 328 B | 192.415x less | -| SyncValidation | FluentValidation | True | 26,091.2 ns | baseline | 3 | 50063 B | | -| SyncValidation | Verifast | True | 112.3 ns | 320.264x faster | 1 | - | NA | +| Type | Method | ValidDto | Mean | Ratio | Allocated | Alloc Ratio | +|---------------- |----------------- |--------- |------------:|----------------:|----------:|--------------:| +| AsyncValidation | FluentValidation | False | 10,935.2 ns | baseline | 14104 B | | +| AsyncValidation | Verifast | False | 2,203.2 ns | 4.96x faster | 1096 B | 12.87x less | +| AsyncValidation | FluentValidation | True | 8,470.8 ns | baseline | 9808 B | | +| AsyncValidation | Verifast | True | 2,101.9 ns | 5.20x faster | 1016 B | 13.88x less | +| | | | | | | | +| SyncValidation | FluentValidation | False | 35,687.7 ns | baseline | 63112 B | | +| SyncValidation | Verifast | False | 141.4 ns | 252.377x faster | 328 B | 192.415x less | +| SyncValidation | FluentValidation | True | 26,091.2 ns | baseline | 50063 B | | +| SyncValidation | Verifast | True | 112.3 ns | 320.264x faster | - | NA | ## Install diff --git a/benchmarks/Verifast.Benchmarks/BenchmarkRun-joined.md b/benchmarks/Verifast.Benchmarks/BenchmarkRun-joined.md index a04e927..2dd69fa 100644 --- a/benchmarks/Verifast.Benchmarks/BenchmarkRun-joined.md +++ b/benchmarks/Verifast.Benchmarks/BenchmarkRun-joined.md @@ -10,14 +10,14 @@ Job=MediumRun OutlierMode=RemoveAll IterationCount=15 IterationTime=100ms LaunchCount=2 WarmupCount=10 ``` -| Type | Method | ValidDto | Mean | Ratio | Rank | Allocated | Alloc Ratio | -|---------------- |----------------- |--------- |------------:|----------------:|-----:|----------:|--------------:| -| AsyncValidation | FluentValidation | False | 10,935.2 ns | baseline | 4 | 14104 B | | -| AsyncValidation | Verifast | False | 2,203.2 ns | 4.96x faster | 2 | 1096 B | 12.87x less | -| AsyncValidation | FluentValidation | True | 8,470.8 ns | baseline | 3 | 9808 B | | -| AsyncValidation | Verifast | True | 2,101.9 ns | 5.20x faster | 1 | 1016 B | 13.88x less | -| | | | | | | | | -| SyncValidation | FluentValidation | False | 35,687.7 ns | baseline | 4 | 63112 B | | -| SyncValidation | Verifast | False | 141.4 ns | 252.377x faster | 2 | 328 B | 192.415x less | -| SyncValidation | FluentValidation | True | 26,091.2 ns | baseline | 3 | 50063 B | | -| SyncValidation | Verifast | True | 112.3 ns | 320.264x faster | 1 | - | NA | +| Type | Method | ValidDto | Mean | Ratio | Allocated | Alloc Ratio | +|---------------- |----------------- |--------- |------------:|----------------:|----------:|--------------:| +| AsyncValidation | FluentValidation | False | 10,935.2 ns | baseline | 14104 B | | +| AsyncValidation | Verifast | False | 2,203.2 ns | 4.96x faster | 1096 B | 12.87x less | +| AsyncValidation | FluentValidation | True | 8,470.8 ns | baseline | 9808 B | | +| AsyncValidation | Verifast | True | 2,101.9 ns | 5.20x faster | 1016 B | 13.88x less | +| | | | | | | | +| SyncValidation | FluentValidation | False | 35,687.7 ns | baseline | 63112 B | | +| SyncValidation | Verifast | False | 141.4 ns | 252.377x faster | 328 B | 192.415x less | +| SyncValidation | FluentValidation | True | 26,091.2 ns | baseline | 50063 B | | +| SyncValidation | Verifast | True | 112.3 ns | 320.264x faster | - | NA | diff --git a/benchmarks/Verifast.Benchmarks/Models/BenchmarkConfig.cs b/benchmarks/Verifast.Benchmarks/Models/BenchmarkConfig.cs index 6694d2b..5d767ff 100644 --- a/benchmarks/Verifast.Benchmarks/Models/BenchmarkConfig.cs +++ b/benchmarks/Verifast.Benchmarks/Models/BenchmarkConfig.cs @@ -22,7 +22,7 @@ public BenchmarkConfig() { AddJob(Job.MediumRun.WithOutlierMode(OutlierMode.RemoveAll).WithIterationTime(TimeInterval.FromMilliseconds(100))); AddColumnProvider(DefaultColumnProviders.Instance); AddColumn(RankColumn.Arabic); - HideColumns(Column.Error, Column.StdDev, Column.Median, Column.RatioSD, Column.Gen0, Column.Gen1, Column.Gen2); + HideColumns(Column.Error, Column.StdDev, Column.Median, Column.RatioSD, Column.Gen0, Column.Gen1, Column.Gen2, Column.Rank); WithOrderer(new GroupByTypeOrderer()); WithOptions(ConfigOptions.JoinSummary); WithOptions(ConfigOptions.StopOnFirstError); From 0889a39af970a7a07149c215a69d93c74478138a Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 30 Dec 2025 12:32:05 +0200 Subject: [PATCH 17/18] Include debug symbols in nuget package --- src/Verifast/Verifast.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Verifast/Verifast.csproj b/src/Verifast/Verifast.csproj index 4202034..7d3f1db 100644 --- a/src/Verifast/Verifast.csproj +++ b/src/Verifast/Verifast.csproj @@ -44,6 +44,8 @@ portable true + true + snupkg $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb From 855128a0b2b99ede448b0e15a380445edefb155c Mon Sep 17 00:00:00 2001 From: David Shnayder Date: Tue, 30 Dec 2025 12:33:37 +0200 Subject: [PATCH 18/18] Update CI --- .github/workflows/benchmarks.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/benchmarks.yaml b/.github/workflows/benchmarks.yaml index 09b18c6..52e1929 100644 --- a/.github/workflows/benchmarks.yaml +++ b/.github/workflows/benchmarks.yaml @@ -1,7 +1,6 @@ name: Benchmarks on: - pull_request: workflow_dispatch: jobs: @@ -18,7 +17,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 9.0.x + dotnet-version: 10.0.x - name: Run Benchmarks run: dotnet run --project ${{ env.PROJECT }} --configuration Release -- --filter '*'