-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Description
🚀 Goal
Provide a runtime implementation of IValidatableTypeInfoResolver so that minimal-API validation still works when the source-generator path is unavailable (e.g., dynamic compilation, IDEs without generators, or environments where generators are turned off).
We already have a runtime implementation for parameter discovery (RuntimeValidatableParameterInfoResolver), but type discovery still falls back to the generated-code path. This issue tracks filling that gap.
📚 Background & Current State
-
Compile-time story
TheMicrosoft.AspNetCore.Http.ValidationsGeneratorsource-generator analyzes user code and emits aGeneratedValidatableInfoResolverthat can resolve every validatable type/property via static look-ups (no reflection, very AOT-friendly). -
Runtime story
RuntimeValidatableParameterInfoResolveralready examines method parameters with reflection.- The type side (
TryGetValidatableTypeInfo) is currently a stub that always returnsfalse.
-
Why we need a runtime fallback
- Enables validation in projects that do not reference the generator-capable SDK.
- Keeps behavior consistent when developers disable generators during debugging.
- Unblocks dynamic scenarios (e.g., plugins, Roslyn scripting).
🗺️ High-level Design
| Concern | Runtime behavior |
|---|---|
| Discovery algorithm | Reflect over the supplied Type, walking public instance properties recursively to build a ValidatableTypeInfo graph that mirrors the compile-time generator’s output. |
| Performance | Cache results in ConcurrentDictionary<Type, IValidatableInfo?> to avoid repeated reflection. |
| Cycles | Track a HashSet<Type> during the walk to break infinite recursion (return null for already-seen types). |
| Trimming | Avoid type.GetProperties() overloads that allocate attribute arrays; use BindingFlags filters and store only needed PropertyInfos. |
| Thread-safety | All caches must be static and thread-safe; rely on ConcurrentDictionary.GetOrAdd instead of lock. |
| Registration order | Add the new resolver to ValidationOptions.Resolvers after generated resolvers so compile-time wins when present, but before any user-added fallback. |
🔨 Step-by-Step Tasks
-
Create the file
src/Http/Http.Abstractions/src/Validation/RuntimeValidatableTypeInfoResolver.cs
(namespaceMicrosoft.AspNetCore.Http.Validation). -
Scaffold the class
internal sealed class RuntimeValidatableTypeInfoResolver : IValidatableInfoResolver { private static readonly ConcurrentDictionary<Type, IValidatableInfo?> _cache = new(); public bool TryGetValidatableTypeInfo( Type type, [NotNullWhen(true)] out IValidatableInfo? info) { // TODO – implement } // Parameter discovery is handled elsewhere public bool TryGetValidatableParameterInfo( ParameterInfo p, [NotNullWhen(true)] out IValidatableInfo? i) { i = null; return false; } }
-
Implement the discovery walk
- Bail out early if the type is primitive,
enum, or one of the special cases handled byRuntimeValidatableParameterInfoResolver.IsClass. - Collect
[ValidationAttribute]instances applied to the type. - Iterate over each
PropertyInfo:- Determine flags:
IsEnumerable→ implementsIEnumerablebut notstring.IsNullable→Nullable.GetUnderlyingType!=nullor reference type.IsRequired→ property has[Required]or is a non-nullable reference type.HasValidatableType→ recurse intoproperty.PropertyTypeand check result.
- Construct a
RuntimeValidatablePropertyInfoobject (mirrors the pattern in the parameter resolver).
- Determine flags:
- Create a
RuntimeValidatableTypeInfoinstance derived fromValidatableTypeInfo. - Cache the result (
nullcounts) before returning.
- Bail out early if the type is primitive,
-
Unit tests
-
Add tests under
src/Http/Http.Extensions/test/…. -
Test cases:
Scenario Expectation POCO with [Required]propertiesAttributes surfaced Nested complex types Recursion works Collection of complex types IsEnumerable == true,HasValidatableType == trueCyclic reference (A ↔ B) No stack overflow; duplicate types handled -
Use
Validator.TryValidateObjectin assertions to validate behavior end-to-end.
-
-
Wire-up
InServiceCollectionValidationExtensions.AddValidation, register:options.Resolvers.Add(new RuntimeValidatableTypeInfoResolver());
Place it after generated resolver registration.
✅ Acceptance Criteria
- A sample minimal-API app without the ValidationsGenerator package validates request bodies & query parameters successfully at runtime.
- All new and existing unit tests pass