Skip to content

Commit d0cd312

Browse files
Use trim-safe ValidationContext constructor and update RuntimeParameterInfoResolver (#60976)
* Use trim-safe ValiationContext constructor and update RuntimeParameterInfoResolver * Address feedback * Add DAM attributes to generated code * Apply suggestions from code review Co-authored-by: Brennan <[email protected]> --------- Co-authored-by: Brennan <[email protected]>
1 parent 06f3c41 commit d0cd312

11 files changed

+258
-19
lines changed

src/Http/Http.Abstractions/src/Validation/RuntimeValidatableParameterInfoResolver.cs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44
using System.ComponentModel.DataAnnotations;
55
using System.Diagnostics.CodeAnalysis;
6+
using System.IO.Pipelines;
67
using System.Linq;
78
using System.Reflection;
9+
using System.Security.Claims;
810

911
namespace Microsoft.AspNetCore.Http.Validation;
1012

@@ -27,6 +29,15 @@ public bool TryGetValidatableParameterInfo(ParameterInfo parameterInfo, [NotNull
2729
var validationAttributes = parameterInfo
2830
.GetCustomAttributes<ValidationAttribute>()
2931
.ToArray();
32+
33+
// If there are no validation attributes and this type is not a complex type
34+
// we don't need to validate it. Complex types without attributes are still
35+
// validatable because we want to run the validations on the properties.
36+
if (validationAttributes.Length == 0 && !IsClass(parameterInfo.ParameterType))
37+
{
38+
validatableInfo = null;
39+
return false;
40+
}
3041
validatableInfo = new RuntimeValidatableParameterInfo(
3142
parameterType: parameterInfo.ParameterType,
3243
name: parameterInfo.Name,
@@ -47,7 +58,7 @@ private static string GetDisplayName(ParameterInfo parameterInfo)
4758
return parameterInfo.Name!;
4859
}
4960

50-
private sealed class RuntimeValidatableParameterInfo(
61+
internal sealed class RuntimeValidatableParameterInfo(
5162
Type parameterType,
5263
string name,
5364
string displayName,
@@ -58,4 +69,41 @@ private sealed class RuntimeValidatableParameterInfo(
5869

5970
private readonly ValidationAttribute[] _validationAttributes = validationAttributes;
6071
}
72+
73+
private static bool IsClass(Type type)
74+
{
75+
// Skip primitives, enums, common built-in types, and types that are specially
76+
// handled by RDF/RDG that don't need validation if they don't have attributes
77+
if (type.IsPrimitive ||
78+
type.IsEnum ||
79+
type == typeof(string) ||
80+
type == typeof(decimal) ||
81+
type == typeof(DateTime) ||
82+
type == typeof(DateTimeOffset) ||
83+
type == typeof(TimeOnly) ||
84+
type == typeof(DateOnly) ||
85+
type == typeof(TimeSpan) ||
86+
type == typeof(Guid) ||
87+
type == typeof(IFormFile) ||
88+
type == typeof(IFormFileCollection) ||
89+
type == typeof(IFormCollection) ||
90+
type == typeof(HttpContext) ||
91+
type == typeof(HttpRequest) ||
92+
type == typeof(HttpResponse) ||
93+
type == typeof(ClaimsPrincipal) ||
94+
type == typeof(CancellationToken) ||
95+
type == typeof(Stream) ||
96+
type == typeof(PipeReader))
97+
{
98+
return false;
99+
}
100+
101+
// Check if the underlying type in a nullable is valid
102+
if (Nullable.GetUnderlyingType(type) is { } nullableType)
103+
{
104+
return IsClass(nullableType);
105+
}
106+
107+
return type.IsClass;
108+
}
61109
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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.IO.Pipelines;
6+
using System.Reflection;
7+
using System.Security.Claims;
8+
9+
namespace Microsoft.AspNetCore.Http.Validation.Tests;
10+
11+
public class RuntimeValidatableParameterInfoResolverTests
12+
{
13+
private readonly RuntimeValidatableParameterInfoResolver _resolver = new();
14+
15+
[Fact]
16+
public void TryGetValidatableTypeInfo_AlwaysReturnsFalse()
17+
{
18+
var result = _resolver.TryGetValidatableTypeInfo(typeof(string), out var validatableInfo);
19+
20+
Assert.False(result);
21+
Assert.Null(validatableInfo);
22+
}
23+
24+
[Fact]
25+
public void TryGetValidatableParameterInfo_WithNullName_ThrowsInvalidOperationException()
26+
{
27+
var parameterInfo = new NullNameParameterInfo();
28+
29+
var exception = Assert.Throws<InvalidOperationException>(() =>
30+
_resolver.TryGetValidatableParameterInfo(parameterInfo, out _));
31+
32+
Assert.Contains("without a name", exception.Message);
33+
}
34+
35+
[Theory]
36+
[InlineData(typeof(string))]
37+
[InlineData(typeof(int))]
38+
[InlineData(typeof(bool))]
39+
[InlineData(typeof(DateTime))]
40+
[InlineData(typeof(Guid))]
41+
[InlineData(typeof(decimal))]
42+
[InlineData(typeof(DayOfWeek))] // Enum
43+
[InlineData(typeof(ClaimsPrincipal))]
44+
[InlineData(typeof(PipeReader))]
45+
[InlineData(typeof(DateTimeOffset))]
46+
[InlineData(typeof(TimeOnly))]
47+
[InlineData(typeof(DateOnly))]
48+
[InlineData(typeof(TimeSpan))]
49+
[InlineData(typeof(IFormFile))]
50+
[InlineData(typeof(IFormFileCollection))]
51+
[InlineData(typeof(IFormCollection))]
52+
[InlineData(typeof(HttpContext))]
53+
[InlineData(typeof(HttpRequest))]
54+
[InlineData(typeof(HttpResponse))]
55+
[InlineData(typeof(CancellationToken))]
56+
public void TryGetValidatableParameterInfo_WithSimpleTypesAndNoAttributes_ReturnsFalse(Type parameterType)
57+
{
58+
var parameterInfo = GetParameter(parameterType);
59+
60+
var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo);
61+
62+
Assert.False(result);
63+
Assert.Null(validatableInfo);
64+
}
65+
66+
[Fact]
67+
public void TryGetValidatableParameterInfo_WithClassTypeAndNoAttributes_ReturnsTrue()
68+
{
69+
var parameterInfo = GetParameter(typeof(TestClass));
70+
71+
var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo);
72+
73+
Assert.True(result);
74+
Assert.NotNull(validatableInfo);
75+
var parameterValidatableInfo = Assert.IsType<RuntimeValidatableParameterInfoResolver.RuntimeValidatableParameterInfo>(validatableInfo);
76+
Assert.Equal("testParam", parameterValidatableInfo.Name);
77+
Assert.Equal("testParam", parameterValidatableInfo.DisplayName);
78+
}
79+
80+
[Fact]
81+
public void TryGetValidatableParameterInfo_WithSimpleTypeAndAttributes_ReturnsTrue()
82+
{
83+
var parameterInfo = typeof(TestController)
84+
.GetMethod(nameof(TestController.MethodWithAttributedParam))!
85+
.GetParameters()[0];
86+
87+
var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo);
88+
89+
Assert.True(result);
90+
Assert.NotNull(validatableInfo);
91+
var parameterValidatableInfo = Assert.IsType<RuntimeValidatableParameterInfoResolver.RuntimeValidatableParameterInfo>(validatableInfo);
92+
Assert.Equal("value", parameterValidatableInfo.Name);
93+
Assert.Equal("value", parameterValidatableInfo.DisplayName);
94+
}
95+
96+
[Fact]
97+
public void TryGetValidatableParameterInfo_WithDisplayAttribute_UsesDisplayNameFromAttribute()
98+
{
99+
var parameterInfo = typeof(TestController)
100+
.GetMethod(nameof(TestController.MethodWithDisplayAttribute))!
101+
.GetParameters()[0];
102+
103+
var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo);
104+
105+
Assert.True(result);
106+
Assert.NotNull(validatableInfo);
107+
var parameterValidatableInfo = Assert.IsType<RuntimeValidatableParameterInfoResolver.RuntimeValidatableParameterInfo>(validatableInfo);
108+
Assert.Equal("value", parameterValidatableInfo.Name);
109+
Assert.Equal("Custom Display Name", parameterValidatableInfo.DisplayName);
110+
}
111+
112+
[Fact]
113+
public void TryGetValidatableParameterInfo_WithDisplayAttributeWithNullName_UsesParameterName()
114+
{
115+
var parameterInfo = typeof(TestController)
116+
.GetMethod(nameof(TestController.MethodWithNullDisplayName))!
117+
.GetParameters()[0];
118+
119+
var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo);
120+
121+
Assert.True(result);
122+
Assert.NotNull(validatableInfo);
123+
var parameterValidatableInfo = Assert.IsType<RuntimeValidatableParameterInfoResolver.RuntimeValidatableParameterInfo>(validatableInfo);
124+
Assert.Equal("value", parameterValidatableInfo.Name);
125+
Assert.Equal("value", parameterValidatableInfo.DisplayName);
126+
}
127+
128+
[Fact]
129+
public void TryGetValidatableParameterInfo_WithNullableValueType_ReturnsFalse()
130+
{
131+
var parameterInfo = GetParameter(typeof(int?));
132+
133+
var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo);
134+
135+
Assert.False(result);
136+
Assert.Null(validatableInfo);
137+
}
138+
139+
[Fact]
140+
public void TryGetValidatableParameterInfo_WithNullableReferenceType_ReturnsTrue()
141+
{
142+
var parameterInfo = GetNullableParameter(typeof(TestClass));
143+
144+
var result = _resolver.TryGetValidatableParameterInfo(parameterInfo, out var validatableInfo);
145+
146+
Assert.True(result);
147+
Assert.NotNull(validatableInfo);
148+
var parameterValidatableInfo = Assert.IsType<RuntimeValidatableParameterInfoResolver.RuntimeValidatableParameterInfo>(validatableInfo);
149+
Assert.Equal("testParam", parameterValidatableInfo.Name);
150+
Assert.Equal("testParam", parameterValidatableInfo.DisplayName);
151+
}
152+
153+
private static ParameterInfo GetParameter(Type parameterType)
154+
{
155+
return typeof(TestParameterHolder)
156+
.GetMethod(nameof(TestParameterHolder.Method))!
157+
.MakeGenericMethod(parameterType)
158+
.GetParameters()[0];
159+
}
160+
161+
private static ParameterInfo GetNullableParameter(Type parameterType)
162+
{
163+
return typeof(TestParameterHolder)
164+
.GetMethod(nameof(TestParameterHolder.MethodWithNullable))!
165+
.MakeGenericMethod(parameterType)
166+
.GetParameters()[0];
167+
}
168+
169+
private class TestClass { }
170+
171+
private class TestParameterHolder
172+
{
173+
public void Method<T>(T testParam) { }
174+
public void MethodWithNullable<T>(T? testParam) { }
175+
}
176+
177+
private class TestController
178+
{
179+
public void MethodWithAttributedParam([Required] string value) { }
180+
181+
public void MethodWithDisplayAttribute([Display(Name = "Custom Display Name")][Required] string value) { }
182+
183+
public void MethodWithNullDisplayName([Display(Name = null)][Required] string value) { }
184+
}
185+
186+
private class NullNameParameterInfo : ParameterInfo
187+
{
188+
public override string? Name => null;
189+
public override Type ParameterType => typeof(string);
190+
}
191+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ namespace Microsoft.AspNetCore.Http.Validation.Generated
5656
file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo
5757
{
5858
public GeneratedValidatablePropertyInfo(
59+
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)]
5960
global::System.Type containingType,
6061
global::System.Type propertyType,
6162
string name,
@@ -76,6 +77,7 @@ public GeneratedValidatablePropertyInfo(
7677
file sealed class GeneratedValidatableTypeInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo
7778
{
7879
public GeneratedValidatableTypeInfo(
80+
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)]
7981
global::System.Type type,
8082
ValidatablePropertyInfo[] members) : base(type, members) { }
8183
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ namespace Microsoft.AspNetCore.Http.Validation.Generated
2828
file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo
2929
{
3030
public GeneratedValidatablePropertyInfo(
31+
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)]
3132
global::System.Type containingType,
3233
global::System.Type propertyType,
3334
string name,
@@ -48,6 +49,7 @@ public GeneratedValidatablePropertyInfo(
4849
file sealed class GeneratedValidatableTypeInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo
4950
{
5051
public GeneratedValidatableTypeInfo(
52+
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)]
5153
global::System.Type type,
5254
ValidatablePropertyInfo[] members) : base(type, members) { }
5355
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ namespace Microsoft.AspNetCore.Http.Validation.Generated
2828
file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo
2929
{
3030
public GeneratedValidatablePropertyInfo(
31+
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)]
3132
global::System.Type containingType,
3233
global::System.Type propertyType,
3334
string name,
@@ -48,6 +49,7 @@ public GeneratedValidatablePropertyInfo(
4849
file sealed class GeneratedValidatableTypeInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo
4950
{
5051
public GeneratedValidatableTypeInfo(
52+
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)]
5153
global::System.Type type,
5254
ValidatablePropertyInfo[] members) : base(type, members) { }
5355
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ namespace Microsoft.AspNetCore.Http.Validation.Generated
2828
file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo
2929
{
3030
public GeneratedValidatablePropertyInfo(
31+
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)]
3132
global::System.Type containingType,
3233
global::System.Type propertyType,
3334
string name,
@@ -48,6 +49,7 @@ public GeneratedValidatablePropertyInfo(
4849
file sealed class GeneratedValidatableTypeInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo
4950
{
5051
public GeneratedValidatableTypeInfo(
52+
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)]
5153
global::System.Type type,
5254
ValidatablePropertyInfo[] members) : base(type, members) { }
5355
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ namespace Microsoft.AspNetCore.Http.Validation.Generated
2828
file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo
2929
{
3030
public GeneratedValidatablePropertyInfo(
31+
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)]
3132
global::System.Type containingType,
3233
global::System.Type propertyType,
3334
string name,
@@ -48,6 +49,7 @@ public GeneratedValidatablePropertyInfo(
4849
file sealed class GeneratedValidatableTypeInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo
4950
{
5051
public GeneratedValidatableTypeInfo(
52+
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)]
5153
global::System.Type type,
5254
ValidatablePropertyInfo[] members) : base(type, members) { }
5355
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ namespace Microsoft.AspNetCore.Http.Validation.Generated
2828
file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo
2929
{
3030
public GeneratedValidatablePropertyInfo(
31+
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)]
3132
global::System.Type containingType,
3233
global::System.Type propertyType,
3334
string name,
@@ -48,6 +49,7 @@ public GeneratedValidatablePropertyInfo(
4849
file sealed class GeneratedValidatableTypeInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo
4950
{
5051
public GeneratedValidatableTypeInfo(
52+
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)]
5153
global::System.Type type,
5254
ValidatablePropertyInfo[] members) : base(type, members) { }
5355
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ namespace Microsoft.AspNetCore.Http.Validation.Generated
2828
file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo
2929
{
3030
public GeneratedValidatablePropertyInfo(
31+
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)]
3132
global::System.Type containingType,
3233
global::System.Type propertyType,
3334
string name,
@@ -48,6 +49,7 @@ public GeneratedValidatablePropertyInfo(
4849
file sealed class GeneratedValidatableTypeInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo
4950
{
5051
public GeneratedValidatableTypeInfo(
52+
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)]
5153
global::System.Type type,
5254
ValidatablePropertyInfo[] members) : base(type, members) { }
5355
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ namespace Microsoft.AspNetCore.Http.Validation.Generated
2828
file sealed class GeneratedValidatablePropertyInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatablePropertyInfo
2929
{
3030
public GeneratedValidatablePropertyInfo(
31+
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)]
3132
global::System.Type containingType,
3233
global::System.Type propertyType,
3334
string name,
@@ -48,6 +49,7 @@ public GeneratedValidatablePropertyInfo(
4849
file sealed class GeneratedValidatableTypeInfo : global::Microsoft.AspNetCore.Http.Validation.ValidatableTypeInfo
4950
{
5051
public GeneratedValidatableTypeInfo(
52+
[param: global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)]
5153
global::System.Type type,
5254
ValidatablePropertyInfo[] members) : base(type, members) { }
5355
}

0 commit comments

Comments
 (0)