-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Support input validation for minimal APIs via generic resolver model #60724
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
Merged
Merged
Changes from 26 commits
Commits
Show all changes
41 commits
Select commit
Hold shift + click to select a range
96592d5
Add generic implementation for validations source generator
captainsafia 1f4e615
Make validate methods async and fix AoT suppressions
captainsafia 0b77a21
Update emitted code
captainsafia f928939
Remove deadcode
captainsafia 75f3d1b
Clean up API shapes a bit
captainsafia 3a7867a
Fix up RequiredAttribute handling
captainsafia 5e25ec3
Add ValidatableContext and simplify API signature
captainsafia a4bf559
Support ValidationOptions and multiple resolvers
captainsafia c1970e5
Make ValidatableContext a setup entry
captainsafia c6f88cf
Move registration of filter to route handlers
captainsafia 98cad73
Fix async for tests and IValidatableObject
captainsafia d69c466
Add doc comments
captainsafia ec8848f
Add more tests
captainsafia 3196915
Enable PublicAPI analyzers and update public API
captainsafia 18b4939
Add MaxDepth handling
captainsafia 0d91665
Docs tweaks and package generator in shared framework
captainsafia bcc2529
Clean up tests
captainsafia c345a1b
Update for trimming
captainsafia 889fd26
Harden parameter resolution check
captainsafia 5bbd091
Switch to runtime-based resolution for ParameterInfo validations
captainsafia 248e82a
Prune out uneeded types
captainsafia 95b4d5d
Fix up ValidatableParameterInfo signature
captainsafia 0df79fb
Make Validate methods virtual and support CustomValidationAttribute
captainsafia 0195662
Fix up emitted code and use explicit namespaces
captainsafia 0878c62
Fix up suppression for ValidationContext trimming
captainsafia 8fc9d1b
Actually use attribute-based suppression
captainsafia 03a3c06
Fix up suppression for trimming warnings
captainsafia fbe46cf
Benchmarks, more tests, some tweaks
captainsafia 1ff9b04
More tests and add DisableValidationFilter
captainsafia 669443e
Update API and add sample app
captainsafia 4d776c0
Tweak more APIs
captainsafia 7ee3e1a
Tweaks after API review
captainsafia cda8a39
Fix nullability handling in generator and add sample Http file
captainsafia bac0c95
Harden nullability and index checks in generator
captainsafia a976778
Don't generate TypeInfo for invalidatable types
captainsafia 1490f4e
No-op code gen under more cases
captainsafia 120c6ed
Address feedback
captainsafia b03f15a
Address feedback
captainsafia 21c2418
Scrub out InterceptsLocationAttribute lines
captainsafia c063ce5
Exempt more types, add no-op tests
captainsafia 144d56e
Use List<Type> directly for method
captainsafia File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,59 @@ | ||
| #nullable enable | ||
| abstract Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo.GetValidationAttributes() -> System.ComponentModel.DataAnnotations.ValidationAttribute![]! | ||
| abstract Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.GetValidationAttributes() -> System.ComponentModel.DataAnnotations.ValidationAttribute![]! | ||
| Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.Description.get -> string? | ||
| Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.Description.set -> void | ||
| Microsoft.AspNetCore.Http.Metadata.IProducesResponseTypeMetadata.Description.get -> string? | ||
| Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver | ||
| Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver.GetValidatableParameterInfo(System.Reflection.ParameterInfo! parameterInfo) -> Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo? | ||
| Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver.GetValidatableTypeInfo(System.Type! type) -> Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo? | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableContext | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableContext.CurrentDepth.get -> int | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableContext.CurrentDepth.set -> void | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableContext.Prefix.get -> string! | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableContext.Prefix.set -> void | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableContext.ValidatableContext() -> void | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableContext.ValidationContext.get -> System.ComponentModel.DataAnnotations.ValidationContext? | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableContext.ValidationContext.set -> void | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableContext.ValidationErrors.get -> System.Collections.Generic.Dictionary<string!, string![]!>? | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableContext.ValidationErrors.set -> void | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableContext.ValidationOptions.get -> Microsoft.AspNetCore.Http.Validation.ValidationOptions! | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableContext.ValidationOptions.set -> void | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo.DisplayName.get -> string! | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo.IsEnumerable.get -> bool | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo.IsNullable.get -> bool | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo.IsRequired.get -> bool | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo.Name.get -> string! | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo.ParameterType.get -> System.Type! | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo.ValidatableParameterInfo(System.Type! parameterType, string! name, string! displayName, bool isNullable, bool isRequired, bool isEnumerable) -> void | ||
| Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo | ||
| Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.DeclaringType.get -> System.Type! | ||
| Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.DisplayName.get -> string! | ||
| Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.HasValidatableType.get -> bool | ||
| Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.IsEnumerable.get -> bool | ||
| Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.IsNullable.get -> bool | ||
| Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.IsRequired.get -> bool | ||
| Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.Name.get -> string! | ||
| Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.PropertyType.get -> System.Type! | ||
| Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.ValidatablePropertyInfo(System.Type! declaringType, System.Type! propertyType, string! name, string! displayName, bool isEnumerable, bool isNullable, bool isRequired, bool hasValidatableType) -> void | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableTypeAttribute | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableTypeAttribute.ValidatableTypeAttribute() -> void | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo.IsIValidatableObject.get -> bool | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo.Members.get -> System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo!>! | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo.Type.get -> System.Type! | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo.ValidatableSubTypes.get -> System.Collections.Generic.IReadOnlyList<System.Type!>? | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo.ValidatableTypeInfo(System.Type! type, System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo!>! members, bool implementsIValidatableObject, System.Collections.Generic.IReadOnlyList<System.Type!>? validatableSubTypes = null) -> void | ||
| Microsoft.AspNetCore.Http.Validation.ValidationOptions | ||
| Microsoft.AspNetCore.Http.Validation.ValidationOptions.MaxDepth.get -> int | ||
| Microsoft.AspNetCore.Http.Validation.ValidationOptions.MaxDepth.set -> void | ||
| Microsoft.AspNetCore.Http.Validation.ValidationOptions.Resolvers.get -> System.Collections.Generic.IList<Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver!>! | ||
| Microsoft.AspNetCore.Http.Validation.ValidationOptions.TryGetValidatableParameterInfo(System.Reflection.ParameterInfo! parameterInfo, out Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo? validatableParameterInfo) -> bool | ||
| Microsoft.AspNetCore.Http.Validation.ValidationOptions.TryGetValidatableTypeInfo(System.Type! type, out Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo? validatableTypeInfo) -> bool | ||
| Microsoft.AspNetCore.Http.Validation.ValidationOptions.ValidationOptions() -> void | ||
| Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions | ||
| static Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action<Microsoft.AspNetCore.Http.Validation.ValidationOptions!>? configureOptions = null) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! | ||
| virtual Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo.Validate(object? value, Microsoft.AspNetCore.Http.Validation.ValidatableContext! context) -> System.Threading.Tasks.Task! | ||
| virtual Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.Validate(object! obj, Microsoft.AspNetCore.Http.Validation.ValidatableContext! context) -> System.Threading.Tasks.Task! | ||
| virtual Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo.Validate(object? value, Microsoft.AspNetCore.Http.Validation.ValidatableContext! context) -> System.Threading.Tasks.Task! |
27 changes: 27 additions & 0 deletions
27
src/Http/Http.Abstractions/src/Validation/IValidatableInfoResolver.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Reflection; | ||
|
|
||
| namespace Microsoft.AspNetCore.Http.Validation; | ||
|
|
||
| /// <summary> | ||
| /// Provides an interface for resolving the validation information associated | ||
| /// with a given <seealso cref="Type"/> or <seealso cref="ParameterInfo"/>. | ||
| /// </summary> | ||
| public interface IValidatableInfoResolver | ||
| { | ||
| /// <summary> | ||
| /// Gets validation type information for the specified type. | ||
| /// </summary> | ||
| /// <param name="type">The type to get validation information for.</param> | ||
| /// <returns>The validation type information, or null if the type is not validatable.</returns> | ||
| ValidatableTypeInfo? GetValidatableTypeInfo(Type type); | ||
|
|
||
| /// <summary> | ||
| /// Gets validation parameter information for the specified parameter. | ||
| /// </summary> | ||
| /// <param name="parameterInfo">The parameter information to get validation for.</param> | ||
| /// <returns>The validation parameter information, or null if the parameter is not validatable.</returns> | ||
| ValidatableParameterInfo? GetValidatableParameterInfo(ParameterInfo parameterInfo); | ||
| } |
92 changes: 92 additions & 0 deletions
92
src/Http/Http.Abstractions/src/Validation/RuntimeValidatableParameterInfoResolver.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.ComponentModel.DataAnnotations; | ||
| using System.Diagnostics; | ||
| using System.Linq; | ||
| using System.Reflection; | ||
|
|
||
| namespace Microsoft.AspNetCore.Http.Validation; | ||
|
|
||
| internal class RuntimeValidatableParameterInfoResolver : IValidatableInfoResolver | ||
| { | ||
| public ValidatableParameterInfo? GetValidatableParameterInfo(ParameterInfo parameterInfo) | ||
| { | ||
| Debug.Assert(parameterInfo.Name != null, "Parameter must have name"); | ||
BrennanConroy marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| var validationAttributes = parameterInfo | ||
| .GetCustomAttributes<ValidationAttribute>() | ||
| .ToArray(); | ||
| return new RuntimeValidatableParameterInfo( | ||
| parameterType: parameterInfo.ParameterType, | ||
| name: parameterInfo.Name, | ||
| displayName: GetDisplayName(parameterInfo), | ||
| isNullable: IsNullable(parameterInfo), | ||
| isRequired: validationAttributes.Any(a => a is RequiredAttribute), | ||
| isEnumerable: IsEnumerable(parameterInfo), | ||
| validationAttributes: validationAttributes | ||
| ); | ||
| } | ||
|
|
||
| private static bool IsNullable(ParameterInfo parameterInfo) | ||
| { | ||
| if (parameterInfo.ParameterType.IsValueType) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| if (parameterInfo.ParameterType.IsGenericType && | ||
| parameterInfo.ParameterType.GetGenericTypeDefinition() == typeof(Nullable<>)) | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| private static string GetDisplayName(ParameterInfo parameterInfo) | ||
| { | ||
| var displayAttribute = parameterInfo.GetCustomAttribute<DisplayAttribute>(); | ||
| if (displayAttribute != null) | ||
| { | ||
| return displayAttribute.Name ?? parameterInfo.Name!; | ||
| } | ||
|
|
||
| return parameterInfo.Name!; | ||
| } | ||
|
|
||
| private static bool IsEnumerable(ParameterInfo parameterInfo) | ||
| { | ||
| if (parameterInfo.ParameterType.IsGenericType && | ||
| parameterInfo.ParameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| if (parameterInfo.ParameterType.IsArray) | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| public ValidatableTypeInfo? GetValidatableTypeInfo(Type type) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| private class RuntimeValidatableParameterInfo( | ||
captainsafia marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Type parameterType, | ||
| string name, | ||
| string displayName, | ||
| bool isNullable, | ||
| bool isRequired, | ||
| bool isEnumerable, | ||
| ValidationAttribute[] validationAttributes) : | ||
| ValidatableParameterInfo(parameterType, name, displayName, isNullable, isRequired, isEnumerable) | ||
| { | ||
| protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes; | ||
|
|
||
| private readonly ValidationAttribute[] _validationAttributes = validationAttributes; | ||
| } | ||
| } | ||
80 changes: 80 additions & 0 deletions
80
src/Http/Http.Abstractions/src/Validation/ValidatableContext.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.ComponentModel.DataAnnotations; | ||
|
|
||
| namespace Microsoft.AspNetCore.Http.Validation; | ||
|
|
||
| /// <summary> | ||
| /// Represents the context for validating a validatable object. | ||
| /// </summary> | ||
| public sealed class ValidatableContext | ||
| { | ||
| /// <summary> | ||
| /// Gets or sets the validation context used for validating objects that implement <see cref="IValidatableObject"/> or have <see cref="ValidationAttribute"/>. | ||
| /// This context provides access to service provider and other validation metadata. | ||
| /// </summary> | ||
| public ValidationContext? ValidationContext { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the prefix used to identify the current object being validated in a complex object graph. | ||
| /// This is used to build property paths in validation error messages (e.g., "Customer.Address.Street"). | ||
| /// </summary> | ||
| public string Prefix { get; set; } = string.Empty; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the validation options that control validation behavior, | ||
| /// including validation depth limits and resolver registration. | ||
| /// </summary> | ||
| public required ValidationOptions ValidationOptions { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the dictionary of validation errors collected during validation. | ||
| /// Keys are property names or paths, and values are arrays of error messages. | ||
| /// This dictionary is lazily initialized when the first validation error is added. | ||
| /// </summary> | ||
| public Dictionary<string, string[]>? ValidationErrors { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the current depth in the validation hierarchy. | ||
| /// This is used to prevent stack overflows from circular references. | ||
| /// </summary> | ||
| public int CurrentDepth { get; set; } | ||
|
|
||
| internal void AddValidationError(string key, string[] error) | ||
| { | ||
| ValidationErrors ??= []; | ||
|
|
||
captainsafia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ValidationErrors[key] = error; | ||
| } | ||
|
|
||
| internal void AddOrExtendValidationErrors(string key, string[] errors) | ||
| { | ||
| ValidationErrors ??= []; | ||
|
|
||
| if (ValidationErrors.TryGetValue(key, out var existingErrors)) | ||
| { | ||
| ValidationErrors[key] = new string[existingErrors.Length + errors.Length]; | ||
| existingErrors.CopyTo(ValidationErrors[key], 0); | ||
| errors.CopyTo(ValidationErrors[key], existingErrors.Length); | ||
| } | ||
| else | ||
| { | ||
| ValidationErrors[key] = errors; | ||
| } | ||
| } | ||
|
|
||
| internal void AddOrExtendValidationError(string key, string error) | ||
| { | ||
| ValidationErrors ??= []; | ||
|
|
||
| if (ValidationErrors.TryGetValue(key, out var existingErrors) && !existingErrors.Contains(error)) | ||
| { | ||
| ValidationErrors[key] = [.. existingErrors, error]; | ||
| } | ||
| else | ||
| { | ||
| ValidationErrors[key] = [error]; | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.