Skip to content

Commit 96751ed

Browse files
Copilotcaptainsafia
andcommitted
Split RuntimeValidatableInfoResolver into separate parameter and type resolvers, mark type resolver as experimental, revert ValidationServiceCollectionExtensions changes
Co-authored-by: captainsafia <[email protected]>
1 parent ced54c5 commit 96751ed

6 files changed

+156
-137
lines changed

src/Validation/src/PublicAPI.Unshipped.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ Microsoft.Extensions.Validation.ValidationOptions.ValidationOptions() -> void
4646
abstract Microsoft.Extensions.Validation.ValidatableParameterInfo.GetValidationAttributes() -> System.ComponentModel.DataAnnotations.ValidationAttribute![]!
4747
abstract Microsoft.Extensions.Validation.ValidatablePropertyInfo.GetValidationAttributes() -> System.ComponentModel.DataAnnotations.ValidationAttribute![]!
4848
static Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidation(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action<Microsoft.Extensions.Validation.ValidationOptions!>? configureOptions = null) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
49-
static Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions.AddValidationCore(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action<Microsoft.Extensions.Validation.ValidationOptions!>? configureOptions = null) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
5049
virtual Microsoft.Extensions.Validation.ValidatableParameterInfo.ValidateAsync(object? value, Microsoft.Extensions.Validation.ValidateContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
5150
virtual Microsoft.Extensions.Validation.ValidatablePropertyInfo.ValidateAsync(object? value, Microsoft.Extensions.Validation.ValidateContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
5251
virtual Microsoft.Extensions.Validation.ValidatableTypeInfo.ValidateAsync(object? value, Microsoft.Extensions.Validation.ValidateContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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.ComponentModel.DataAnnotations;
7+
using System.Diagnostics.CodeAnalysis;
8+
using System.IO.Pipelines;
9+
using System.Linq;
10+
using System.Reflection;
11+
using System.Security.Claims;
12+
using Microsoft.Extensions.DependencyInjection;
13+
14+
namespace Microsoft.Extensions.Validation;
15+
16+
internal sealed class RuntimeValidatableParameterInfoResolver : IValidatableInfoResolver
17+
{
18+
// TODO: the implementation currently relies on static discovery of types.
19+
public bool TryGetValidatableTypeInfo(Type type, [NotNullWhen(true)] out IValidatableInfo? validatableInfo)
20+
{
21+
validatableInfo = null;
22+
return false;
23+
}
24+
25+
public bool TryGetValidatableParameterInfo(ParameterInfo parameterInfo, [NotNullWhen(true)] out IValidatableInfo? validatableInfo)
26+
{
27+
if (parameterInfo.Name == null)
28+
{
29+
throw new InvalidOperationException($"Encountered a parameter of type '{parameterInfo.ParameterType}' without a name. Parameters must have a name.");
30+
}
31+
32+
// Skip parameters marked with [FromService] or [FromKeyedService] attributes
33+
if (HasFromServiceAttributes(parameterInfo.GetCustomAttributes()))
34+
{
35+
validatableInfo = null;
36+
return false;
37+
}
38+
39+
var validationAttributes = parameterInfo
40+
.GetCustomAttributes<ValidationAttribute>()
41+
.ToArray();
42+
43+
// If there are no validation attributes and this type is not a complex type
44+
// we don't need to validate it. Complex types without attributes are still
45+
// validatable because we want to run the validations on the properties.
46+
if (validationAttributes.Length == 0 && !IsClass(parameterInfo.ParameterType))
47+
{
48+
validatableInfo = null;
49+
return false;
50+
}
51+
validatableInfo = new RuntimeValidatableParameterInfo(
52+
parameterType: parameterInfo.ParameterType,
53+
name: parameterInfo.Name,
54+
displayName: GetDisplayName(parameterInfo),
55+
validationAttributes: validationAttributes
56+
);
57+
return true;
58+
}
59+
60+
private static string GetDisplayName(ParameterInfo parameterInfo)
61+
{
62+
var displayAttribute = parameterInfo.GetCustomAttribute<DisplayAttribute>();
63+
if (displayAttribute != null)
64+
{
65+
return displayAttribute.Name ?? parameterInfo.Name!;
66+
}
67+
68+
return parameterInfo.Name!;
69+
}
70+
71+
private static bool IsClass(Type type)
72+
{
73+
// Skip primitives, enums, common built-in types, and types that are specially
74+
// handled by RDF/RDG that don't need validation if they don't have attributes
75+
if (type.IsPrimitive ||
76+
type.IsEnum ||
77+
type == typeof(string) ||
78+
type == typeof(decimal) ||
79+
type == typeof(DateTime) ||
80+
type == typeof(DateTimeOffset) ||
81+
type == typeof(TimeOnly) ||
82+
type == typeof(DateOnly) ||
83+
type == typeof(TimeSpan) ||
84+
type == typeof(Guid) ||
85+
type == typeof(ClaimsPrincipal) ||
86+
type == typeof(CancellationToken) ||
87+
type == typeof(Stream) ||
88+
type == typeof(PipeReader))
89+
{
90+
return false;
91+
}
92+
93+
// Check if the underlying type in a nullable is valid
94+
if (Nullable.GetUnderlyingType(type) is { } nullableType)
95+
{
96+
return IsClass(nullableType);
97+
}
98+
99+
return type.IsClass;
100+
}
101+
102+
private static bool HasFromServiceAttributes(IEnumerable<Attribute> attributes)
103+
{
104+
// Note: Use name-based comparison for FromServices attribute defined in
105+
// MVC assemblies.
106+
return attributes.Any(attr =>
107+
attr.GetType().Name == "FromServicesAttribute" ||
108+
attr.GetType() == typeof(FromKeyedServicesAttribute));
109+
}
110+
111+
internal sealed class RuntimeValidatableParameterInfo(
112+
Type parameterType,
113+
string name,
114+
string displayName,
115+
ValidationAttribute[] validationAttributes) :
116+
ValidatableParameterInfo(parameterType, name, displayName)
117+
{
118+
protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes;
119+
120+
private readonly ValidationAttribute[] _validationAttributes = validationAttributes;
121+
}
122+
}

src/Validation/src/RuntimeValidatableInfoResolver.cs renamed to src/Validation/src/RuntimeValidatableTypeInfoResolver.cs

Lines changed: 22 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -5,70 +5,48 @@
55

66
using System.Collections.Concurrent;
77
using System.ComponentModel.DataAnnotations;
8-
using System.Diagnostics;
98
using System.Diagnostics.CodeAnalysis;
10-
using System.IO.Pipelines;
119
using System.Linq;
1210
using System.Reflection;
13-
using System.Security.Claims;
1411
using System.Text.Json.Serialization;
1512
using Microsoft.Extensions.DependencyInjection;
1613

1714
namespace Microsoft.Extensions.Validation;
1815

19-
[RequiresUnreferencedCode("RuntimeValidatableInfoResolver uses reflection to inspect types, properties, and attributes at runtime, including JsonDerivedTypeAttribute and record constructors. Trimming or AOT compilation may remove members required for validation.")]
20-
internal sealed class RuntimeValidatableInfoResolver : IValidatableInfoResolver
16+
/// <summary>
17+
/// Experimental runtime implementation of <see cref="IValidatableInfoResolver"/> for type validation.
18+
/// </summary>
19+
/// <remarks>
20+
/// This is an experimental API and may change in future versions.
21+
/// </remarks>
22+
[RequiresUnreferencedCode("RuntimeValidatableTypeInfoResolver uses reflection to inspect types, properties, and attributes at runtime, including JsonDerivedTypeAttribute and record constructors. Trimming or AOT compilation may remove members required for validation.")]
23+
[Experimental("ASP0029")]
24+
internal sealed class RuntimeValidatableTypeInfoResolver : IValidatableInfoResolver
2125
{
22-
private static readonly ConcurrentDictionary<Type, IValidatableInfo?> _typeCache = new();
26+
private static readonly ConcurrentDictionary<Type, IValidatableInfo?> _cache = new();
2327

24-
public bool TryGetValidatableTypeInfo(Type type, [NotNullWhen(true)] out IValidatableInfo? info)
28+
public bool TryGetValidatableTypeInfo(
29+
Type type,
30+
[NotNullWhen(true)] out IValidatableInfo? info)
2531
{
26-
if (_typeCache.TryGetValue(type, out info))
32+
if (_cache.TryGetValue(type, out info))
2733
{
2834
return info is not null;
2935
}
3036

3137
info = CreateValidatableTypeInfo(type, new HashSet<Type>());
32-
_typeCache.TryAdd(type, info);
38+
_cache.TryAdd(type, info);
3339
return info is not null;
3440
}
3541

3642
public bool TryGetValidatableParameterInfo(
3743
ParameterInfo parameterInfo,
3844
[NotNullWhen(true)] out IValidatableInfo? validatableInfo)
3945
{
40-
if (parameterInfo.Name == null)
41-
{
42-
throw new InvalidOperationException($"Encountered a parameter of type '{parameterInfo.ParameterType}' without a name. Parameters must have a name.");
43-
}
44-
45-
// Skip parameters marked with [FromService] or [FromKeyedService] attributes
46-
if (HasFromServiceAttributes(parameterInfo.GetCustomAttributes()))
47-
{
48-
validatableInfo = null;
49-
return false;
50-
}
51-
52-
var validationAttributes = parameterInfo
53-
.GetCustomAttributes<ValidationAttribute>()
54-
.ToArray();
55-
56-
// If there are no validation attributes and this type is not a complex type
57-
// we don't need to validate it. Complex types without attributes are still
58-
// validatable because we want to run the validations on the properties.
59-
if (validationAttributes.Length == 0 && !IsClassForParameter(parameterInfo.ParameterType))
60-
{
61-
validatableInfo = null;
62-
return false;
63-
}
64-
validatableInfo = new RuntimeValidatableParameterInfo(
65-
parameterType: parameterInfo.ParameterType,
66-
name: parameterInfo.Name,
67-
displayName: GetDisplayNameForParameter(parameterInfo),
68-
validationAttributes: validationAttributes
69-
);
70-
return true;
46+
validatableInfo = null;
47+
return false;
7148
}
49+
7250
private static RuntimeValidatableTypeInfo? CreateValidatableTypeInfo(Type type, HashSet<Type> visitedTypes)
7351
{
7452
// Prevent infinite recursion by tracking visited types
@@ -205,17 +183,6 @@ private static string GetDisplayNameForProperty(PropertyInfo property)
205183
return property.Name;
206184
}
207185

208-
private static string GetDisplayNameForParameter(ParameterInfo parameterInfo)
209-
{
210-
Debug.Assert(parameterInfo.Name != null, "ParameterInfo.Name should not be null.");
211-
var displayAttribute = parameterInfo.GetCustomAttribute<DisplayAttribute>();
212-
if (displayAttribute != null)
213-
{
214-
return displayAttribute.Name ?? parameterInfo.Name;
215-
}
216-
217-
return parameterInfo.Name;
218-
}
219186
private static string? GetDisplayNameFromConstructorParameter(Type type, string propertyName)
220187
{
221188
var constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
@@ -240,6 +207,7 @@ private static string GetDisplayNameForParameter(ParameterInfo parameterInfo)
240207

241208
return null;
242209
}
210+
243211
private static bool IsRecordType(Type type)
244212
{
245213
// Check if the type is a record by looking for specific record-related compiler-generated methods
@@ -313,6 +281,7 @@ private static Type UnwrapType(Type type)
313281

314282
return type;
315283
}
284+
316285
private static bool IsParsableType(Type type)
317286
{
318287
var unwrappedType = UnwrapType(type);
@@ -377,37 +346,6 @@ private static bool IsParsableType(Type type)
377346
private static bool IsClassForType(Type type)
378347
=> !IsParsableType(type) && type.IsClass;
379348

380-
private static bool IsClassForParameter(Type type)
381-
{
382-
// Skip primitives, enums, common built-in types, and types that are specially
383-
// handled by RDF/RDG that don't need validation if they don't have attributes
384-
if (type.IsPrimitive ||
385-
type.IsEnum ||
386-
type == typeof(string) ||
387-
type == typeof(decimal) ||
388-
type == typeof(DateTime) ||
389-
type == typeof(DateTimeOffset) ||
390-
type == typeof(TimeOnly) ||
391-
type == typeof(DateOnly) ||
392-
type == typeof(TimeSpan) ||
393-
type == typeof(Guid) ||
394-
type == typeof(ClaimsPrincipal) ||
395-
type == typeof(CancellationToken) ||
396-
type == typeof(Stream) ||
397-
type == typeof(PipeReader))
398-
{
399-
return false;
400-
}
401-
402-
// Check if the underlying type in a nullable is valid
403-
if (Nullable.GetUnderlyingType(type) is { } nullableType)
404-
{
405-
return IsClassForParameter(nullableType);
406-
}
407-
408-
return type.IsClass;
409-
}
410-
411349
private static bool HasFromServiceAttributes(IEnumerable<Attribute> attributes)
412350
{
413351
// Note: Use name-based comparison for FromServices attribute defined in
@@ -417,18 +355,6 @@ private static bool HasFromServiceAttributes(IEnumerable<Attribute> attributes)
417355
attr.GetType() == typeof(FromKeyedServicesAttribute));
418356
}
419357

420-
internal sealed class RuntimeValidatableParameterInfo(
421-
Type parameterType,
422-
string name,
423-
string displayName,
424-
ValidationAttribute[] validationAttributes) :
425-
ValidatableParameterInfo(parameterType, name, displayName)
426-
{
427-
protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes;
428-
429-
private readonly ValidationAttribute[] _validationAttributes = validationAttributes;
430-
}
431-
432358
internal sealed class RuntimeValidatablePropertyInfo([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type declaringType,
433359
Type propertyType,
434360
string name,
@@ -440,9 +366,10 @@ internal sealed class RuntimeValidatablePropertyInfo([DynamicallyAccessedMembers
440366

441367
protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes;
442368
}
369+
443370
internal sealed class RuntimeValidatableTypeInfo(
444371
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type type,
445372
IReadOnlyList<RuntimeValidatablePropertyInfo> members) :
446373
ValidatableTypeInfo(type, [.. members])
447374
{ }
448-
}
375+
}
Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Diagnostics.CodeAnalysis;
54
using Microsoft.Extensions.Validation;
65

76
namespace Microsoft.Extensions.DependencyInjection;
@@ -17,12 +16,6 @@ public static class ValidationServiceCollectionExtensions
1716
/// <param name="services">The <see cref="IServiceCollection" /> to add the services to.</param>
1817
/// <param name="configureOptions">An optional action to configure the <see cref="ValidationOptions"/>.</param>
1918
/// <returns>The <see cref="IServiceCollection" /> for chaining.</returns>
20-
/// <remarks>
21-
/// This API enables both the source-generated and runtime-based implementation of the built-in validation resolver.
22-
/// It is not recommended for use in applications where native AoT compat is required. In those
23-
/// scenarios, it is recommend to use <see cref="ValidationServiceCollectionExtensions.AddValidationCore(IServiceCollection, Action{ValidationOptions}?)" />.
24-
/// </remarks>
25-
[RequiresUnreferencedCode("AddValidation enables the RuntimeValidatableInfoResolver by default which is not compatible with trimming or AOT compilation.")]
2619
public static IServiceCollection AddValidation(this IServiceCollection services, Action<ValidationOptions>? configureOptions = null)
2720
{
2821
services.Configure<ValidationOptions>(options =>
@@ -31,33 +24,11 @@ public static IServiceCollection AddValidation(this IServiceCollection services,
3124
{
3225
configureOptions(options);
3326
}
34-
// Support both ParameterInfo and TypeInfo resolution at runtime
27+
// Support ParameterInfo resolution at runtime
3528
#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.
36-
options.Resolvers.Add(new RuntimeValidatableInfoResolver());
29+
options.Resolvers.Add(new RuntimeValidatableParameterInfoResolver());
3730
#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.
3831
});
3932
return services;
4033
}
41-
42-
/// <summary>
43-
/// Adds the validation services to the specified <see cref="IServiceCollection" />.
44-
/// </summary>
45-
/// <param name="services">The <see cref="IServiceCollection" /> to add the services to.</param>
46-
/// <param name="configureOptions">An optional action to configure the <see cref="ValidationOptions"/>.</param>
47-
/// <returns>The <see cref="IServiceCollection" /> for chaining.</returns>
48-
/// <remarks>
49-
/// This API only enables the source generator-based implementation of the built-in validation resolver, by default
50-
/// and is recommended for use in applications where native AoT compat is required.
51-
/// </remarks>
52-
public static IServiceCollection AddValidationCore(this IServiceCollection services, Action<ValidationOptions>? configureOptions = null)
53-
{
54-
services.Configure<ValidationOptions>(options =>
55-
{
56-
if (configureOptions is not null)
57-
{
58-
configureOptions(options);
59-
}
60-
});
61-
return services;
62-
}
6334
}

0 commit comments

Comments
 (0)