Skip to content

Commit 12fc3b0

Browse files
committed
Rework validation order
1 parent a5444a7 commit 12fc3b0

File tree

4 files changed

+111
-50
lines changed

4 files changed

+111
-50
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.Extensions.Validation;
5+
6+
namespace BasicTestApp.ValidationModels;
7+
8+
#pragma warning disable ASP0029 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
9+
[ValidatableType]
10+
#pragma warning restore ASP0029 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
11+
public class HumanModel : Person
12+
{
13+
public override bool IsACat => false;
14+
}

src/Components/test/testassets/Components.TestServer/RazorComponents/ValidationModels/PersonModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace BasicTestApp.ValidationModels;
1111
public class Person : IValidatableObject
1212
{
1313
[Required]
14-
public bool IsACat { get; set; }
14+
public virtual bool IsACat { get; set; }
1515

1616
[Range(0, int.MaxValue, ErrorMessage = "Under-zeros should not be filling out forms")]
1717
public int AgeInYears { get; set; }

src/Validation/src/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#nullable enable
2+
abstract Microsoft.Extensions.Validation.ValidatableTypeInfo.GetValidationAttributes() -> System.ComponentModel.DataAnnotations.ValidationAttribute![]!
23
Microsoft.Extensions.DependencyInjection.ValidationServiceCollectionExtensions
34
Microsoft.Extensions.Validation.IValidatableInfo
45
Microsoft.Extensions.Validation.IValidatableInfo.ValidateAsync(object? value, Microsoft.Extensions.Validation.ValidateContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!

src/Validation/src/ValidatableTypeInfo.cs

Lines changed: 95 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ namespace Microsoft.Extensions.Validation;
1414
public 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

Comments
 (0)