-
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
Changes from 17 commits
96592d5
1f4e615
0b77a21
f928939
75f3d1b
3a7867a
5e25ec3
a4bf559
c1970e5
c6f88cf
98cad73
d69c466
ec8848f
3196915
18b4939
0d91665
bcc2529
c345a1b
889fd26
5bbd091
248e82a
95b4d5d
0df79fb
0195662
0878c62
8fc9d1b
03a3c06
fbe46cf
1ff9b04
669443e
4d776c0
7ee3e1a
cda8a39
bac0c95
a976778
1490f4e
120c6ed
b03f15a
21c2418
c063ce5
144d56e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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.HasValidatableType.get -> bool | ||
| 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.ValidatableParameterInfo(string! name, string! displayName, bool isNullable, bool isRequired, bool hasValidatableType, bool isEnumerable) -> void | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo.Validate(object? value, Microsoft.AspNetCore.Http.Validation.ValidatableContext! context) -> System.Threading.Tasks.Task! | ||
| 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.ValidatablePropertyInfo.Validate(object! obj, Microsoft.AspNetCore.Http.Validation.ValidatableContext! context) -> System.Threading.Tasks.Task! | ||
| 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.ValidatableTypeInfo.Validate(object? value, Microsoft.AspNetCore.Http.Validation.ValidatableContext! context) -> System.Threading.Tasks.Task! | ||
| 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! |
| 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); | ||
| } |
| 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 ??= []; | ||
|
|
||
| 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]; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Collections; | ||
| using System.ComponentModel.DataAnnotations; | ||
| using System.Diagnostics; | ||
| using System.Linq; | ||
|
|
||
| namespace Microsoft.AspNetCore.Http.Validation; | ||
|
|
||
| /// <summary> | ||
| /// Contains validation information for a parameter. | ||
| /// </summary> | ||
| public abstract class ValidatableParameterInfo | ||
| { | ||
| /// <summary> | ||
| /// Creates a new instance of <see cref="ValidatableParameterInfo"/>. | ||
| /// </summary> | ||
| /// <param name="name">The parameter name.</param> | ||
| /// <param name="displayName">The display name for the parameter.</param> | ||
| /// <param name="isNullable">Whether the parameter is optional.</param> | ||
| /// <param name="isRequired"></param> | ||
| /// <param name="hasValidatableType">Whether the parameter type is validatable.</param> | ||
| /// <param name="isEnumerable">Whether the parameter is enumerable.</param> | ||
| public ValidatableParameterInfo( | ||
| string name, | ||
| string displayName, | ||
| bool isNullable, | ||
| bool isRequired, | ||
| bool hasValidatableType, | ||
| bool isEnumerable) | ||
| { | ||
| Name = name; | ||
| DisplayName = displayName; | ||
| IsNullable = isNullable; | ||
| IsRequired = isRequired; | ||
| HasValidatableType = hasValidatableType; | ||
| IsEnumerable = isEnumerable; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the parameter name. | ||
| /// </summary> | ||
| public string Name { get; } | ||
|
|
||
| /// <summary> | ||
| /// Gets the display name for the parameter. | ||
| /// </summary> | ||
| public string DisplayName { get; } | ||
|
|
||
| /// <summary> | ||
| /// Gets whether the parameter is optional. | ||
| /// </summary> | ||
| public bool IsNullable { get; } | ||
|
|
||
| /// <summary> | ||
| /// Gets whether the parameter is annotated with the <see cref="RequiredAttribute"/>. | ||
| /// </summary> | ||
| public bool IsRequired { get; } | ||
|
|
||
| /// <summary> | ||
| /// Gets whether the parameter type is validatable. | ||
| /// </summary> | ||
| public bool HasValidatableType { get; } | ||
|
|
||
| /// <summary> | ||
| /// Gets whether the parameter is enumerable. | ||
| /// </summary> | ||
| public bool IsEnumerable { get; } | ||
|
|
||
| /// <summary> | ||
| /// Gets the validation attributes for this parameter. | ||
| /// </summary> | ||
| /// <returns>An array of validation attributes to apply to this parameter.</returns> | ||
| protected abstract ValidationAttribute[] GetValidationAttributes(); | ||
|
|
||
| /// <summary> | ||
| /// Validates the parameter value. | ||
| /// </summary> | ||
| /// <param name="value">The value to validate.</param> | ||
| /// <param name="context">The context for the validation.</param> | ||
| /// <returns>A task representing the asynchronous operation.</returns> | ||
| /// <remarks> | ||
| /// If the parameter is a collection, each item in the collection will be validated. | ||
| /// If the parameter is not a collection but has a validatable type, the single value will be validated. | ||
| /// </remarks> | ||
| public Task Validate(object? value, ValidatableContext context) | ||
| { | ||
| Debug.Assert(context.ValidationContext is not null); | ||
|
|
||
| // Skip validation if value is null and parameter is optional | ||
| if (value == null && IsNullable && !IsRequired) | ||
| { | ||
| return Task.CompletedTask; | ||
| } | ||
|
|
||
| context.ValidationContext.DisplayName = DisplayName; | ||
| context.ValidationContext.MemberName = Name; | ||
|
|
||
| var validationAttributes = GetValidationAttributes(); | ||
|
|
||
| if (IsRequired && validationAttributes.OfType<RequiredAttribute>().SingleOrDefault() is { } requiredAttribute) | ||
| { | ||
| var result = requiredAttribute.GetValidationResult(value, context.ValidationContext); | ||
|
|
||
| if (result is not null && result != ValidationResult.Success) | ||
| { | ||
| var key = string.IsNullOrEmpty(context.Prefix) ? Name : $"{context.Prefix}.{Name}"; | ||
| context.AddValidationError(key, [result.ErrorMessage!]); | ||
captainsafia marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
captainsafia marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return Task.CompletedTask; | ||
| } | ||
| } | ||
|
|
||
| // Validate against validation attributes | ||
| foreach (var attribute in validationAttributes) | ||
| { | ||
| try | ||
| { | ||
| var result = attribute.GetValidationResult(value, context.ValidationContext); | ||
| if (result is not null && result != ValidationResult.Success) | ||
| { | ||
| var key = string.IsNullOrEmpty(context.Prefix) ? Name : $"{context.Prefix}.{Name}"; | ||
| context.AddOrExtendValidationErrors(key, [result!.ErrorMessage!]); | ||
| } | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| var key = string.IsNullOrEmpty(context.Prefix) ? Name : $"{context.Prefix}.{Name}"; | ||
| context.AddValidationError(key, [ex.Message]); | ||
| } | ||
| } | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Increment CurrentDepth?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need to increment the current depth here since we're in the |
||
| // If the parameter is a collection, validate each item | ||
| if (IsEnumerable && value is IEnumerable enumerable && HasValidatableType) | ||
| { | ||
| var index = 0; | ||
| foreach (var item in enumerable) | ||
| { | ||
| if (item != null) | ||
| { | ||
| var itemPrefix = string.IsNullOrEmpty(context.Prefix) | ||
| ? $"{Name}[{index}]" | ||
| : $"{context.Prefix}.{Name}[{index}]"; | ||
|
|
||
| if (context.ValidationOptions.TryGetValidatableTypeInfo(item.GetType(), out var validatableType)) | ||
| { | ||
| validatableType.Validate(item, context); | ||
| } | ||
| } | ||
| index++; | ||
| } | ||
| } | ||
| // If not enumerable but has a validatable type, validate the single value | ||
| else if (HasValidatableType && value != null) | ||
| { | ||
| var valueType = value.GetType(); | ||
| if (context.ValidationOptions.TryGetValidatableTypeInfo(valueType, out var validatableType)) | ||
| { | ||
| validatableType.Validate(value, context); | ||
| } | ||
| } | ||
|
|
||
| return Task.CompletedTask; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.