Implement runtime-based `IValidatableTypeInfoResolver` implementation
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 290
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**
The `Microsoft.AspNetCore.Http.ValidationsGenerator` source-generator analyzes user code and emits a `GeneratedValidatableInfoResolver` 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 returns `false`.
* **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` 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
1. **Create the file**
`src/Http/Http.Abstractions/src/Validation/RuntimeValidatableTypeInfoResolver.cs`
(namespace `Microsoft.AspNetCore.Http.Validation`).
2. **Scaffold the class**
```csharp
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;
}
}
```
3. **Implement the discovery walk**
* Bail out early if the type is primitive, `enum`, or one of the special cases handled by `RuntimeValidatableParameterInfoResolver.IsClass`.
* Collect `[ValidationAttribute]` instances applied to the type.
* Iterate over each `PropertyInfo`:
* Determine flags:
* `IsEnumerable` → implements `IEnumerable` but **not** `string`.
* `IsNullable` → `Nullable.GetUnderlyingType` != `null` or reference type.
* `IsRequired` → property has `[Required]` **or** is a non-nullable reference type.
* `HasValidatableType` → recurse into `property.PropertyType` and check result.
* Construct a `RuntimeValidatablePropertyInfo` object (mirrors the pattern in the parameter resolver).
* Create a `RuntimeValidatableTypeInfo` instance derived from `ValidatableTypeInfo`.
* Cache the result (`null` counts) before returning.
4. **Unit tests**
* Add tests under `src/Http/Http.Extensions/test/…`.
* Test cases:
| Scenario | Expectation |
|----------|-------------|
| POCO with `[Required]` properties | Attributes 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.
5. **Wire-up**
In `ServiceCollectionValidationExtensions.AddValidation`, register:
```csharp
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
Contributor guide
Assessment
This issue has not been assessed yet.