@@ -14,7 +14,7 @@ namespace Microsoft.Extensions.Validation;
14
14
public abstract class ValidatableTypeInfo : IValidatableInfo
15
15
{
16
16
private readonly int _membersCount ;
17
- private readonly List < Type > _subTypes ;
17
+ private readonly List < Type > _superTypes ;
18
18
19
19
/// <summary>
20
20
/// Creates a new instance of <see cref="ValidatableTypeInfo"/>.
@@ -28,9 +28,15 @@ protected ValidatableTypeInfo(
28
28
Type = type ;
29
29
Members = members ;
30
30
_membersCount = members . Count ;
31
- _subTypes = type . GetAllImplementedTypes ( ) ;
31
+ _superTypes = type . GetAllImplementedTypes ( ) ;
32
32
}
33
33
34
+ /// <summary>
35
+ /// Gets the validation attributes for this member.
36
+ /// </summary>
37
+ /// <returns>An array of validation attributes to apply to this member.</returns>
38
+ protected abstract ValidationAttribute [ ] GetValidationAttributes ( ) ;
39
+
34
40
/// <summary>
35
41
/// The type being validated.
36
42
/// </summary>
@@ -59,75 +65,139 @@ public virtual async Task ValidateAsync(object? value, ValidateContext context,
59
65
}
60
66
61
67
var originalPrefix = context . CurrentValidationPath ;
68
+ var originalErrorCount = context . ValidationErrors ? . Count ?? 0 ;
62
69
63
70
try
64
71
{
72
+ // First validate direct members
73
+ await ValidateMembersAsync ( value , context , cancellationToken ) ;
74
+
65
75
var actualType = value . GetType ( ) ;
66
76
67
- // First validate members
68
- for ( var i = 0 ; i < _membersCount ; i ++ )
77
+ // Then validate inherited members
78
+ foreach ( var superTypeInfo in GetSuperTypeInfos ( actualType , context ) )
79
+ {
80
+ await superTypeInfo . ValidateMembersAsync ( value , context , cancellationToken ) ;
81
+ }
82
+
83
+ // If any property-level validation errors were found, return early
84
+ if ( context . ValidationErrors is not null && context . ValidationErrors . Count > originalErrorCount )
85
+ {
86
+ return ;
87
+ }
88
+
89
+ // Validate type-level attributes
90
+ ValidateTypeAttributes ( value , context ) ;
91
+
92
+ // If any type-level attribute errors were found, return early
93
+ if ( context . ValidationErrors is not null && context . ValidationErrors . Count > originalErrorCount )
94
+ {
95
+ return ;
96
+ }
97
+
98
+ // Finally validate IValidatableObject if implemented
99
+ ValidateValidatableObjectInterface ( value , context ) ;
100
+ }
101
+ finally
102
+ {
103
+ context . CurrentValidationPath = originalPrefix ;
104
+ }
105
+ }
106
+
107
+ private async Task ValidateMembersAsync ( object ? value , ValidateContext context , CancellationToken cancellationToken )
108
+ {
109
+ var originalPrefix = context . CurrentValidationPath ;
110
+
111
+ for ( var i = 0 ; i < _membersCount ; i ++ )
112
+ {
113
+ try
69
114
{
70
115
await Members [ i ] . ValidateAsync ( value , context , cancellationToken ) ;
116
+
117
+ }
118
+ finally
119
+ {
71
120
context . CurrentValidationPath = originalPrefix ;
72
121
}
122
+ }
123
+ }
124
+
125
+ private void ValidateTypeAttributes ( object ? value , ValidateContext context )
126
+ {
127
+ var validationAttributes = GetValidationAttributes ( ) ;
128
+ var errorPrefix = context . CurrentValidationPath ;
73
129
74
- // Then validate sub-types if any
75
- foreach ( var subType in _subTypes )
130
+ for ( var i = 0 ; i < validationAttributes . Length ; i ++ )
131
+ {
132
+ var attribute = validationAttributes [ i ] ;
133
+ var result = attribute . GetValidationResult ( value , context . ValidationContext ) ;
134
+ if ( result is not null && result != ValidationResult . Success && result . ErrorMessage is not null )
76
135
{
77
- // Check if the actual type is assignable to the sub-type
78
- // and validate it if it is
79
- if ( subType . IsAssignableFrom ( actualType ) )
136
+ // Create a validation error for each member name that is provided
137
+ foreach ( var memberName in result . MemberNames )
80
138
{
81
- if ( context . ValidationOptions . TryGetValidatableTypeInfo ( subType , out var subTypeInfo ) )
82
- {
83
- await subTypeInfo . ValidateAsync ( value , context , cancellationToken ) ;
84
- context . CurrentValidationPath = originalPrefix ;
85
- }
139
+ var key = string . IsNullOrEmpty ( errorPrefix ) ? memberName : $ "{ errorPrefix } .{ memberName } ";
140
+ context . AddOrExtendValidationError ( memberName , key , result . ErrorMessage , value ) ;
141
+ }
142
+
143
+ if ( ! result . MemberNames . Any ( ) )
144
+ {
145
+ // If no member names are specified, then treat this as a top-level error
146
+ context . AddOrExtendValidationError ( string . Empty , errorPrefix , result . ErrorMessage , value ) ;
86
147
}
87
148
}
149
+ }
150
+ }
88
151
89
- // Finally validate IValidatableObject if implemented
90
- if ( Type . ImplementsInterface ( typeof ( IValidatableObject ) ) && value is IValidatableObject validatable )
152
+ private void ValidateValidatableObjectInterface ( object ? value , ValidateContext context )
153
+ {
154
+ if ( Type . ImplementsInterface ( typeof ( IValidatableObject ) ) && value is IValidatableObject validatable )
155
+ {
156
+ // Important: Set the DisplayName to the type name for top-level validations
157
+ // and restore the original validation context properties
158
+ var originalDisplayName = context . ValidationContext . DisplayName ;
159
+ var originalMemberName = context . ValidationContext . MemberName ;
160
+ var errorPrefix = context . CurrentValidationPath ;
161
+
162
+ // Set the display name to the class name for IValidatableObject validation
163
+ context . ValidationContext . DisplayName = Type . Name ;
164
+ context . ValidationContext . MemberName = null ;
165
+
166
+ var validationResults = validatable . Validate ( context . ValidationContext ) ;
167
+ foreach ( var validationResult in validationResults )
91
168
{
92
- // Important: Set the DisplayName to the type name for top-level validations
93
- // and restore the original validation context properties
94
- var originalDisplayName = context . ValidationContext . DisplayName ;
95
- var originalMemberName = context . ValidationContext . MemberName ;
96
-
97
- // Set the display name to the class name for IValidatableObject validation
98
- context . ValidationContext . DisplayName = Type . Name ;
99
- context . ValidationContext . MemberName = null ;
100
-
101
- var validationResults = validatable . Validate ( context . ValidationContext ) ;
102
- foreach ( var validationResult in validationResults )
169
+ if ( validationResult != ValidationResult . Success && validationResult . ErrorMessage is not null )
103
170
{
104
- if ( validationResult != ValidationResult . Success && validationResult . ErrorMessage is not null )
171
+ // Create a validation error for each member name that is provided
172
+ foreach ( var memberName in validationResult . MemberNames )
105
173
{
106
- // Create a validation error for each member name that is provided
107
- foreach ( var memberName in validationResult . MemberNames )
108
- {
109
- var key = string . IsNullOrEmpty ( originalPrefix ) ?
110
- memberName :
111
- $ "{ originalPrefix } .{ memberName } ";
112
- context . AddOrExtendValidationError ( memberName , key , validationResult . ErrorMessage , value ) ;
113
- }
114
-
115
- if ( ! validationResult . MemberNames . Any ( ) )
116
- {
117
- // If no member names are specified, then treat this as a top-level error
118
- context . AddOrExtendValidationError ( string . Empty , string . Empty , validationResult . ErrorMessage , value ) ;
119
- }
174
+ var key = string . IsNullOrEmpty ( errorPrefix ) ? memberName : $ "{ errorPrefix } .{ memberName } ";
175
+ context . AddOrExtendValidationError ( memberName , key , validationResult . ErrorMessage , value ) ;
120
176
}
121
- }
122
177
123
- // Restore the original validation context properties
124
- context . ValidationContext . DisplayName = originalDisplayName ;
125
- context . ValidationContext . MemberName = originalMemberName ;
178
+ if ( ! validationResult . MemberNames . Any ( ) )
179
+ {
180
+ // If no member names are specified, then treat this as a top-level error
181
+ context . AddOrExtendValidationError ( string . Empty , string . Empty , validationResult . ErrorMessage , value ) ;
182
+ }
183
+ }
126
184
}
185
+
186
+ // Restore the original validation context properties
187
+ context . ValidationContext . DisplayName = originalDisplayName ;
188
+ context . ValidationContext . MemberName = originalMemberName ;
127
189
}
128
- finally
190
+ }
191
+
192
+ private IEnumerable < ValidatableTypeInfo > GetSuperTypeInfos ( Type actualType , ValidateContext context )
193
+ {
194
+ foreach ( var superType in _superTypes . Where ( t => t . IsAssignableFrom ( actualType ) ) )
129
195
{
130
- context . CurrentValidationPath = originalPrefix ;
196
+ if ( context . ValidationOptions . TryGetValidatableTypeInfo ( superType , out var found )
197
+ && found is ValidatableTypeInfo superTypeInfo )
198
+ {
199
+ yield return superTypeInfo ;
200
+ }
131
201
}
132
202
}
133
203
}
0 commit comments