@@ -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>
@@ -62,72 +68,112 @@ public virtual async Task ValidateAsync(object? value, ValidateContext context,
6268
6369 try
6470 {
71+ // First validate direct members
72+ await ValidateMembersAsync ( value , context , cancellationToken ) ;
73+
6574 var actualType = value . GetType ( ) ;
6675
67- // First validate members
68- for ( var i = 0 ; i < _membersCount ; i ++ )
76+ // Then validate inherited members
77+ foreach ( var superTypeInfo in GetSuperTypeInfos ( actualType , context ) )
6978 {
70- await Members [ i ] . ValidateAsync ( value , context , cancellationToken ) ;
79+ await superTypeInfo . ValidateAsync ( value , context , cancellationToken ) ;
7180 context . CurrentValidationPath = originalPrefix ;
7281 }
7382
74- // Then validate sub-types if any
75- foreach ( var subType in _subTypes )
83+ // If any property-level validation errors were found, return early
84+ if ( context . ValidationErrors is not null && context . ValidationErrors . Count > 0 )
7685 {
77- // Check if the actual type is assignable to the sub-type
78- // and validate it if it is
79- if ( subType . IsAssignableFrom ( actualType ) )
80- {
81- if ( context . ValidationOptions . TryGetValidatableTypeInfo ( subType , out var subTypeInfo ) )
82- {
83- await subTypeInfo . ValidateAsync ( value , context , cancellationToken ) ;
84- context . CurrentValidationPath = originalPrefix ;
85- }
86- }
86+ return ;
8787 }
8888
89- // Finally validate IValidatableObject if implemented
90- if ( Type . ImplementsInterface ( typeof ( IValidatableObject ) ) && value is IValidatableObject validatable )
89+ // Validate direct type-level attributes
90+ ValidateTypeAttributes ( value , context ) ;
91+
92+ // Validate inherited type-level attributes
93+ foreach ( var superTypeInfo in GetSuperTypeInfos ( actualType , context ) )
9194 {
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 ;
95+ superTypeInfo . ValidateTypeAttributes ( value , context ) ;
96+ context . CurrentValidationPath = originalPrefix ;
97+ }
9698
97- // Set the display name to the class name for IValidatableObject validation
98- context . ValidationContext . DisplayName = Type . Name ;
99- context . ValidationContext . MemberName = null ;
99+ // Finally validate IValidatableObject if implemented
100+ ValidateValidatableObjectInterface ( value , context ) ;
101+ }
102+ finally
103+ {
104+ context . CurrentValidationPath = originalPrefix ;
105+ }
106+ }
100107
101- var validationResults = validatable . Validate ( context . ValidationContext ) ;
102- foreach ( var validationResult in validationResults )
108+ private async Task ValidateMembersAsync ( object ? value , ValidateContext context , CancellationToken cancellationToken )
109+ {
110+ var originalPrefix = context . CurrentValidationPath ;
111+
112+ for ( var i = 0 ; i < _membersCount ; i ++ )
113+ {
114+ await Members [ i ] . ValidateAsync ( value , context , cancellationToken ) ;
115+ context . CurrentValidationPath = originalPrefix ;
116+ }
117+ }
118+
119+ private void ValidateTypeAttributes ( object ? value , ValidateContext context )
120+ {
121+ // TODO: Implement this, probably record attributes in SG
122+ }
123+
124+ private void ValidateValidatableObjectInterface ( object ? value , ValidateContext context )
125+ {
126+ if ( Type . ImplementsInterface ( typeof ( IValidatableObject ) ) && value is IValidatableObject validatable )
127+ {
128+ // Important: Set the DisplayName to the type name for top-level validations
129+ // and restore the original validation context properties
130+ var originalPrefix = context . CurrentValidationPath ;
131+ var originalDisplayName = context . ValidationContext . DisplayName ;
132+ var originalMemberName = context . ValidationContext . MemberName ;
133+
134+ // Set the display name to the class name for IValidatableObject validation
135+ context . ValidationContext . DisplayName = Type . Name ;
136+ context . ValidationContext . MemberName = null ;
137+
138+ var validationResults = validatable . Validate ( context . ValidationContext ) ;
139+ foreach ( var validationResult in validationResults )
140+ {
141+ if ( validationResult != ValidationResult . Success && validationResult . ErrorMessage is not null )
103142 {
104- if ( validationResult != ValidationResult . Success && validationResult . ErrorMessage is not null )
143+ // Create a validation error for each member name that is provided
144+ foreach ( var memberName in validationResult . MemberNames )
105145 {
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- }
146+ var key = string . IsNullOrEmpty ( originalPrefix ) ?
147+ memberName :
148+ $ "{ originalPrefix } .{ memberName } ";
149+ context . AddOrExtendValidationError ( memberName , key , validationResult . ErrorMessage , value ) ;
120150 }
121- }
122151
123- // Restore the original validation context properties
124- context . ValidationContext . DisplayName = originalDisplayName ;
125- context . ValidationContext . MemberName = originalMemberName ;
152+ if ( ! validationResult . MemberNames . Any ( ) )
153+ {
154+ // If no member names are specified, then treat this as a top-level error
155+ context . AddOrExtendValidationError ( string . Empty , string . Empty , validationResult . ErrorMessage , value ) ;
156+ }
157+ }
126158 }
159+
160+ // Restore the original validation context properties
161+ context . ValidationContext . DisplayName = originalDisplayName ;
162+ context . ValidationContext . MemberName = originalMemberName ;
127163 }
128- finally
164+ }
165+
166+ private IEnumerable < ValidatableTypeInfo > GetSuperTypeInfos ( Type actualType , ValidateContext context )
167+ {
168+ foreach ( var superType in _superTypes . Where ( t => t . IsAssignableFrom ( actualType ) ) )
129169 {
130- context . CurrentValidationPath = originalPrefix ;
170+ // Check if the actual type is assignable to the super-type
171+ if ( superType . IsAssignableFrom ( actualType )
172+ && context . ValidationOptions . TryGetValidatableTypeInfo ( superType , out var superTypeInfo )
173+ && superTypeInfo is ValidatableTypeInfo superTypeInfoSpecialized )
174+ {
175+ yield return superTypeInfoSpecialized ;
176+ }
131177 }
132178 }
133179}
0 commit comments