Skip to content

Commit 05b5152

Browse files
Address code review feedback: fix FromService checking, optimize attribute checks, support record structs, and improve validation logic
Co-authored-by: BrennanConroy <[email protected]>
1 parent 3998440 commit 05b5152

File tree

3 files changed

+56
-16
lines changed

3 files changed

+56
-16
lines changed

src/Validation/src/Microsoft.Extensions.Validation.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
</PropertyGroup>
1313

1414
<ItemGroup>
15+
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
1516
<Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
1617
<Reference Include="Microsoft.Extensions.Options" />
1718
</ItemGroup>

src/Validation/src/RuntimeValidatableParameterInfoResolver.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Linq;
1010
using System.Reflection;
1111
using System.Security.Claims;
12+
using Microsoft.AspNetCore.Http.Metadata;
1213
using Microsoft.Extensions.DependencyInjection;
1314

1415
namespace Microsoft.Extensions.Validation;
@@ -101,11 +102,9 @@ private static bool IsClass(Type type)
101102

102103
private static bool HasFromServiceAttributes(IEnumerable<Attribute> attributes)
103104
{
104-
// Note: Use name-based comparison for FromServices attribute defined in
105-
// MVC assemblies.
106105
return attributes.Any(attr =>
107-
attr.GetType().Name == "FromServicesAttribute" ||
108-
attr.GetType() == typeof(FromKeyedServicesAttribute));
106+
attr is IFromServiceMetadata ||
107+
attr is FromKeyedServicesAttribute);
109108
}
110109

111110
internal sealed class RuntimeValidatableParameterInfo(

src/Validation/src/RuntimeValidatableTypeInfoResolver.cs

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Linq;
1010
using System.Reflection;
1111
using System.Text.Json.Serialization;
12+
using Microsoft.AspNetCore.Http.Metadata;
1213
using Microsoft.Extensions.DependencyInjection;
1314

1415
namespace Microsoft.Extensions.Validation;
@@ -83,14 +84,17 @@ public bool TryGetValidatableParameterInfo(
8384
.GetCustomAttributes<ValidationAttribute>()
8485
.ToArray();
8586

87+
// Skip early if the type has no validation attributes and no validatable properties
88+
var hasTypeValidationAttributes = typeValidationAttributes.Length > 0;
89+
8690
// Get all public instance properties
8791
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
8892
var validatableProperties = new List<RuntimeValidatablePropertyInfo>();
8993

9094
foreach (var property in properties)
9195
{
9296
// Skip properties without setters (read-only properties) for normal classes
93-
if (!property.CanWrite && !IsRecordType(type))
97+
if (!property.CanWrite && !IsRecordType(type) && !IsRecordStruct(type))
9498
{
9599
continue;
96100
}
@@ -107,12 +111,15 @@ public bool TryGetValidatableParameterInfo(
107111
.ToArray();
108112

109113
// For record types, also check constructor parameters for validation attributes
110-
if (propertyValidationAttributes.Length == 0 && IsRecordType(type))
114+
if (IsRecordType(type) || IsRecordStruct(type))
111115
{
112116
var constructorValidationAttributes = GetValidationAttributesFromConstructorParameter(type, property.Name);
113117
if (constructorValidationAttributes.Length > 0)
114118
{
115-
propertyValidationAttributes = constructorValidationAttributes;
119+
// Merge property and constructor validation attributes
120+
var allAttributes = new List<ValidationAttribute>(propertyValidationAttributes);
121+
allAttributes.AddRange(constructorValidationAttributes);
122+
propertyValidationAttributes = [.. allAttributes];
116123
}
117124
}
118125

@@ -141,12 +148,16 @@ public bool TryGetValidatableParameterInfo(
141148
var derivedTypes = GetDerivedTypes(type);
142149
foreach (var derivedType in derivedTypes)
143150
{
144-
// Recursively ensure derived types are also cached
145-
CreateValidatableTypeInfo(derivedType, visitedTypes);
151+
// Ensure derived types are also available for validation
152+
// We don't need to use the return value as it's automatically cached
153+
if (!_cache.ContainsKey(derivedType))
154+
{
155+
CreateValidatableTypeInfo(derivedType, visitedTypes);
156+
}
146157
}
147158

148159
// Only create type info if there are validation attributes on the type or validatable properties
149-
if (typeValidationAttributes.Length > 0 || validatableProperties.Count > 0)
160+
if (hasTypeValidationAttributes || validatableProperties.Count > 0)
150161
{
151162
return new RuntimeValidatableTypeInfo(type, validatableProperties);
152163
}
@@ -186,7 +197,7 @@ private static string GetDisplayNameForProperty(PropertyInfo property)
186197
}
187198

188199
// For record types, also check constructor parameter for Display attribute
189-
if (IsRecordType(property.DeclaringType!))
200+
if (IsRecordType(property.DeclaringType!) || IsRecordStruct(property.DeclaringType!))
190201
{
191202
var constructorDisplayName = GetDisplayNameFromConstructorParameter(property.DeclaringType!, property.Name);
192203
if (!string.IsNullOrEmpty(constructorDisplayName))
@@ -232,6 +243,37 @@ private static bool IsRecordType(Type type)
232243
.Any(m => m.Name == "<Clone>$" || m.Name == "get_EqualityContract");
233244
}
234245

246+
private static bool IsRecordStruct(Type type)
247+
{
248+
// Check if the type is a record struct by looking for record-specific characteristics
249+
// Record structs are value types with specific compiler-generated methods
250+
if (!type.IsValueType || type.IsEnum || type.IsPrimitive)
251+
{
252+
return false;
253+
}
254+
255+
// Record structs have an EqualityContract property like classes but as static readonly
256+
var equalityContract = type.GetProperty("EqualityContract", BindingFlags.Public | BindingFlags.Static);
257+
if (equalityContract?.GetMethod?.IsStatic == true)
258+
{
259+
return true;
260+
}
261+
262+
// Alternative check: Record structs have a primary constructor pattern
263+
// They typically have a ToString() override and specific constructor patterns
264+
var constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
265+
var hasParameterizedConstructor = constructors.Any(c => c.GetParameters().Length > 0);
266+
267+
if (hasParameterizedConstructor)
268+
{
269+
// Check for ToString override that's compiler generated for records
270+
var toStringMethod = type.GetMethod("ToString", BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null);
271+
return toStringMethod?.DeclaringType == type;
272+
}
273+
274+
return false;
275+
}
276+
235277
private static ValidationAttribute[] GetValidationAttributesFromConstructorParameter(Type type, string propertyName)
236278
{
237279
// Look for primary constructor parameters that match the property name
@@ -359,15 +401,13 @@ private static bool IsParsableType(Type type)
359401
}
360402

361403
private static bool IsClassForType(Type type)
362-
=> !IsParsableType(type) && type.IsClass;
404+
=> !IsParsableType(type) && (type.IsClass || IsRecordStruct(type));
363405

364406
private static bool HasFromServiceAttributes(IEnumerable<Attribute> attributes)
365407
{
366-
// Note: Use name-based comparison for FromServices attribute defined in
367-
// MVC assemblies.
368408
return attributes.Any(attr =>
369-
attr.GetType().Name == "FromServicesAttribute" ||
370-
attr.GetType() == typeof(FromKeyedServicesAttribute));
409+
attr is IFromServiceMetadata ||
410+
attr is FromKeyedServicesAttribute);
371411
}
372412

373413
internal sealed class RuntimeValidatablePropertyInfo([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type declaringType,

0 commit comments

Comments
 (0)