Skip to content

Commit f31a387

Browse files
Copilotcaptainsafia
andcommitted
Implement RuntimeValidatableTypeInfoResolver with basic functionality
Co-authored-by: captainsafia <1857993+captainsafia@users.noreply.github.com>
1 parent 3afb54b commit f31a387

File tree

2 files changed

+180
-0
lines changed

2 files changed

+180
-0
lines changed
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
#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.
2+
3+
// Licensed to the .NET Foundation under one or more agreements.
4+
// The .NET Foundation licenses this file to you under the MIT license.
5+
6+
using System.Collections.Concurrent;
7+
using System.ComponentModel.DataAnnotations;
8+
using System.Diagnostics.CodeAnalysis;
9+
using System.IO.Pipelines;
10+
using System.Linq;
11+
using System.Reflection;
12+
using System.Security.Claims;
13+
14+
namespace Microsoft.AspNetCore.Http.Validation;
15+
16+
[RequiresUnreferencedCode("Uses unbounded Reflection to inspect property types.")]
17+
internal sealed class RuntimeValidatableTypeInfoResolver : IValidatableInfoResolver
18+
{
19+
private static readonly ConcurrentDictionary<Type, IValidatableInfo?> _cache = new();
20+
21+
public bool TryGetValidatableTypeInfo(Type type, [NotNullWhen(true)] out IValidatableInfo? info)
22+
{
23+
info = _cache.GetOrAdd(type, static type => BuildValidatableTypeInfo(type, new HashSet<Type>()));
24+
return info is not null;
25+
}
26+
27+
// Parameter discovery is handled elsewhere
28+
public bool TryGetValidatableParameterInfo(ParameterInfo parameterInfo, [NotNullWhen(true)] out IValidatableInfo? info)
29+
{
30+
info = null;
31+
return false;
32+
}
33+
34+
private static IValidatableInfo? BuildValidatableTypeInfo([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.Interfaces)] Type type, HashSet<Type> visitedTypes)
35+
{
36+
// Prevent cycles - if we've already seen this type, return null
37+
if (!visitedTypes.Add(type))
38+
{
39+
return null;
40+
}
41+
42+
try
43+
{
44+
// Bail out early if this isn't a validatable class
45+
if (!IsValidatableClass(type))
46+
{
47+
return null;
48+
}
49+
50+
// Get public instance properties
51+
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
52+
.Where(p => p.CanRead)
53+
.ToArray();
54+
55+
var validatableProperties = new List<RuntimeValidatablePropertyInfo>();
56+
57+
foreach (var property in properties)
58+
{
59+
var propertyInfo = BuildValidatablePropertyInfo(property, visitedTypes);
60+
if (propertyInfo is not null)
61+
{
62+
validatableProperties.Add(propertyInfo);
63+
}
64+
}
65+
66+
// Only create type info if there are validatable properties
67+
if (validatableProperties.Count > 0)
68+
{
69+
return new RuntimeValidatableTypeInfo(type, validatableProperties);
70+
}
71+
72+
return null;
73+
}
74+
finally
75+
{
76+
visitedTypes.Remove(type);
77+
}
78+
}
79+
80+
private static RuntimeValidatablePropertyInfo? BuildValidatablePropertyInfo(PropertyInfo property, HashSet<Type> visitedTypes)
81+
{
82+
var validationAttributes = property
83+
.GetCustomAttributes<ValidationAttribute>()
84+
.ToArray();
85+
86+
// Check if the property type itself is validatable (recursive check)
87+
var hasValidatableType = false;
88+
if (IsValidatableClass(property.PropertyType))
89+
{
90+
var nestedTypeInfo = BuildValidatableTypeInfo(property.PropertyType, visitedTypes);
91+
hasValidatableType = nestedTypeInfo is not null;
92+
}
93+
94+
// Only create property info if it has validation attributes or a validatable type
95+
if (validationAttributes.Length > 0 || hasValidatableType)
96+
{
97+
var displayName = GetDisplayName(property);
98+
return new RuntimeValidatablePropertyInfo(
99+
property.DeclaringType!,
100+
property.PropertyType,
101+
property.Name,
102+
displayName,
103+
validationAttributes);
104+
}
105+
106+
return null;
107+
}
108+
109+
private static string GetDisplayName(PropertyInfo property)
110+
{
111+
var displayAttribute = property.GetCustomAttribute<DisplayAttribute>();
112+
if (displayAttribute is not null)
113+
{
114+
return displayAttribute.Name ?? property.Name;
115+
}
116+
117+
return property.Name;
118+
}
119+
120+
private static bool IsValidatableClass(Type type)
121+
{
122+
// Skip primitives, enums, common built-in types, and types that are specially
123+
// handled by RDF/RDG that don't need validation if they don't have attributes
124+
if (type.IsPrimitive ||
125+
type.IsEnum ||
126+
type == typeof(string) ||
127+
type == typeof(decimal) ||
128+
type == typeof(DateTime) ||
129+
type == typeof(DateTimeOffset) ||
130+
type == typeof(TimeOnly) ||
131+
type == typeof(DateOnly) ||
132+
type == typeof(TimeSpan) ||
133+
type == typeof(Guid) ||
134+
type == typeof(IFormFile) ||
135+
type == typeof(IFormFileCollection) ||
136+
type == typeof(IFormCollection) ||
137+
type == typeof(HttpContext) ||
138+
type == typeof(HttpRequest) ||
139+
type == typeof(HttpResponse) ||
140+
type == typeof(ClaimsPrincipal) ||
141+
type == typeof(CancellationToken) ||
142+
type == typeof(Stream) ||
143+
type == typeof(PipeReader))
144+
{
145+
return false;
146+
}
147+
148+
// Check if the underlying type in a nullable is valid
149+
if (Nullable.GetUnderlyingType(type) is { } nullableType)
150+
{
151+
return IsValidatableClass(nullableType);
152+
}
153+
154+
return type.IsClass || type.IsValueType;
155+
}
156+
157+
internal sealed class RuntimeValidatableTypeInfo(
158+
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type type,
159+
IReadOnlyList<RuntimeValidatablePropertyInfo> members) :
160+
ValidatableTypeInfo(type, members.Cast<ValidatablePropertyInfo>().ToArray())
161+
{
162+
}
163+
164+
internal sealed class RuntimeValidatablePropertyInfo(
165+
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type declaringType,
166+
Type propertyType,
167+
string name,
168+
string displayName,
169+
ValidationAttribute[] validationAttributes) :
170+
ValidatablePropertyInfo(declaringType, propertyType, name, displayName)
171+
{
172+
private readonly ValidationAttribute[] _validationAttributes = validationAttributes;
173+
174+
protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes;
175+
}
176+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ public static IServiceCollection AddValidation(this IServiceCollection services,
2727
// Support ParameterInfo resolution at runtime
2828
#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.
2929
options.Resolvers.Add(new RuntimeValidatableParameterInfoResolver());
30+
// Support Type resolution at runtime
31+
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
32+
options.Resolvers.Add(new RuntimeValidatableTypeInfoResolver());
33+
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
3034
#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.
3135
});
3236
return services;

0 commit comments

Comments
 (0)