9
9
using System . Linq ;
10
10
using System . Reflection ;
11
11
using System . Text . Json . Serialization ;
12
+ using Microsoft . AspNetCore . Http . Metadata ;
12
13
using Microsoft . Extensions . DependencyInjection ;
13
14
14
15
namespace Microsoft . Extensions . Validation ;
@@ -83,14 +84,17 @@ public bool TryGetValidatableParameterInfo(
83
84
. GetCustomAttributes < ValidationAttribute > ( )
84
85
. ToArray ( ) ;
85
86
87
+ // Skip early if the type has no validation attributes and no validatable properties
88
+ var hasTypeValidationAttributes = typeValidationAttributes . Length > 0 ;
89
+
86
90
// Get all public instance properties
87
91
var properties = type . GetProperties ( BindingFlags . Public | BindingFlags . Instance ) ;
88
92
var validatableProperties = new List < RuntimeValidatablePropertyInfo > ( ) ;
89
93
90
94
foreach ( var property in properties )
91
95
{
92
96
// Skip properties without setters (read-only properties) for normal classes
93
- if ( ! property . CanWrite && ! IsRecordType ( type ) )
97
+ if ( ! property . CanWrite && ! IsRecordType ( type ) && ! IsRecordStruct ( type ) )
94
98
{
95
99
continue ;
96
100
}
@@ -107,12 +111,15 @@ public bool TryGetValidatableParameterInfo(
107
111
. ToArray ( ) ;
108
112
109
113
// For record types, also check constructor parameters for validation attributes
110
- if ( propertyValidationAttributes . Length == 0 && IsRecordType ( type ) )
114
+ if ( IsRecordType ( type ) || IsRecordStruct ( type ) )
111
115
{
112
116
var constructorValidationAttributes = GetValidationAttributesFromConstructorParameter ( type , property . Name ) ;
113
117
if ( constructorValidationAttributes . Length > 0 )
114
118
{
115
- propertyValidationAttributes = constructorValidationAttributes ;
119
+ // Merge property and constructor validation attributes
120
+ var allAttributes = new List < ValidationAttribute > ( propertyValidationAttributes ) ;
121
+ allAttributes . AddRange ( constructorValidationAttributes ) ;
122
+ propertyValidationAttributes = [ .. allAttributes ] ;
116
123
}
117
124
}
118
125
@@ -141,12 +148,16 @@ public bool TryGetValidatableParameterInfo(
141
148
var derivedTypes = GetDerivedTypes ( type ) ;
142
149
foreach ( var derivedType in derivedTypes )
143
150
{
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
+ }
146
157
}
147
158
148
159
// 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 )
150
161
{
151
162
return new RuntimeValidatableTypeInfo ( type , validatableProperties ) ;
152
163
}
@@ -186,7 +197,7 @@ private static string GetDisplayNameForProperty(PropertyInfo property)
186
197
}
187
198
188
199
// For record types, also check constructor parameter for Display attribute
189
- if ( IsRecordType ( property . DeclaringType ! ) )
200
+ if ( IsRecordType ( property . DeclaringType ! ) || IsRecordStruct ( property . DeclaringType ! ) )
190
201
{
191
202
var constructorDisplayName = GetDisplayNameFromConstructorParameter ( property . DeclaringType ! , property . Name ) ;
192
203
if ( ! string . IsNullOrEmpty ( constructorDisplayName ) )
@@ -232,6 +243,37 @@ private static bool IsRecordType(Type type)
232
243
. Any ( m => m . Name == "<Clone>$" || m . Name == "get_EqualityContract" ) ;
233
244
}
234
245
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
+
235
277
private static ValidationAttribute [ ] GetValidationAttributesFromConstructorParameter ( Type type , string propertyName )
236
278
{
237
279
// Look for primary constructor parameters that match the property name
@@ -359,15 +401,13 @@ private static bool IsParsableType(Type type)
359
401
}
360
402
361
403
private static bool IsClassForType ( Type type )
362
- => ! IsParsableType ( type ) && type . IsClass ;
404
+ => ! IsParsableType ( type ) && ( type . IsClass || IsRecordStruct ( type ) ) ;
363
405
364
406
private static bool HasFromServiceAttributes ( IEnumerable < Attribute > attributes )
365
407
{
366
- // Note: Use name-based comparison for FromServices attribute defined in
367
- // MVC assemblies.
368
408
return attributes . Any ( attr =>
369
- attr . GetType ( ) . Name == "FromServicesAttribute" ||
370
- attr . GetType ( ) == typeof ( FromKeyedServicesAttribute ) ) ;
409
+ attr is IFromServiceMetadata ||
410
+ attr is FromKeyedServicesAttribute ) ;
371
411
}
372
412
373
413
internal sealed class RuntimeValidatablePropertyInfo ( [ DynamicallyAccessedMembers ( DynamicallyAccessedMemberTypes . PublicProperties ) ] Type declaringType ,
0 commit comments