Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 Feb 27, 2025
1f4e615
Make validate methods async and fix AoT suppressions
captainsafia Feb 27, 2025
0b77a21
Update emitted code
captainsafia Feb 27, 2025
f928939
Remove deadcode
captainsafia Feb 28, 2025
75f3d1b
Clean up API shapes a bit
captainsafia Feb 28, 2025
3a7867a
Fix up RequiredAttribute handling
captainsafia Feb 28, 2025
5e25ec3
Add ValidatableContext and simplify API signature
captainsafia Feb 28, 2025
a4bf559
Support ValidationOptions and multiple resolvers
captainsafia Feb 28, 2025
c1970e5
Make ValidatableContext a setup entry
captainsafia Feb 28, 2025
c6f88cf
Move registration of filter to route handlers
captainsafia Mar 3, 2025
98cad73
Fix async for tests and IValidatableObject
captainsafia Mar 3, 2025
d69c466
Add doc comments
captainsafia Mar 3, 2025
ec8848f
Add more tests
captainsafia Mar 3, 2025
3196915
Enable PublicAPI analyzers and update public API
captainsafia Mar 3, 2025
18b4939
Add MaxDepth handling
captainsafia Mar 3, 2025
0d91665
Docs tweaks and package generator in shared framework
captainsafia Mar 3, 2025
bcc2529
Clean up tests
captainsafia Mar 3, 2025
c345a1b
Update for trimming
captainsafia Mar 3, 2025
889fd26
Harden parameter resolution check
captainsafia Mar 4, 2025
5bbd091
Switch to runtime-based resolution for ParameterInfo validations
captainsafia Mar 4, 2025
248e82a
Prune out uneeded types
captainsafia Mar 4, 2025
95b4d5d
Fix up ValidatableParameterInfo signature
captainsafia Mar 4, 2025
0df79fb
Make Validate methods virtual and support CustomValidationAttribute
captainsafia Mar 4, 2025
0195662
Fix up emitted code and use explicit namespaces
captainsafia Mar 5, 2025
0878c62
Fix up suppression for ValidationContext trimming
captainsafia Mar 5, 2025
8fc9d1b
Actually use attribute-based suppression
captainsafia Mar 5, 2025
03a3c06
Fix up suppression for trimming warnings
captainsafia Mar 5, 2025
fbe46cf
Benchmarks, more tests, some tweaks
captainsafia Mar 6, 2025
1ff9b04
More tests and add DisableValidationFilter
captainsafia Mar 6, 2025
669443e
Update API and add sample app
captainsafia Mar 9, 2025
4d776c0
Tweak more APIs
captainsafia Mar 9, 2025
7ee3e1a
Tweaks after API review
captainsafia Mar 10, 2025
cda8a39
Fix nullability handling in generator and add sample Http file
captainsafia Mar 10, 2025
bac0c95
Harden nullability and index checks in generator
captainsafia Mar 11, 2025
a976778
Don't generate TypeInfo for invalidatable types
captainsafia Mar 11, 2025
1490f4e
No-op code gen under more cases
captainsafia Mar 11, 2025
120c6ed
Address feedback
captainsafia Mar 11, 2025
b03f15a
Address feedback
captainsafia Mar 12, 2025
21c2418
Scrub out InterceptsLocationAttribute lines
captainsafia Mar 12, 2025
c063ce5
Exempt more types, add no-op tests
captainsafia Mar 12, 2025
144d56e
Use List<Type> directly for method
captainsafia Mar 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@
Private="false"
OutputItemType="AspNetCoreAnalyzer"
ReferenceOutputAssembly="false" />

<ProjectReference Include="$(RepoRoot)src\Http\Http.Extensions\gen\Microsoft.AspNetCore.Http.ValidationsGenerator\Microsoft.AspNetCore.Http.ValidationsGenerator.csproj"
Private="false"
OutputItemType="AspNetCoreAnalyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>

<ItemGroup>
Expand Down
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
{
}
40 changes: 40 additions & 0 deletions src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
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!
19 changes: 19 additions & 0 deletions src/Http/Http.Abstractions/src/Validation/IValidatableInfo.cs
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 <paramref name="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);
}
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,61 @@
// 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.CodeAnalysis;
using System.Linq;
using System.Reflection;

namespace Microsoft.AspNetCore.Http.Validation;

internal sealed class RuntimeValidatableParameterInfoResolver : IValidatableInfoResolver
{
// TODO: the implementation currently relies on static discovery of types.
public bool TryGetValidatableTypeInfo(Type type, [NotNullWhen(true)] out IValidatableInfo? validatableInfo)
{
validatableInfo = null;
return false;
}

public bool TryGetValidatableParameterInfo(ParameterInfo parameterInfo, [NotNullWhen(true)] out IValidatableInfo? validatableInfo)
{
if (parameterInfo.Name == null)
{
throw new InvalidOperationException($"Encountered a parameter of type '{parameterInfo.ParameterType}' without a name. Parameters must have a name.");
}

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 sealed class RuntimeValidatableParameterInfo(
Type parameterType,
string name,
string displayName,
ValidationAttribute[] validationAttributes) :
ValidatableParameterInfo(parameterType, name, displayName)
{
protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes;

private readonly ValidationAttribute[] _validationAttributes = validationAttributes;
}
}
134 changes: 134 additions & 0 deletions src/Http/Http.Abstractions/src/Validation/TypeExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// 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)
Copy link
Member

Choose a reason for hiding this comment

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

What about AsyncEnumerable?

Copy link
Member Author

Choose a reason for hiding this comment

The 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 List<Type> GetAllImplementedTypes([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type type)
{
ArgumentNullException.ThrowIfNull(type);

var implementedTypes = new List<Type>();

// Yield all interfaces directly and indirectly implemented by this type
foreach (var interfaceType in type.GetInterfaces())
{
implementedTypes.Add(interfaceType);
}

// Finally, walk up the inheritance chain
var baseType = type.BaseType;
while (baseType != null && baseType != typeof(object))
{
implementedTypes.Add(baseType);
baseType = baseType.BaseType;
}

return implementedTypes;
}

/// <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);
}
}
Loading
Loading