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.ValidationsGenerator
source-generator analyzes user code and emits aGeneratedValidatableInfoResolver
that can resolve every validatable type/property via static look-ups (no reflection, very AOT-friendly). -
Runtime story
RuntimeValidatableParameterInfoResolver
already 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 PropertyInfo s. |
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
→ implementsIEnumerable
but notstring
.IsNullable
→Nullable.GetUnderlyingType
!=null
or reference type.IsRequired
→ property has[Required]
or is a non-nullable reference type.HasValidatableType
→ recurse intoproperty.PropertyType
and check result.
- Construct a
RuntimeValidatablePropertyInfo
object (mirrors the pattern in the parameter resolver).
- Determine flags:
- Create a
RuntimeValidatableTypeInfo
instance derived fromValidatableTypeInfo
. - Cache the result (
null
counts) 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 == true
Cyclic reference (A ↔ B) No stack overflow; duplicate types handled -
Use
Validator.TryValidateObject
in 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
🔗 Helpful Code References
- Generator logic:
src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/*
- Existing runtime parameter resolver:
src/Http/Http.Abstractions/src/Validation/RuntimeValidatableParameterInfoResolver.cs