-
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 37 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 |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| namespace Microsoft.AspNetCore.Http.Metadata; | ||
|
|
||
| /// <summary> | ||
| /// A marker interface which can be used to identify metadata that disables validation | ||
| /// on a given endpoint. | ||
| /// </summary> | ||
| public interface IDisableValidationMetadata | ||
| { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,44 @@ | ||
| #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.Metadata.IDisableValidationMetadata | ||
| 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.IValidatableInfo | ||
| Microsoft.AspNetCore.Http.Validation.IValidatableInfo.ValidateAsync(object? value, Microsoft.AspNetCore.Http.Validation.ValidateContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! | ||
| Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver | ||
| Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver.TryGetValidatableParameterInfo(System.Reflection.ParameterInfo! parameterInfo, out Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) -> bool | ||
| Microsoft.AspNetCore.Http.Validation.IValidatableInfoResolver.TryGetValidatableTypeInfo(System.Type! type, out Microsoft.AspNetCore.Http.Validation.IValidatableInfo? validatableInfo) -> bool | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo.ValidatableParameterInfo(System.Type! parameterType, string! name, string! displayName) -> void | ||
| Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo | ||
| Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.ValidatablePropertyInfo(System.Type! declaringType, System.Type! propertyType, string! name, string! displayName) -> void | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableTypeAttribute | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableTypeAttribute.ValidatableTypeAttribute() -> void | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo | ||
| Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo.ValidatableTypeInfo(System.Type! type, System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo!>! members) -> void | ||
| Microsoft.AspNetCore.Http.Validation.ValidateContext | ||
| Microsoft.AspNetCore.Http.Validation.ValidateContext.CurrentDepth.get -> int | ||
| Microsoft.AspNetCore.Http.Validation.ValidateContext.CurrentDepth.set -> void | ||
| Microsoft.AspNetCore.Http.Validation.ValidateContext.CurrentValidationPath.get -> string! | ||
| Microsoft.AspNetCore.Http.Validation.ValidateContext.CurrentValidationPath.set -> void | ||
| Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidateContext() -> void | ||
| Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationContext.get -> System.ComponentModel.DataAnnotations.ValidationContext? | ||
| Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationContext.set -> void | ||
| Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationErrors.get -> System.Collections.Generic.Dictionary<string!, string![]!>? | ||
| Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationErrors.set -> void | ||
| Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationOptions.get -> Microsoft.AspNetCore.Http.Validation.ValidationOptions! | ||
| Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationOptions.set -> 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.IValidatableInfo? validatableInfo) -> bool | ||
| Microsoft.AspNetCore.Http.Validation.ValidationOptions.TryGetValidatableTypeInfo(System.Type! type, out Microsoft.AspNetCore.Http.Validation.IValidatableInfo? 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.ValidateAsync(object? value, Microsoft.AspNetCore.Http.Validation.ValidateContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! | ||
| virtual Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo.ValidateAsync(object? value, Microsoft.AspNetCore.Http.Validation.ValidateContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! | ||
| virtual Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo.ValidateAsync(object? value, Microsoft.AspNetCore.Http.Validation.ValidateContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| namespace Microsoft.AspNetCore.Http.Validation; | ||
|
|
||
| /// <summary> | ||
| /// Represents an interface for validating a value. | ||
| /// </summary> | ||
| public interface IValidatableInfo | ||
| { | ||
| /// <summary> | ||
| /// Validates the specified value. | ||
| /// </summary> | ||
| /// <param name="value">The value to validate.</param> | ||
| /// <param name="context">The validation context.</param> | ||
| /// <param name="cancellationToken">A cancellation token to support cancellation of the validation.</param> | ||
| /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> | ||
| Task ValidateAsync(object? value, ValidateContext context, CancellationToken cancellationToken); | ||
captainsafia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Diagnostics.CodeAnalysis; | ||
| 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 information for the specified type. | ||
| /// </summary> | ||
| /// <param name="type">The type to get validation information for.</param> | ||
| /// <param name="validatableInfo"> | ||
| /// The output parameter that will contain the validatable information if found. | ||
| /// </param> | ||
| /// <returns><see langword="true" /> if the validatable type information was found; otherwise, false.</returns> | ||
| bool TryGetValidatableTypeInfo(Type type, [NotNullWhen(true)] out IValidatableInfo? validatableInfo); | ||
|
|
||
| /// <summary> | ||
| /// Gets validation information for the specified parameter. | ||
| /// </summary> | ||
| /// <param name="parameterInfo">The parameter to get validation information for.</param> | ||
| /// <param name="validatableInfo">The output parameter that will contain the validatable information if found.</param> | ||
| /// <returns><see langword="true" /> if the validatable parameter information was found; otherwise, false.</returns> | ||
| bool TryGetValidatableParameterInfo(ParameterInfo parameterInfo, [NotNullWhen(true)] out IValidatableInfo? validatableInfo); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| // 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.Diagnostics.CodeAnalysis; | ||
| using System.Linq; | ||
| using System.Reflection; | ||
|
|
||
| namespace Microsoft.AspNetCore.Http.Validation; | ||
|
|
||
| internal class RuntimeValidatableParameterInfoResolver : IValidatableInfoResolver | ||
captainsafia marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| public bool TryGetValidatableTypeInfo(Type type, [NotNullWhen(true)] out IValidatableInfo? validatableInfo) | ||
| { | ||
captainsafia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| validatableInfo = null; | ||
| return false; | ||
| } | ||
|
|
||
| public bool TryGetValidatableParameterInfo(ParameterInfo parameterInfo, [NotNullWhen(true)] out IValidatableInfo? validatableInfo) | ||
| { | ||
| 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(); | ||
| validatableInfo = new RuntimeValidatableParameterInfo( | ||
| parameterType: parameterInfo.ParameterType, | ||
| name: parameterInfo.Name, | ||
| displayName: GetDisplayName(parameterInfo), | ||
| validationAttributes: validationAttributes | ||
| ); | ||
| return true; | ||
| } | ||
|
|
||
| private static string GetDisplayName(ParameterInfo parameterInfo) | ||
| { | ||
| var displayAttribute = parameterInfo.GetCustomAttribute<DisplayAttribute>(); | ||
| if (displayAttribute != null) | ||
| { | ||
| return displayAttribute.Name ?? parameterInfo.Name!; | ||
| } | ||
|
|
||
| return parameterInfo.Name!; | ||
| } | ||
|
|
||
| private class RuntimeValidatableParameterInfo( | ||
captainsafia marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Type parameterType, | ||
| string name, | ||
| string displayName, | ||
| ValidationAttribute[] validationAttributes) : | ||
| ValidatableParameterInfo(parameterType, name, displayName) | ||
| { | ||
| protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes; | ||
|
|
||
| private readonly ValidationAttribute[] _validationAttributes = validationAttributes; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| // 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.CodeAnalysis; | ||
|
|
||
| namespace Microsoft.AspNetCore.Http.Validation; | ||
|
|
||
| internal static class TypeExtensions | ||
| { | ||
| /// <summary> | ||
| /// Determines whether the specified type is an enumerable type. | ||
| /// </summary> | ||
| /// <param name="type">The type to check.</param> | ||
| /// <returns><see langword="true"/> if the type is enumerable; otherwise, <see langword="false"/>.</returns> | ||
| public static bool IsEnumerable(this Type type) | ||
|
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. What about AsyncEnumerable?
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. We don't support deserializing AsyncEnumerable with STJ AFAIK, just serializing to the response, so they're not likely to appear in the input for an endpoint and need to be validated. |
||
| { | ||
| // Check if type itself is an IEnumerable | ||
| if (type.IsGenericType && | ||
| (type.GetGenericTypeDefinition() == typeof(IEnumerable<>) || | ||
| type.GetGenericTypeDefinition() == typeof(ICollection<>) || | ||
| type.GetGenericTypeDefinition() == typeof(List<>) || | ||
| type.GetGenericTypeDefinition() == typeof(IList<>))) | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| // Or an array | ||
| if (type.IsArray) | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| // Then evaluate if it implements IEnumerable and is not a string | ||
| if (typeof(IEnumerable).IsAssignableFrom(type) && | ||
| type != typeof(string)) | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Determines whether the specified type is a nullable type. | ||
| /// </summary> | ||
| /// <param name="type">The type to check.</param> | ||
| /// <returns><see langword="true"/> if the type is nullable; otherwise, <see langword="false"/>.</returns> | ||
| public static bool IsNullable(this Type type) | ||
| { | ||
| if (type.IsValueType) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| if (type.IsGenericType && | ||
| type.GetGenericTypeDefinition() == typeof(Nullable<>)) | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Tries to get the <see cref="RequiredAttribute"/> from the specified array of validation attributes. | ||
| /// </summary> | ||
| /// <param name="attributes">The array of <see cref="ValidationAttribute"/> to search.</param> | ||
| /// <param name="requiredAttribute">The found <see cref="RequiredAttribute"/> if available, otherwise null.</param> | ||
| /// <returns><see langword="true"/> if a <see cref="RequiredAttribute"/> is found; otherwise, <see langword="false"/>.</returns> | ||
| public static bool TryGetRequiredAttribute(this ValidationAttribute[] attributes, [NotNullWhen(true)] out RequiredAttribute? requiredAttribute) | ||
| { | ||
| foreach (var attribute in attributes) | ||
| { | ||
| if (attribute is RequiredAttribute requiredAttr) | ||
| { | ||
| requiredAttribute = requiredAttr; | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| requiredAttribute = null; | ||
| return false; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets all types that the specified type implements or inherits from. | ||
| /// </summary> | ||
| /// <param name="type">The type to analyze.</param> | ||
| /// <returns>A collection containing all implemented interfaces and all base types of the given type.</returns> | ||
| public static IEnumerable<Type> GetAllImplementedTypes([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type type) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(type); | ||
|
|
||
| // Yield all interfaces directly and indirectly implemented by this type | ||
| foreach (var interfaceType in type.GetInterfaces()) | ||
| { | ||
| yield return interfaceType; | ||
| } | ||
|
|
||
| // Finally, walk up the inheritance chain | ||
| var baseType = type.BaseType; | ||
| while (baseType != null && baseType != typeof(object)) | ||
| { | ||
| yield return baseType; | ||
| baseType = baseType.BaseType; | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Determines whether the specified type implements the given interface. | ||
| /// </summary> | ||
| /// <param name="type">The type to check.</param> | ||
| /// <param name="interfaceType">The interface type to check for.</param> | ||
| /// <returns>True if the type implements the specified interface; otherwise, false.</returns> | ||
| public static bool ImplementsInterface(this Type type, Type interfaceType) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(type); | ||
| ArgumentNullException.ThrowIfNull(interfaceType); | ||
|
|
||
| // Check if interfaceType is actually an interface | ||
| if (!interfaceType.IsInterface) | ||
| { | ||
| throw new ArgumentException($"Type {interfaceType.FullName} is not an interface.", nameof(interfaceType)); | ||
| } | ||
|
|
||
| return interfaceType.IsAssignableFrom(type); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.