@@ -14,7 +14,7 @@ namespace Microsoft.Extensions.Validation;
1414public abstract class ValidatableTypeInfo : IValidatableInfo
1515{
1616 private readonly int _membersCount ;
17- private readonly List < Type > _subTypes ;
17+ private readonly List < Type > _superTypes ;
1818
1919 /// <summary>
2020 /// Creates a new instance of <see cref="ValidatableTypeInfo"/>.
@@ -28,9 +28,15 @@ protected ValidatableTypeInfo(
2828 Type = type ;
2929 Members = members ;
3030 _membersCount = members . Count ;
31- _subTypes = type . GetAllImplementedTypes ( ) ;
31+ _superTypes = type . GetAllImplementedTypes ( ) ;
3232 }
3333
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+
3440 /// <summary>
3541 /// The type being validated.
3642 /// </summary>
@@ -59,75 +65,139 @@ public virtual async Task ValidateAsync(object? value, ValidateContext context,
5965 }
6066
6167 var originalPrefix = context . CurrentValidationPath ;
68+ var originalErrorCount = context . ValidationErrors ? . Count ?? 0 ;
6269
6370 try
6471 {
72+ // First validate direct members
73+ await ValidateMembersAsync ( value , context , cancellationToken ) ;
74+
6575 var actualType = value . GetType ( ) ;
6676
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
69114 {
70115 await Members [ i ] . ValidateAsync ( value , context , cancellationToken ) ;
116+
117+ }
118+ finally
119+ {
71120 context . CurrentValidationPath = originalPrefix ;
72121 }
122+ }
123+ }
124+
125+ private void ValidateTypeAttributes ( object ? value , ValidateContext context )
126+ {
127+ var validationAttributes = GetValidationAttributes ( ) ;
128+ var errorPrefix = context . CurrentValidationPath ;
73129
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 )
76135 {
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 )
80138 {
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 ) ;
86147 }
87148 }
149+ }
150+ }
88151
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 )
91168 {
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 )
103170 {
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 )
105173 {
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 ) ;
120176 }
121- }
122177
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+ }
126184 }
185+
186+ // Restore the original validation context properties
187+ context . ValidationContext . DisplayName = originalDisplayName ;
188+ context . ValidationContext . MemberName = originalMemberName ;
127189 }
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 ) ) )
129195 {
130- context . CurrentValidationPath = originalPrefix ;
196+ if ( context . ValidationOptions . TryGetValidatableTypeInfo ( superType , out var found )
197+ && found is ValidatableTypeInfo superTypeInfo )
198+ {
199+ yield return superTypeInfo ;
200+ }
131201 }
132202 }
133203}
0 commit comments