Skip to content

Implement runtime-based IValidatableTypeInfoResolver implementation #62091

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

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -0,0 +1,176 @@
#pragma warning disable ASP0029 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

// 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.Concurrent;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.IO.Pipelines;
using System.Linq;
using System.Reflection;
using System.Security.Claims;

namespace Microsoft.AspNetCore.Http.Validation;

[RequiresUnreferencedCode("Uses unbounded Reflection to inspect property types.")]
internal sealed class RuntimeValidatableTypeInfoResolver : IValidatableInfoResolver
{
private static readonly ConcurrentDictionary<Type, IValidatableInfo?> _cache = new();

public bool TryGetValidatableTypeInfo(Type type, [NotNullWhen(true)] out IValidatableInfo? info)
{
info = _cache.GetOrAdd(type, static type => BuildValidatableTypeInfo(type, new HashSet<Type>()));
return info is not null;
}

// Parameter discovery is handled elsewhere
public bool TryGetValidatableParameterInfo(ParameterInfo parameterInfo, [NotNullWhen(true)] out IValidatableInfo? info)
{
info = null;
return false;
}

private static IValidatableInfo? BuildValidatableTypeInfo([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.Interfaces)] Type type, HashSet<Type> visitedTypes)
{
// Prevent cycles - if we've already seen this type, return null
if (!visitedTypes.Add(type))
{
return null;
}

try
{
// Bail out early if this isn't a validatable class
if (!IsValidatableClass(type))
{
return null;
}

// Get public instance properties
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead)
.ToArray();

var validatableProperties = new List<RuntimeValidatablePropertyInfo>();

foreach (var property in properties)
{
var propertyInfo = BuildValidatablePropertyInfo(property, visitedTypes);
if (propertyInfo is not null)
{
validatableProperties.Add(propertyInfo);
}
}

// Only create type info if there are validatable properties
if (validatableProperties.Count > 0)
{
return new RuntimeValidatableTypeInfo(type, validatableProperties);
}

return null;
}
finally
{
visitedTypes.Remove(type);
}
}

private static RuntimeValidatablePropertyInfo? BuildValidatablePropertyInfo(PropertyInfo property, HashSet<Type> visitedTypes)
{
var validationAttributes = property
.GetCustomAttributes<ValidationAttribute>()
.ToArray();

// Check if the property type itself is validatable (recursive check)
var hasValidatableType = false;
if (IsValidatableClass(property.PropertyType))
{
var nestedTypeInfo = BuildValidatableTypeInfo(property.PropertyType, visitedTypes);
hasValidatableType = nestedTypeInfo is not null;
}

// Only create property info if it has validation attributes or a validatable type
if (validationAttributes.Length > 0 || hasValidatableType)
{
var displayName = GetDisplayName(property);
return new RuntimeValidatablePropertyInfo(
property.DeclaringType!,
property.PropertyType,
property.Name,
displayName,
validationAttributes);
}

return null;
}

private static string GetDisplayName(PropertyInfo property)
{
var displayAttribute = property.GetCustomAttribute<DisplayAttribute>();
if (displayAttribute is not null)
{
return displayAttribute.Name ?? property.Name;
}

return property.Name;
}

private static bool IsValidatableClass(Type type)
{
// Skip primitives, enums, common built-in types, and types that are specially
// handled by RDF/RDG that don't need validation if they don't have attributes
if (type.IsPrimitive ||
type.IsEnum ||
type == typeof(string) ||
type == typeof(decimal) ||
type == typeof(DateTime) ||
type == typeof(DateTimeOffset) ||
type == typeof(TimeOnly) ||
type == typeof(DateOnly) ||
type == typeof(TimeSpan) ||
type == typeof(Guid) ||
type == typeof(IFormFile) ||
type == typeof(IFormFileCollection) ||
type == typeof(IFormCollection) ||
type == typeof(HttpContext) ||
type == typeof(HttpRequest) ||
type == typeof(HttpResponse) ||
type == typeof(ClaimsPrincipal) ||
type == typeof(CancellationToken) ||
type == typeof(Stream) ||
type == typeof(PipeReader))
{
return false;
}

// Check if the underlying type in a nullable is valid
if (Nullable.GetUnderlyingType(type) is { } nullableType)
{
return IsValidatableClass(nullableType);
}

return type.IsClass || type.IsValueType;
}

internal sealed class RuntimeValidatableTypeInfo(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type type,
IReadOnlyList<RuntimeValidatablePropertyInfo> members) :
ValidatableTypeInfo(type, members.Cast<ValidatablePropertyInfo>().ToArray())
{
}

internal sealed class RuntimeValidatablePropertyInfo(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type declaringType,
Type propertyType,
string name,
string displayName,
ValidationAttribute[] validationAttributes) :
ValidatablePropertyInfo(declaringType, propertyType, name, displayName)
{
private readonly ValidationAttribute[] _validationAttributes = validationAttributes;

protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
// Support ParameterInfo resolution at runtime
#pragma warning disable ASP0029 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
options.Resolvers.Add(new RuntimeValidatableParameterInfoResolver());
// Support Type resolution at runtime
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
options.Resolvers.Add(new RuntimeValidatableTypeInfoResolver());

Check failure on line 32 in src/Http/Http.Abstractions/src/Validation/ValidationServiceCollectionExtensions.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Test: Ubuntu x64)

src/Http/Http.Abstractions/src/Validation/ValidationServiceCollectionExtensions.cs#L32

src/Http/Http.Abstractions/src/Validation/ValidationServiceCollectionExtensions.cs(32,13): error IL2026: (NETCORE_ENGINEERING_TELEMETRY=Build) Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.<>c__DisplayClass0_0.<AddValidation>b__0(ValidationOptions): Using member 'Microsoft.AspNetCore.Http.Validation.RuntimeValidatableTypeInfoResolver.RuntimeValidatableTypeInfoResolver()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Uses unbounded Reflection to inspect property types.

Check failure on line 32 in src/Http/Http.Abstractions/src/Validation/ValidationServiceCollectionExtensions.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci

src/Http/Http.Abstractions/src/Validation/ValidationServiceCollectionExtensions.cs#L32

src/Http/Http.Abstractions/src/Validation/ValidationServiceCollectionExtensions.cs(32,13): error IL2026: (NETCORE_ENGINEERING_TELEMETRY=Build) Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.<>c__DisplayClass0_0.<AddValidation>b__0(ValidationOptions): Using member 'Microsoft.AspNetCore.Http.Validation.RuntimeValidatableTypeInfoResolver.RuntimeValidatableTypeInfoResolver()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Uses unbounded Reflection to inspect property types.
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
#pragma warning restore ASP0029 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
});
return services;
Expand Down
Loading
Loading