Skip to content

Implement runtime-based IValidatableTypeInfoResolver for minimal API validation #62497

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

Copilot
Copy link
Contributor

@Copilot Copilot AI commented Jun 27, 2025

This PR implements a runtime implementation of IValidatableTypeInfoResolver to enable minimal-API validation when the source-generator path is unavailable (e.g., dynamic compilation, IDEs without generators, or environments where generators are turned off).

Background

Previously, the validation system had:

  • Compile-time story: Microsoft.AspNetCore.Http.ValidationsGenerator source-generator for AOT-friendly static lookups
  • Runtime parameter discovery: RuntimeValidatableParameterInfoResolver for method parameters
  • Runtime type discovery: TryGetValidatableTypeInfo was a stub that always returned false

This left a gap where validation wouldn't work in dynamic scenarios without the generator.

Implementation

Core Components

RuntimeValidatableTypeInfoResolver - Main resolver class that:

  • Uses reflection to walk public instance properties recursively
  • Builds ValidatableTypeInfo graphs that mirror compile-time generator output
  • Handles validation attributes like [Required], [Range], [Display], etc.

Cycle Prevention & Performance:

  • HashSet<Type> tracking during discovery prevents infinite recursion (A ↔ B references)
  • ConcurrentDictionary<Type, IValidatableInfo?> for thread-safe caching
  • Skips primitives, enums, strings, DateTime (same logic as parameter resolver)

Registration:

  • Added to ValidationOptions.Resolvers in AddValidation()
  • Placed after parameter resolver but before user-added fallbacks
  • Generated resolvers still take precedence when available

Example Usage

// Works without source generators now!
public class Person
{
    [Required]
    [StringLength(50)]
    public string Name { get; set; } = string.Empty;

    [Range(0, 120)]
    public int Age { get; set; }

    public Address Address { get; set; } = new();
}

public class Address  
{
    [Required]
    public string City { get; set; } = string.Empty;
}

// Validation now works at runtime
services.AddValidation();
// Validates nested objects, collections, handles cycles automatically

Testing

  • 11 unit tests covering primitives, complex types, collections, cycles, caching
  • 4 integration tests verifying service registration and resolver ordering
  • Manual verification with real POCOs and validation scenarios

All tests pass and validate the expected behavior matches the original issue requirements.

Fixes #61220.


💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click here to start the survey.

Copy link
Contributor

Greetings human! You've submitted a PR that modifies code that is shared with https://github.com/dotnet/runtime . Please make sure you synchronize this code with the changes in that repo!

@Copilot Copilot AI changed the title [WIP] Implement runtime-based IValidatableTypeInfoResolver implementation Implement runtime-based IValidatableTypeInfoResolver for minimal API validation Jun 27, 2025
@Copilot Copilot AI requested a review from captainsafia June 27, 2025 23:31
Copilot finished work on behalf of captainsafia June 27, 2025 23:31
@captainsafia
Copy link
Member

@copilot Update the RuntimeValidatableTypeInfoResolverTests tests to use the validatable info types returned by cases that support them to validate actual test data.

Copy link
Contributor Author

Copilot AI commented Jun 28, 2025

@copilot Update the RuntimeValidatableTypeInfoResolverTests tests to use the validatable info types returned by cases that support them to validate actual test data.

Updated the RuntimeValidatableTypeInfoResolverTests to use the returned validatable info objects to validate actual test data. The tests now verify validation behavior with both valid and invalid data scenarios, ensuring the runtime resolver works correctly in practice. Commit ee9c087

Copilot finished work on behalf of captainsafia June 28, 2025 00:12
@captainsafia captainsafia marked this pull request as ready for review June 30, 2025 16:56
Copy link
Contributor

Looks like this PR hasn't been active for some time and the codebase could have been changed in the meantime.
To make sure no conflicting changes have occurred, please rerun validation before merging. You can do this by leaving an /azp run comment here (requires commit rights), or by simply closing and reopening.

@dotnet-policy-service dotnet-policy-service bot added the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Jul 8, 2025
@captainsafia
Copy link
Member

@copilot Split up the RuntimeValidatableInfoResolver into separate types for parameters (RuntimeValidatableParameterInfoResolver) and types (RuntimeValidatableTypeInfoResolver). Mark the RuntimeValidatableTypeInfoResolver as experimental. Revert the changes made to the ValidationServiceCollectionExtensions class.

… resolvers, mark type resolver as experimental, revert ValidationServiceCollectionExtensions changes

Co-authored-by: captainsafia <[email protected]>
Copy link
Contributor Author

Copilot AI commented Jul 16, 2025

@copilot Split up the RuntimeValidatableInfoResolver into separate types for parameters (RuntimeValidatableParameterInfoResolver) and types (RuntimeValidatableTypeInfoResolver). Mark the `RuntimeV...

Split the RuntimeValidatableInfoResolver into separate RuntimeValidatableParameterInfoResolver and RuntimeValidatableTypeInfoResolver classes. Marked the RuntimeValidatableTypeInfoResolver as experimental with [Experimental("ASP0029")] attribute. Reverted the ValidationServiceCollectionExtensions changes to original state. Commit 96751ed

Copilot finished work on behalf of captainsafia July 16, 2025 01:28
@captainsafia
Copy link
Member

@copilot Make the new RuntimeValidatableTypeInfoResolver type public.

Copy link
Contributor Author

Copilot AI commented Jul 16, 2025

@copilot Make the new RuntimeValidatableTypeInfoResolver type public.

Made the RuntimeValidatableTypeInfoResolver class public and added appropriate API entries to PublicAPI.Unshipped.txt. All tests are passing successfully. Commit d0ab1a8

Copilot finished work on behalf of captainsafia July 16, 2025 04:14
Copy link
Member

@captainsafia captainsafia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot More feedback.

{
private static readonly ConcurrentDictionary<Type, IValidatableInfo?> _cache = new();

public bool TryGetValidatableTypeInfo(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Add an XML comment to this class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added XML documentation to the TryGetValidatableTypeInfo method with summary, parameter descriptions, and return value details. Commit 3998440

return info is not null;
}

public bool TryGetValidatableParameterInfo(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Add an XML comment to this method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added XML documentation to the TryGetValidatableParameterInfo method with summary, parameter descriptions, return value details, and remarks explaining why this implementation always returns false. Commit 3998440

Copilot finished work on behalf of captainsafia July 16, 2025 16:25
@Copilot Copilot AI requested a review from captainsafia July 16, 2025 16:25
// Note: Use name-based comparison for FromServices attribute defined in
// MVC assemblies.
return attributes.Any(attr =>
attr.GetType().Name == "FromServicesAttribute" ||
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we be checking if the attribute implements IFromServiceMetadata instead? That's what we did in #61895

Also, if you inherit from either of the attributes these checks would fail, we should be doing something like:

if (attribute is IFromServiceMetadata ||
    attribute is FromKeyedServicesAttribute)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed to use IFromServiceMetadata interface and handle inheritance properly. Now checks for attr is IFromServiceMetadata || attr is FromKeyedServicesAttribute to support any attributes that implement the interface or inherit from the specific attribute. Commit 05b5152

}

// Get validation attributes applied to the type
var typeValidationAttributes = type
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: looks like we're just checking if there are any attributes, so use .Any() instead of .ToArray()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed to avoid creating the array just for checking existence. Now uses early evaluation with variable hasTypeValidationAttributes to optimize the check. Commit 05b5152

try
{
// Skip types that don't need validation (same logic as parameter resolver)
if (!IsClassForType(type))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Record structs won't be validated?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed to support record structs. Updated IsClassForType to include IsRecordStruct(type) check and added logic to detect record structs by their compiler-generated characteristics. Commit 05b5152

.ToArray();

// For record types, also check constructor parameters for validation attributes
if (propertyValidationAttributes.Length == 0 && IsRecordType(type))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this would skip the record ctor if it also had parameters?

record struct H([Range(1, 10)] int A)
{
    [Required]
    public bool B { get; set; }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the logic to merge both property and constructor validation attributes instead of skipping constructor parameters when properties have attributes. Now both property and constructor validation attributes are collected and combined for record types. Commit 05b5152

var derivedTypes = GetDerivedTypes(type);
foreach (var derivedType in derivedTypes)
{
// Recursively ensure derived types are also cached
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for this? Wouldn't they be added to the cache once they are used?

Also, I don't see how these are being added to the cache. The return value is ignored.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the derived type caching logic. Added a check to avoid unnecessary work if the derived type is already cached (!_cache.ContainsKey(derivedType)) and clarified the comment to explain that caching happens automatically in the called method. Commit 05b5152

…ibute checks, support record structs, and improve validation logic

Co-authored-by: BrennanConroy <[email protected]>
@Copilot Copilot AI requested a review from BrennanConroy July 18, 2025 21:15
Copilot finished work on behalf of BrennanConroy July 18, 2025 21:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Attention: Shared Code Modified pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement runtime-based IValidatableTypeInfoResolver implementation
3 participants