You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Light.PortableResults already models validation failures well at the transport boundary through ErrorCategory.Validation, Error.Target, and HTTP validation problem serialization, but it does not yet provide an
in-process validation API for DTOs. This plan introduces the foundational validation types by selectively adapting the
successful parts of Light.Validation: a ValidationContext that owns configuration and error creation, a low-allocation Check<T> struct for validating a single value, a dedicated ValidationOutcome<T> result model, and validator base
classes for DTO-centric workflows. The design should align with PortableResults primitives instead of copying the old
library verbatim: validation failures become flat Error entries inside Errors, hierarchical paths are represented
through composed Error.Target values such as address.zipCode or addresses[0].zipCode, synchronous convenience
APIs can still produce plain Result failures for Minimal APIs or MVC, and validators may either normalize a DTO in
place or transform it into another validated type for anti-corruption boundaries.
Acceptance Criteria
A new project src/Light.PortableResults.Validation exists, targets netstandard2.0, references Light.PortableResults, is added to the solution, and src/AGENTS.md is updated to describe the new project.
A corresponding test project tests/Light.PortableResults.Validation.Tests exists for the new validation package
and is added to the solution.
The validation foundation exposes public types for ValidationContext, Check<T>, ValidationOutcome<T>, BaseValidator<TSource>, Validator<T>, Validator<TSource, TValidated>, AsyncValidator<T>, AsyncValidator<TSource, TValidated>, IValidationContextFactory, IValidationTargetNormalizer, a default ValidationContextFactory, and the supporting options/template types needed to create contexts.
ValidationContext lazily accumulates validation failures as flat Error entries with ErrorCategory.Validation, creates Check<T> instances via caller argument expressions, normalizes strings when
configured, and materializes failures as Errors / Result values without nested error containers.
Public path-normalization and path-composition tooling is available so callers can build validation targets with
the same rules as the library instead of relying on internal-only helpers.
Check<T> is implemented as a low-overhead readonly struct that supports normalized value flow,
target/display-name handling, short-circuiting, and manual error creation so that future check extension methods can
build on it without redesigning the type.
The built-in target normalizer supports configurable casing conventions, uses cache-backed default normalization,
and is configured through the selected normalizer instance rather than through a conflicting parallel casing option on ValidationContextOptions.
ValidationOutcome<T> preserves the validated value on success, stores flat Errors on failure, and provides
helpers to convert failures into Result so that endpoint code can stay concise without losing a path to normalized
values.
Validator<T> performs automatic null checking, delegates rule execution to PerformValidation(ValidationContext, T), and returns ValidationOutcome<T> while also offering synchronous
convenience APIs such as CheckForErrors(...) / TryValidate(...) for short endpoint code.
Validator<TSource, TValidated> supports validation plus transformation to a different validated output type,
enabling immutable records, commands, and anti-corruption-layer mappings.
AsyncValidator<T> and AsyncValidator<TSource, TValidated> provide the same validation and transformation
model for I/O-bound validation workflows, accept CancellationToken, and return ValueTask<ValidationOutcome<T>> / ValueTask<ValidationOutcome<TValidated>>.
Nested validation is modeled through target prefixing rather than nested Errors instances, and the foundation
provides the path-composition hooks needed for future child-validator and collection-validation extensions.
Benchmarks compare Light.PortableResults.Validation against FluentValidation 12.1.1 using two equivalent
in-memory Minimal API endpoints: one with simple validation logic and one with more complex validation logic. The
benchmarks measure runtime performance and allocated memory, with FluentValidation as the baseline. Both implementations
must use the same DTOs and produce behaviorally equivalent responses so the comparison stays meaningful.
Technical Details
Create Light.PortableResults.Validation as a framework-agnostic companion package, not as an ASP.NET-specific
integration. It should target netstandard2.0 like the core project so the validation API can be reused in HTTP, gRPC,
messaging, and non-web code. Because the design relies on CallerArgumentExpressionAttribute for ergonomics, the
project will need the same kind of compatibility support that the solution already uses for newer C# features on older
target frameworks.
For the original design reference, review these Light.Validation sources before implementing the new types:
Do not port Light.Validation’s ExtensibleObject, Dictionary<string, object> error store, or MultipleErrorsPerKeyBehavior. Those concepts were useful for nested error graphs, but they conflict with
PortableResults’ flat Errors value object. ValidationContext should instead own a lightweight internal error builder
that avoids allocations on the success path and grows only when errors are added. The builder can stay internal, but it
should be designed like the rest of the library: keep the first error inline, expand lazily, and only materialize an Errors instance when the caller asks for one. ValidationContext should also own immutable references to ValidationContextOptions and ValidationErrorTemplates, expose Check<T>(...), AddError(...), TryGetErrors(...),
and convenience methods that convert the current state into Errors and Result failures.
ValidationContextOptions should keep only the options that still make sense with flat errors: target normalization,
string normalization, and automatic-null-error creation. The old key comparer and multiple-errors-per-key settings
should be dropped. Likewise, the target normalizer should no longer be just a delegate. Introduce a public IValidationTargetNormalizer with string Normalize(string rawPath) so callers can plug in their own policy while the
default implementation remains free to add caching internally. The default behavior should preserve member paths instead
of taking only the last segment: dto.Address.ZipCode should become address.zipCode, and collection/indexer syntax
should remain expressible as addresses[0].zipCode. An empty target string must continue to represent the root object
because the existing HTTP serialization already treats "" as the root validation target.
Do not force callers to replace the entire normalizer just to switch the casing convention. The built-in normalizer
should expose a first-class casing option, for example via a small enum that supports at least CamelCase, PascalCase, and Preserve, so callers can align validation targets with their DTO/JSON naming conventions while
still benefiting from the default parser and cache. That casing setting should configure the built-in normalizer
instance itself through its constructor. ValidationContextOptions should
then hold only the selected IValidationTargetNormalizer instance, avoiding an ambiguous API where a separate TargetCasing setting might conflict with a custom normalizer.
Make the default target-path normalization rules explicit in the implementation and tests: remove irrelevant leading
parameter roots such as dto., preserve the member path, and convert member names to the expected lower-camel-case
segments so that expressions like dto.Address.ZipCode become address.zipCode. The default normalizer should own a
thread-safe cache for rawPath -> normalizedPath mappings, but only for the built-in normalization strategy. Do not
globally cache prefix-composed paths such as addresses[123].zipCode because indexed prefixes can grow without a
useful bound.
ValidationErrorTemplates should stay close to Light.Validation’s strengths, but adapted to Error. Keep the localized
format strings and formatting helpers so future rule extensions can reuse them, but do not make templates responsible
for storing errors. The future check extension methods should use the templates to create Error.Message values while
setting Error.Code, Error.Target, Error.Category, and optional MetadataObject explicitly. This is where the
PortableResults metadata system comes in: machine-readable validation details belong on Error.Metadata, not in a
mutable object bag on the context.
Check<T> should mirror the successful shape of Light.Validation’s Check<T> while fitting the current library style.
Make it a readonly struct that carries the context, raw or normalized target, display name, value, and a short-circuit
flag. Keep the implicit conversion to T so normalized values can be reassigned naturally in validators. Add manual
primitives that future extension methods can compose around: methods such as WithValue, WithDisplayName, ShortCircuit, NormalizeTargetIfNecessary, and AddError. AddError should support both fully constructed Error
values and a simpler overload for message-based failures, automatically applying the check’s target when no explicit
target is provided.
Introduce a dedicated ValidationOutcome<T> value type as the primary result model of the validation package. It
should expose a non-throwing Value property, flat Errors, and IsValid / HasErrors semantics, plus ToFailureResult() so callers can easily convert failures into Result for HTTP and MVC integration. The type should
not revive Light.Validation’s loosely typed ValidationResult<T> shape; it should stay aligned with PortableResults
primitives by using Errors directly. Errors should always be available and use Errors' empty instance when no
failures occurred. ToFailureResult() should throw when called on a valid outcome. For invalid outcomes, Value
should still be readable and should contain the best available normalized or transformed value, or default(T) when no
meaningful value could be produced, such as after an automatic null check or an aborted transformation. This behavior
should be documented with appropriate nullability annotations rather than hidden behind throwing accessors.
Introduce a public BaseValidator<TSource> plus synchronous and asynchronous validator base classes. Validator<T>
should keep Light.Validation’s ergonomic PerformValidation(ValidationContext, T) shape for the common case where
validation returns the same DTO type after normalization. In addition, add Validator<TSource, TValidated> with PerformValidation(ValidationContext, TSource) returning TValidated so validation can also form an anti-corruption
layer that maps incoming DTOs to immutable record structs, commands, or other validated internal models. Mirror both
shapes with AsyncValidator<T> and AsyncValidator<TSource, TValidated> using asynchronous PerformValidationAsync(...) methods that accept CancellationToken and return ValueTask<ValidationOutcome<T>> / ValueTask<ValidationOutcome<TValidated>>. ValueTask is preferable here because
many validators will still complete synchronously even when they participate in an async pipeline, and the library is
explicitly performance-focused. The async implementation should also follow normal library guidance for netstandard2.0, including the deliberate use of ConfigureAwait(false) where appropriate.
The public API should center on Validate(...) overloads returning ValidationOutcome<T> or ValidationOutcome<TValidated>, with overloads that accept an existing ValidationContext for multi-step validation
pipelines. On top of that, provide synchronous convenience methods tailored to endpoint ergonomics, for example CheckForErrors(...) and TryValidate(...), so callers can keep short patterns such as if (validator.CheckForErrors(dto, out Result failure)) return failure.ToMinimalApiResult();. These convenience
methods should be thin adapters over ValidationOutcome<T> rather than the primary abstraction, because out-based
patterns do not translate to async validators. Async validators should therefore expose only outcome-based APIs such as ValidateAsync(...), keeping the model consistent while avoiding awkward out-parameter designs. All sync and async
APIs should use nullability annotations carefully so the compiler can understand when validated values or errors are
available.
For transformed validators (TSource -> TValidated), the validated output should only be considered available on
success. That keeps the API clean for immutable and anti-corruption use cases where a partially built output object is
usually not meaningful. For same-type validators (T -> T), callers can still mutate a reference-type DTO in place
while validating, but the formal contract remains ValidationOutcome<T> rather than exposing partially normalized
values on failure through Result<T>.
To preserve a path for future child validators without nested error trees, keep a scoped-context concept in the design. IValidationContextFactory should support creating both root contexts and child/scoped contexts that share the same
underlying error sink while prepending a target prefix. The child context no longer owns its own nested error
dictionary; instead, it contributes flat errors whose targets are composed from the parent prefix and the child member
path. That keeps future ValidateWith(...) and ValidateItems(...) extensions straightforward and compatible with the
existing HTTP validation serialization logic. Do not hide that composition logic behind internal-only helpers. Expose
the same path-composition mechanism publicly, either through a small public value type or a public static helper, so
callers can build consistent validation targets when they integrate with the library manually.
Document the intended lifetime model in the implementation notes and XML docs. Validators are expected to be reusable
and will often be good singleton candidates when they only depend on singleton-safe collaborators. ValidationContext
instances, on the other hand, must be created per validation run and must never be shared across concurrent requests or
parallel validation operations.
Add a small benchmark suite in the benchmarks project that models two equivalent Minimal API endpoints twice: once with
Light.PortableResults.Validation and once with FluentValidation 12.1.1. One endpoint should exercise simple DTO
validation with only a few field checks, while the other should exercise more complex validation with nested paths,
multiple field checks, and a transformed validated output. Keep both versions behaviorally equivalent and in-memory so
the measurements reflect validation overhead rather than networking or hosting noise. Report runtime performance and
allocated memory, treating FluentValidation as the baseline.
Tests in Light.PortableResults.Validation.Tests should stay mostly unit-level and focus on behavior that locks down
the architectural decisions in this plan: no allocations or state buildup on the success path beyond the context
instance, string normalization semantics, target normalization for nested members, automatic null failures with root
targets, conversion of accumulated failures into Errors, Result, and ValidationOutcome<T>, synchronous
endpoint-oriented convenience methods, transformed validation from TSource to TValidated, async validation result
flow including cancellation propagation, nullability behavior, cache-backed default target normalization, and
scoped-context composition yielding paths such as address.zipCode and addresses[0].zipCode.
Rationale
Light.PortableResults already models validation failures well at the transport boundary through
ErrorCategory.Validation,Error.Target, and HTTP validation problem serialization, but it does not yet provide anin-process validation API for DTOs. This plan introduces the foundational validation types by selectively adapting the
successful parts of Light.Validation: a
ValidationContextthat owns configuration and error creation, a low-allocationCheck<T>struct for validating a single value, a dedicatedValidationOutcome<T>result model, and validator baseclasses for DTO-centric workflows. The design should align with PortableResults primitives instead of copying the old
library verbatim: validation failures become flat
Errorentries insideErrors, hierarchical paths are representedthrough composed
Error.Targetvalues such asaddress.zipCodeoraddresses[0].zipCode, synchronous convenienceAPIs can still produce plain
Resultfailures for Minimal APIs or MVC, and validators may either normalize a DTO inplace or transform it into another validated type for anti-corruption boundaries.
Acceptance Criteria
src/Light.PortableResults.Validationexists, targetsnetstandard2.0, referencesLight.PortableResults, is added to the solution, andsrc/AGENTS.mdis updated to describe the new project.tests/Light.PortableResults.Validation.Testsexists for the new validation packageand is added to the solution.
ValidationContext,Check<T>,ValidationOutcome<T>,BaseValidator<TSource>,Validator<T>,Validator<TSource, TValidated>,AsyncValidator<T>,AsyncValidator<TSource, TValidated>,IValidationContextFactory,IValidationTargetNormalizer, a defaultValidationContextFactory, and the supporting options/template types needed to create contexts.ValidationContextlazily accumulates validation failures as flatErrorentries withErrorCategory.Validation, createsCheck<T>instances via caller argument expressions, normalizes strings whenconfigured, and materializes failures as
Errors/Resultvalues without nested error containers.the same rules as the library instead of relying on internal-only helpers.
Check<T>is implemented as a low-overhead readonly struct that supports normalized value flow,target/display-name handling, short-circuiting, and manual error creation so that future check extension methods can
build on it without redesigning the type.
and is configured through the selected normalizer instance rather than through a conflicting parallel casing option on
ValidationContextOptions.ValidationOutcome<T>preserves the validated value on success, stores flatErrorson failure, and provideshelpers to convert failures into
Resultso that endpoint code can stay concise without losing a path to normalizedvalues.
Validator<T>performs automatic null checking, delegates rule execution toPerformValidation(ValidationContext, T), and returnsValidationOutcome<T>while also offering synchronousconvenience APIs such as
CheckForErrors(...)/TryValidate(...)for short endpoint code.Validator<TSource, TValidated>supports validation plus transformation to a different validated output type,enabling immutable records, commands, and anti-corruption-layer mappings.
AsyncValidator<T>andAsyncValidator<TSource, TValidated>provide the same validation and transformationmodel for I/O-bound validation workflows, accept
CancellationToken, and returnValueTask<ValidationOutcome<T>>/ValueTask<ValidationOutcome<TValidated>>.Errorsinstances, and the foundationprovides the path-composition hooks needed for future child-validator and collection-validation extensions.
automatic null validation, and flat hierarchical target composition.
12.1.1using two equivalentin-memory Minimal API endpoints: one with simple validation logic and one with more complex validation logic. The
benchmarks measure runtime performance and allocated memory, with FluentValidation as the baseline. Both implementations
must use the same DTOs and produce behaviorally equivalent responses so the comparison stays meaningful.
Technical Details
Create
Light.PortableResults.Validationas a framework-agnostic companion package, not as an ASP.NET-specificintegration. It should target
netstandard2.0like the core project so the validation API can be reused in HTTP, gRPC,messaging, and non-web code. Because the design relies on
CallerArgumentExpressionAttributefor ergonomics, theproject will need the same kind of compatibility support that the solution already uses for newer C# features on older
target frameworks.
For the original design reference, review these Light.Validation sources before implementing the new types:
ValidationContext: https://github.com/feO2x/Light.Validation/blob/main/Code/Light.Validation/ValidationContext.csCheck<T>: https://github.com/feO2x/Light.Validation/blob/main/Code/Light.Validation/Check.csValidator<T>: https://github.com/feO2x/Light.Validation/blob/main/Code/Light.Validation/Validator.csBaseValidator<T>: https://github.com/feO2x/Light.Validation/blob/main/Code/Light.Validation/BaseValidator.csAsyncValidator<T>: https://github.com/feO2x/Light.Validation/blob/main/Code/Light.Validation/AsyncValidator.csIValidationContextFactory: https://github.com/feO2x/Light.Validation/blob/main/Code/Light.Validation/IValidationContextFactory.csValidationContextOptions: https://github.com/feO2x/Light.Validation/blob/main/Code/Light.Validation/ValidationContextOptions.csValidationResult<T>: https://github.com/feO2x/Light.Validation/blob/main/Code/Light.Validation/ValidationResult.csErrorTemplates: https://github.com/feO2x/Light.Validation/blob/main/Code/Light.Validation/Tools/ErrorTemplates.csDo not port Light.Validation’s
ExtensibleObject,Dictionary<string, object>error store, orMultipleErrorsPerKeyBehavior. Those concepts were useful for nested error graphs, but they conflict withPortableResults’ flat
Errorsvalue object.ValidationContextshould instead own a lightweight internal error builderthat avoids allocations on the success path and grows only when errors are added. The builder can stay internal, but it
should be designed like the rest of the library: keep the first error inline, expand lazily, and only materialize an
Errorsinstance when the caller asks for one.ValidationContextshould also own immutable references toValidationContextOptionsandValidationErrorTemplates, exposeCheck<T>(...),AddError(...),TryGetErrors(...),and convenience methods that convert the current state into
ErrorsandResultfailures.ValidationContextOptionsshould keep only the options that still make sense with flat errors: target normalization,string normalization, and automatic-null-error creation. The old key comparer and multiple-errors-per-key settings
should be dropped. Likewise, the target normalizer should no longer be just a delegate. Introduce a public
IValidationTargetNormalizerwithstring Normalize(string rawPath)so callers can plug in their own policy while thedefault implementation remains free to add caching internally. The default behavior should preserve member paths instead
of taking only the last segment:
dto.Address.ZipCodeshould becomeaddress.zipCode, and collection/indexer syntaxshould remain expressible as
addresses[0].zipCode. An empty target string must continue to represent the root objectbecause the existing HTTP serialization already treats
""as the root validation target.Do not force callers to replace the entire normalizer just to switch the casing convention. The built-in normalizer
should expose a first-class casing option, for example via a small enum that supports at least
CamelCase,PascalCase, andPreserve, so callers can align validation targets with their DTO/JSON naming conventions whilestill benefiting from the default parser and cache. That casing setting should configure the built-in normalizer
instance itself through its constructor.
ValidationContextOptionsshouldthen hold only the selected
IValidationTargetNormalizerinstance, avoiding an ambiguous API where a separateTargetCasingsetting might conflict with a custom normalizer.Make the default target-path normalization rules explicit in the implementation and tests: remove irrelevant leading
parameter roots such as
dto., preserve the member path, and convert member names to the expected lower-camel-casesegments so that expressions like
dto.Address.ZipCodebecomeaddress.zipCode. The default normalizer should own athread-safe cache for
rawPath -> normalizedPathmappings, but only for the built-in normalization strategy. Do notglobally cache prefix-composed paths such as
addresses[123].zipCodebecause indexed prefixes can grow without auseful bound.
ValidationErrorTemplatesshould stay close to Light.Validation’s strengths, but adapted toError. Keep the localizedformat strings and formatting helpers so future rule extensions can reuse them, but do not make templates responsible
for storing errors. The future check extension methods should use the templates to create
Error.Messagevalues whilesetting
Error.Code,Error.Target,Error.Category, and optionalMetadataObjectexplicitly. This is where thePortableResults metadata system comes in: machine-readable validation details belong on
Error.Metadata, not in amutable
objectbag on the context.Check<T>should mirror the successful shape of Light.Validation’sCheck<T>while fitting the current library style.Make it a readonly struct that carries the context, raw or normalized target, display name, value, and a short-circuit
flag. Keep the implicit conversion to
Tso normalized values can be reassigned naturally in validators. Add manualprimitives that future extension methods can compose around: methods such as
WithValue,WithDisplayName,ShortCircuit,NormalizeTargetIfNecessary, andAddError.AddErrorshould support both fully constructedErrorvalues and a simpler overload for message-based failures, automatically applying the check’s target when no explicit
target is provided.
Introduce a dedicated
ValidationOutcome<T>value type as the primary result model of the validation package. Itshould expose a non-throwing
Valueproperty, flatErrors, andIsValid/HasErrorssemantics, plusToFailureResult()so callers can easily convert failures intoResultfor HTTP and MVC integration. The type shouldnot revive Light.Validation’s loosely typed
ValidationResult<T>shape; it should stay aligned with PortableResultsprimitives by using
Errorsdirectly.Errorsshould always be available and useErrors' empty instance when nofailures occurred.
ToFailureResult()should throw when called on a valid outcome. For invalid outcomes,Valueshould still be readable and should contain the best available normalized or transformed value, or
default(T)when nomeaningful value could be produced, such as after an automatic null check or an aborted transformation. This behavior
should be documented with appropriate nullability annotations rather than hidden behind throwing accessors.
Introduce a public
BaseValidator<TSource>plus synchronous and asynchronous validator base classes.Validator<T>should keep Light.Validation’s ergonomic
PerformValidation(ValidationContext, T)shape for the common case wherevalidation returns the same DTO type after normalization. In addition, add
Validator<TSource, TValidated>withPerformValidation(ValidationContext, TSource)returningTValidatedso validation can also form an anti-corruptionlayer that maps incoming DTOs to immutable record structs, commands, or other validated internal models. Mirror both
shapes with
AsyncValidator<T>andAsyncValidator<TSource, TValidated>using asynchronousPerformValidationAsync(...)methods that acceptCancellationTokenand returnValueTask<ValidationOutcome<T>>/ValueTask<ValidationOutcome<TValidated>>.ValueTaskis preferable here becausemany validators will still complete synchronously even when they participate in an async pipeline, and the library is
explicitly performance-focused. The async implementation should also follow normal library guidance for
netstandard2.0, including the deliberate use ofConfigureAwait(false)where appropriate.The public API should center on
Validate(...)overloads returningValidationOutcome<T>orValidationOutcome<TValidated>, with overloads that accept an existingValidationContextfor multi-step validationpipelines. On top of that, provide synchronous convenience methods tailored to endpoint ergonomics, for example
CheckForErrors(...)andTryValidate(...), so callers can keep short patterns such asif (validator.CheckForErrors(dto, out Result failure)) return failure.ToMinimalApiResult();. These conveniencemethods should be thin adapters over
ValidationOutcome<T>rather than the primary abstraction, becauseout-basedpatterns do not translate to async validators. Async validators should therefore expose only outcome-based APIs such as
ValidateAsync(...), keeping the model consistent while avoiding awkwardout-parameter designs. All sync and asyncAPIs should use nullability annotations carefully so the compiler can understand when validated values or errors are
available.
For transformed validators (
TSource -> TValidated), the validated output should only be considered available onsuccess. That keeps the API clean for immutable and anti-corruption use cases where a partially built output object is
usually not meaningful. For same-type validators (
T -> T), callers can still mutate a reference-type DTO in placewhile validating, but the formal contract remains
ValidationOutcome<T>rather than exposing partially normalizedvalues on failure through
Result<T>.To preserve a path for future child validators without nested error trees, keep a scoped-context concept in the design.
IValidationContextFactoryshould support creating both root contexts and child/scoped contexts that share the sameunderlying error sink while prepending a target prefix. The child context no longer owns its own nested error
dictionary; instead, it contributes flat errors whose targets are composed from the parent prefix and the child member
path. That keeps future
ValidateWith(...)andValidateItems(...)extensions straightforward and compatible with theexisting HTTP validation serialization logic. Do not hide that composition logic behind internal-only helpers. Expose
the same path-composition mechanism publicly, either through a small public value type or a public static helper, so
callers can build consistent validation targets when they integrate with the library manually.
Document the intended lifetime model in the implementation notes and XML docs. Validators are expected to be reusable
and will often be good singleton candidates when they only depend on singleton-safe collaborators.
ValidationContextinstances, on the other hand, must be created per validation run and must never be shared across concurrent requests or
parallel validation operations.
Add a small benchmark suite in the benchmarks project that models two equivalent Minimal API endpoints twice: once with
Light.PortableResults.Validation and once with FluentValidation
12.1.1. One endpoint should exercise simple DTOvalidation with only a few field checks, while the other should exercise more complex validation with nested paths,
multiple field checks, and a transformed validated output. Keep both versions behaviorally equivalent and in-memory so
the measurements reflect validation overhead rather than networking or hosting noise. Report runtime performance and
allocated memory, treating FluentValidation as the baseline.
Tests in
Light.PortableResults.Validation.Testsshould stay mostly unit-level and focus on behavior that locks downthe architectural decisions in this plan: no allocations or state buildup on the success path beyond the context
instance, string normalization semantics, target normalization for nested members, automatic null failures with root
targets, conversion of accumulated failures into
Errors,Result, andValidationOutcome<T>, synchronousendpoint-oriented convenience methods, transformed validation from
TSourcetoTValidated, async validation resultflow including cancellation propagation, nullability behavior, cache-backed default target normalization, and
scoped-context composition yielding paths such as
address.zipCodeandaddresses[0].zipCode.