Skip to content

Commit 5bbd091

Browse files
committed
Switch to runtime-based resolution for ParameterInfo validations
1 parent 889fd26 commit 5bbd091

9 files changed

+109
-428
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.ComponentModel.DataAnnotations;
5+
using System.Linq;
6+
using System.Reflection;
7+
8+
namespace Microsoft.AspNetCore.Http.Validation;
9+
10+
internal class RuntimeValidatableParameterInfoResolver : IValidatableInfoResolver
11+
{
12+
public ValidatableParameterInfo? GetValidatableParameterInfo(ParameterInfo parameterInfo)
13+
{
14+
var validationAttributes = parameterInfo
15+
.GetCustomAttributes<ValidationAttribute>()
16+
.ToArray();
17+
return new RuntimeValidatableParameterInfo(
18+
name: parameterInfo.Name!,
19+
displayName: GetDisplayName(parameterInfo),
20+
isNullable: IsNullable(parameterInfo),
21+
isRequired: validationAttributes.Any(a => a is RequiredAttribute),
22+
// All parameters are validatable, implementation will no-op if type resolution fails
23+
hasValidatableType: true,
24+
isEnumerable: IsEnumerable(parameterInfo),
25+
validationAttributes: validationAttributes
26+
);
27+
}
28+
29+
private static bool IsNullable(ParameterInfo parameterInfo)
30+
{
31+
if (parameterInfo.ParameterType.IsValueType)
32+
{
33+
return false;
34+
}
35+
36+
if (parameterInfo.ParameterType.IsGenericType &&
37+
parameterInfo.ParameterType.GetGenericTypeDefinition() == typeof(Nullable<>))
38+
{
39+
return true;
40+
}
41+
42+
return false;
43+
}
44+
45+
private static string GetDisplayName(ParameterInfo parameterInfo)
46+
{
47+
var displayAttribute = parameterInfo.GetCustomAttribute<DisplayAttribute>();
48+
if (displayAttribute != null)
49+
{
50+
return displayAttribute.Name ?? parameterInfo.Name!;
51+
}
52+
53+
return parameterInfo.Name!;
54+
}
55+
56+
private static bool IsEnumerable(ParameterInfo parameterInfo)
57+
{
58+
if (parameterInfo.ParameterType.IsGenericType &&
59+
parameterInfo.ParameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
60+
{
61+
return true;
62+
}
63+
64+
if (parameterInfo.ParameterType.IsArray)
65+
{
66+
return true;
67+
}
68+
69+
return false;
70+
}
71+
72+
public ValidatableTypeInfo? GetValidatableTypeInfo(Type type)
73+
{
74+
return null;
75+
}
76+
77+
private class RuntimeValidatableParameterInfo(
78+
string name,
79+
string displayName,
80+
bool isNullable,
81+
bool isRequired,
82+
bool hasValidatableType,
83+
bool isEnumerable,
84+
ValidationAttribute[] validationAttributes) :
85+
ValidatableParameterInfo(name, displayName, isNullable, isRequired, hasValidatableType, isEnumerable)
86+
{
87+
protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes;
88+
89+
private readonly ValidationAttribute[] _validationAttributes = validationAttributes;
90+
}
91+
}

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ public static class ValidationServiceCollectionExtensions
1818
/// <returns>The <see cref="IServiceCollection" /> for chaining.</returns>
1919
public static IServiceCollection AddValidation(this IServiceCollection services, Action<ValidationOptions>? configureOptions = null)
2020
{
21-
if (configureOptions is null)
21+
services.Configure<ValidationOptions>(options =>
2222
{
23-
services.Configure<ValidationOptions>(_ => { });
24-
return services;
25-
}
26-
27-
services.Configure(configureOptions);
23+
if (configureOptions is not null)
24+
{
25+
configureOptions(options);
26+
}
27+
// Support ParameterInfo resolution at runtime
28+
options.Resolvers.Add(new RuntimeValidatableParameterInfoResolver());
29+
});
2830
return services;
2931
}
3032
}

src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Emitters/ValidationsGenerator.Emitter.cs

Lines changed: 4 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ public sealed partial class ValidationsGenerator : IIncrementalGenerator
1616
public static string GeneratedCodeConstructor => $@"System.CodeDom.Compiler.GeneratedCodeAttribute(""{typeof(ValidationsGenerator).Assembly.FullName}"", ""{typeof(ValidationsGenerator).Assembly.GetName().Version}"")";
1717
public static string GeneratedCodeAttribute => $"[{GeneratedCodeConstructor}]";
1818

19-
internal static void Emit(SourceProductionContext context, ((InterceptableLocation? AddValidation, ImmutableArray<ValidatableType> Types) First, ImmutableArray<ValidatableParameter> Parameters) emitInputs)
19+
internal static void Emit(SourceProductionContext context, (InterceptableLocation? AddValidation, ImmutableArray<ValidatableType> ValidatableTypes) emitInputs)
2020
{
21-
var source = Emit(emitInputs.First.AddValidation, emitInputs.First.Types, emitInputs.Parameters);
21+
var source = Emit(emitInputs.AddValidation, emitInputs.ValidatableTypes);
2222
context.AddSource("ValidatableInfoResolver.g.cs", SourceText.From(source, Encoding.UTF8));
2323
}
2424

25-
private static string Emit(InterceptableLocation? addValidation, ImmutableArray<ValidatableType> validatableTypes, ImmutableArray<ValidatableParameter> validatableParameters) => $$"""
25+
private static string Emit(InterceptableLocation? addValidation, ImmutableArray<ValidatableType> validatableTypes) => $$"""
2626
//------------------------------------------------------------------------------
2727
// <auto-generated>
2828
// This code was generated by a tool.
@@ -77,26 +77,6 @@ public GeneratedValidatablePropertyInfo(
7777
protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes;
7878
}
7979
80-
{{GeneratedCodeAttribute}}
81-
file sealed class GeneratedValidatableParameterInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo
82-
{
83-
private readonly ValidationAttribute[] _validationAttributes;
84-
85-
public GeneratedValidatableParameterInfo(
86-
string name,
87-
string displayName,
88-
bool isNullable,
89-
bool isRequired,
90-
bool hasValidatableType,
91-
bool isEnumerable,
92-
ValidationAttribute[] validationAttributes) : base(name, displayName, isNullable, isRequired, hasValidatableType, isEnumerable)
93-
{
94-
_validationAttributes = validationAttributes;
95-
}
96-
97-
protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes;
98-
}
99-
10080
{{GeneratedCodeAttribute}}
10181
file sealed class GeneratedValidatableTypeInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo
10282
{
@@ -116,14 +96,13 @@ public GeneratedValidatableTypeInfo(
11696
return null;
11797
}
11898
99+
// No-ops, rely on runtime code for ParameterInfo-based resolution
119100
public ValidatableParameterInfo? GetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo)
120101
{
121-
{{EmitParameterTypeChecks(validatableParameters)}}
122102
return null;
123103
}
124104
125105
{{EmitCreateMethods(validatableTypes)}}
126-
{{EmitCreateParameterMethods(validatableParameters)}}
127106
}
128107
129108
{{GeneratedCodeAttribute}}
@@ -296,71 +275,6 @@ private static string EmitTypeChecks(ImmutableArray<ValidatableType> validatable
296275
return sw.ToString();
297276
}
298277

299-
private static string EmitParameterTypeChecks(ImmutableArray<ValidatableParameter> validatableParameters)
300-
{
301-
var sw = new StringWriter();
302-
var cw = new CodeWriter(sw, baseIndent: 3);
303-
304-
// Group parameters by name to handle potential duplicates
305-
var parameterGroups = validatableParameters.GroupBy(p => p.Name).ToList();
306-
307-
foreach (var group in parameterGroups)
308-
{
309-
var name = group.Key;
310-
var parameters = group.ToList();
311-
312-
// If there's only one parameter with this name, use the simple check
313-
if (parameters.Count == 1)
314-
{
315-
var param = parameters[0];
316-
var parameterTypeName = param.Type.ToDisplayString();
317-
cw.WriteLine($"if (parameterInfo.Name == \"{param.Name}\" && parameterInfo.ParameterType == typeof({parameterTypeName}))");
318-
cw.StartBlock();
319-
cw.WriteLine($"return CreateParameterInfo_{SanitizeTypeName(param.OriginalType.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat))}_{SanitizeTypeName(param.Name)}_{param.Index}();");
320-
cw.EndBlock();
321-
}
322-
else
323-
{
324-
// For parameters with the same name, we need additional checks to distinguish them
325-
cw.WriteLine($"if (parameterInfo.Name == \"{name}\")");
326-
cw.StartBlock();
327-
328-
// Check parameter type first as it's faster
329-
for (var i = 0; i < parameters.Count; i++)
330-
{
331-
var param = parameters[i];
332-
var parameterTypeName = param.Type.ToDisplayString();
333-
334-
// For first item, use 'if', for others use 'else if'
335-
string ifStatement = i == 0 ? "if" : "else if";
336-
337-
cw.WriteLine($"{ifStatement} (parameterInfo.ParameterType == typeof({parameterTypeName}))");
338-
cw.StartBlock();
339-
340-
// Add position check if available
341-
if (param.Index >= 0)
342-
{
343-
cw.WriteLine($"if (parameterInfo.Position == {param.Index})");
344-
cw.StartBlock();
345-
}
346-
347-
cw.WriteLine($"return CreateParameterInfo_{SanitizeTypeName(param.OriginalType.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat))}_{SanitizeTypeName(param.Name)}_{param.Index}();");
348-
349-
if (param.Index >= 0)
350-
{
351-
cw.EndBlock();
352-
}
353-
354-
cw.EndBlock();
355-
}
356-
357-
cw.EndBlock();
358-
}
359-
}
360-
361-
return sw.ToString();
362-
}
363-
364278
private static string EmitCreateMethods(ImmutableArray<ValidatableType> validatableTypes)
365279
{
366280
var sw = new StringWriter();
@@ -404,34 +318,6 @@ private static string EmitValidatableMemberForCreate(ValidatableProperty member)
404318
""";
405319
}
406320

407-
private static string EmitCreateParameterMethods(ImmutableArray<ValidatableParameter> validatableParameters)
408-
{
409-
var sw = new StringWriter();
410-
var cw = new CodeWriter(sw, baseIndent: 3);
411-
foreach (var validatableParameter in validatableParameters)
412-
{
413-
var parameterTypeName = validatableParameter.Type.ToDisplayString();
414-
cw.WriteLine($@"private ValidatableParameterInfo CreateParameterInfo_{SanitizeTypeName(validatableParameter.OriginalType.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat))}_{SanitizeTypeName(validatableParameter.Name)}_{validatableParameter.Index}()");
415-
cw.StartBlock();
416-
var validationAttributes = validatableParameter.Attributes.IsDefaultOrEmpty
417-
? "[]"
418-
: $"[{string.Join(", ", validatableParameter.Attributes.Select(EmitValidationAttributeForCreate))}]";
419-
cw.WriteLine($"""
420-
return new GeneratedValidatableParameterInfo(
421-
name: "{validatableParameter.Name}",
422-
displayName: "{validatableParameter.DisplayName}",
423-
isRequired: {validatableParameter.IsRequired.ToString().ToLowerInvariant()},
424-
isNullable: {validatableParameter.IsNullable.ToString().ToLowerInvariant()},
425-
hasValidatableType: {validatableParameter.HasValidatableType.ToString().ToLowerInvariant()},
426-
isEnumerable: {validatableParameter.IsEnumerable.ToString().ToLowerInvariant()},
427-
validationAttributes: {validationAttributes}
428-
);
429-
""");
430-
cw.EndBlock();
431-
}
432-
return sw.ToString();
433-
}
434-
435321
private static string EmitValidationAttributeForCreate(ValidationAttribute attr)
436322
{
437323
var args = attr.Arguments.Count > 0

src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/ValidationsGenerator.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
4141
.Concat(validatableTypesWithAttribute)
4242
.Distinct(ValidatableTypeComparer.Instance)
4343
.Collect();
44-
// Extract all validatable parameters encountered in minimal endpoints.
45-
var validatableParameters = validatableEndpoints
46-
.SelectMany((endpoint, ct) => endpoint.Parameters)
47-
.Collect();
4844

4945
var emitInputs = addValidation
50-
.Combine(validatableTypes)
51-
.Combine(validatableParameters);
46+
.Combine(validatableTypes);
5247

5348
// Emit ValidatableTypeInfo for all validatable types.
5449
context.RegisterSourceOutput(emitInputs, Emit);

src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -53,26 +53,6 @@ public GeneratedValidatablePropertyInfo(
5353
protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes;
5454
}
5555

56-
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
57-
file sealed class GeneratedValidatableParameterInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatableParameterInfo
58-
{
59-
private readonly ValidationAttribute[] _validationAttributes;
60-
61-
public GeneratedValidatableParameterInfo(
62-
string name,
63-
string displayName,
64-
bool isNullable,
65-
bool isRequired,
66-
bool hasValidatableType,
67-
bool isEnumerable,
68-
ValidationAttribute[] validationAttributes) : base(name, displayName, isNullable, isRequired, hasValidatableType, isEnumerable)
69-
{
70-
_validationAttributes = validationAttributes;
71-
}
72-
73-
protected override ValidationAttribute[] GetValidationAttributes() => _validationAttributes;
74-
}
75-
7656
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]
7757
file sealed class GeneratedValidatableTypeInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo
7858
{
@@ -104,13 +84,9 @@ public GeneratedValidatableTypeInfo(
10484
return null;
10585
}
10686

87+
// No-ops, rely on runtime code for ParameterInfo-based resolution
10788
public ValidatableParameterInfo? GetValidatableParameterInfo(global::System.Reflection.ParameterInfo parameterInfo)
10889
{
109-
if (parameterInfo.Name == "complexType" && parameterInfo.ParameterType == typeof(ComplexType))
110-
{
111-
return CreateParameterInfo_ComplexType_complexType_0();
112-
}
113-
11490
return null;
11591
}
11692

@@ -252,19 +228,6 @@ private ValidatableTypeInfo CreateComplexType()
252228
implementsIValidatableObject: false);
253229
}
254230

255-
private ValidatableParameterInfo CreateParameterInfo_ComplexType_complexType_0()
256-
{
257-
return new GeneratedValidatableParameterInfo(
258-
name: "complexType",
259-
displayName: "complexType",
260-
isRequired: false,
261-
isNullable: false,
262-
hasValidatableType: true,
263-
isEnumerable: false,
264-
validationAttributes: []
265-
);
266-
}
267-
268231
}
269232

270233
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.ValidationsGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")]

0 commit comments

Comments
 (0)