Skip to content

Commit f544e7c

Browse files
authored
Treat ValidationContext as required in validation resolver APIs (#61854)
* Tolerate null ValidationContext in validation resolver APIs * Make ValidationContext a required property
1 parent b92588d commit f544e7c

File tree

7 files changed

+98
-87
lines changed

7 files changed

+98
-87
lines changed

src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Microsoft.AspNetCore.Http.Validation.ValidateContext.CurrentDepth.set -> void
2424
Microsoft.AspNetCore.Http.Validation.ValidateContext.CurrentValidationPath.get -> string!
2525
Microsoft.AspNetCore.Http.Validation.ValidateContext.CurrentValidationPath.set -> void
2626
Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidateContext() -> void
27-
Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationContext.get -> System.ComponentModel.DataAnnotations.ValidationContext?
27+
Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationContext.get -> System.ComponentModel.DataAnnotations.ValidationContext!
2828
Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationContext.set -> void
2929
Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationErrors.get -> System.Collections.Generic.Dictionary<string!, string![]!>?
3030
Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationErrors.set -> void

src/Http/Http.Abstractions/src/Validation/ValidatableParameterInfo.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System.Collections;
55
using System.ComponentModel.DataAnnotations;
6-
using System.Diagnostics;
76
using System.Diagnostics.CodeAnalysis;
87

98
namespace Microsoft.AspNetCore.Http.Validation;
@@ -60,8 +59,6 @@ protected ValidatableParameterInfo(
6059
/// </remarks>
6160
public virtual async Task ValidateAsync(object? value, ValidateContext context, CancellationToken cancellationToken)
6261
{
63-
Debug.Assert(context.ValidationContext is not null);
64-
6562
// Skip validation if value is null and parameter is optional
6663
if (value == null && ParameterType.IsNullable())
6764
{

src/Http/Http.Abstractions/src/Validation/ValidatablePropertyInfo.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.ComponentModel.DataAnnotations;
5-
using System.Diagnostics;
65
using System.Diagnostics.CodeAnalysis;
76

87
namespace Microsoft.AspNetCore.Http.Validation;
@@ -61,8 +60,6 @@ protected ValidatablePropertyInfo(
6160
/// <inheritdoc />
6261
public virtual async Task ValidateAsync(object? value, ValidateContext context, CancellationToken cancellationToken)
6362
{
64-
Debug.Assert(context.ValidationContext is not null);
65-
6663
var property = DeclaringType.GetProperty(Name) ?? throw new InvalidOperationException($"Property '{Name}' not found on type '{DeclaringType.Name}'.");
6764
var propertyValue = property.GetValue(value);
6865
var validationAttributes = GetValidationAttributes();

src/Http/Http.Abstractions/src/Validation/ValidatableTypeInfo.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.ComponentModel.DataAnnotations;
5-
using System.Diagnostics;
65
using System.Diagnostics.CodeAnalysis;
76
using System.Linq;
87

@@ -45,7 +44,6 @@ protected ValidatableTypeInfo(
4544
/// <inheritdoc />
4645
public virtual async Task ValidateAsync(object? value, ValidateContext context, CancellationToken cancellationToken)
4746
{
48-
Debug.Assert(context.ValidationContext is not null);
4947
if (value == null)
5048
{
5149
return;

src/Http/Http.Abstractions/src/Validation/ValidateContext.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,24 @@ public sealed class ValidateContext
1616
/// Gets or sets the validation context used for validating objects that implement <see cref="IValidatableObject"/> or have <see cref="ValidationAttribute"/>.
1717
/// This context provides access to service provider and other validation metadata.
1818
/// </summary>
19-
public ValidationContext? ValidationContext { get; set; }
19+
/// <remarks>
20+
/// This property should be set by the consumer of the <see cref="IValidatableInfo"/>
21+
/// interface to provide the necessary context for validation. The object should be initialized
22+
/// with the current object being validated, the display name, and the service provider to support
23+
/// the complete set of validation scenarios.
24+
/// </remarks>
25+
/// <example>
26+
/// <code>
27+
/// var validationContext = new ValidationContext(objectToValidate, serviceProvider, items);
28+
/// var validationOptions = serviceProvider.GetService&lt;IOptions&lt;ValidationOptions&gt;&gt;()?.Value;
29+
/// var validateContext = new ValidateContext
30+
/// {
31+
/// ValidationContext = validationContext,
32+
/// ValidationOptions = validationOptions
33+
/// };
34+
/// </code>
35+
/// </example>
36+
public required ValidationContext ValidationContext { get; set; }
2037

2138
/// <summary>
2239
/// Gets or sets the prefix used to identify the current object being validated in a complex object graph.

src/Http/Http.Abstractions/test/Validation/ValidatableTypeInfoTests.cs

Lines changed: 61 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,16 @@ [new RequiredAttribute()])
4141
{ typeof(Address), addressType }
4242
});
4343

44-
var context = new ValidateContext
45-
{
46-
ValidationOptions = validationOptions,
47-
};
48-
4944
var personWithMissingRequiredFields = new Person
5045
{
5146
Age = 150, // Invalid age
5247
Address = new Address() // Missing required City and Street
5348
};
54-
context.ValidationContext = new ValidationContext(personWithMissingRequiredFields);
49+
var context = new ValidateContext
50+
{
51+
ValidationOptions = validationOptions,
52+
ValidationContext = new ValidationContext(personWithMissingRequiredFields)
53+
};
5554

5655
// Act
5756
await personType.ValidateAsync(personWithMissingRequiredFields, context, default);
@@ -96,21 +95,20 @@ [new RequiredAttribute()]),
9695
[])
9796
]);
9897

99-
var context = new ValidateContext
100-
{
101-
ValidationOptions = new TestValidationOptions(new Dictionary<Type, ValidatableTypeInfo>
102-
{
103-
{ typeof(Employee), employeeType }
104-
})
105-
};
106-
10798
var employee = new Employee
10899
{
109100
Name = "John Doe",
110101
Department = "IT",
111102
Salary = -5000 // Negative salary will trigger IValidatableObject validation
112103
};
113-
context.ValidationContext = new ValidationContext(employee);
104+
var context = new ValidateContext
105+
{
106+
ValidationOptions = new TestValidationOptions(new Dictionary<Type, ValidatableTypeInfo>
107+
{
108+
{ typeof(Employee), employeeType }
109+
}),
110+
ValidationContext = new ValidationContext(employee)
111+
};
114112

115113
// Act
116114
await employeeType.ValidateAsync(employee, context, default);
@@ -142,22 +140,21 @@ [new RequiredAttribute()])
142140
[new RangeAttribute(2, 5)])
143141
]);
144142

143+
var car = new Car
144+
{
145+
// Missing Make and Model (required in base type)
146+
Doors = 7 // Invalid number of doors
147+
};
145148
var context = new ValidateContext
146149
{
147150
ValidationOptions = new TestValidationOptions(new Dictionary<Type, ValidatableTypeInfo>
148151
{
149152
{ typeof(Vehicle), baseType },
150153
{ typeof(Car), derivedType }
151-
})
154+
}),
155+
ValidationContext = new ValidationContext(car)
152156
};
153157

154-
var car = new Car
155-
{
156-
// Missing Make and Model (required in base type)
157-
Doors = 7 // Invalid number of doors
158-
};
159-
context.ValidationContext = new ValidationContext(car);
160-
161158
// Act
162159
await derivedType.ValidateAsync(car, context, default);
163160

@@ -203,15 +200,6 @@ [new RequiredAttribute()]),
203200
[])
204201
]);
205202

206-
var context = new ValidateContext
207-
{
208-
ValidationOptions = new TestValidationOptions(new Dictionary<Type, ValidatableTypeInfo>
209-
{
210-
{ typeof(OrderItem), itemType },
211-
{ typeof(Order), orderType }
212-
})
213-
};
214-
215203
var order = new Order
216204
{
217205
OrderNumber = "ORD-12345",
@@ -222,7 +210,15 @@ [new RequiredAttribute()]),
222210
new OrderItem { ProductName = "Another Product", Quantity = 200 /* Invalid quantity */ }
223211
]
224212
};
225-
context.ValidationContext = new ValidationContext(order);
213+
var context = new ValidateContext
214+
{
215+
ValidationOptions = new TestValidationOptions(new Dictionary<Type, ValidatableTypeInfo>
216+
{
217+
{ typeof(OrderItem), itemType },
218+
{ typeof(Order), orderType }
219+
}),
220+
ValidationContext = new ValidationContext(order)
221+
};
226222

227223
// Act
228224
await orderType.ValidateAsync(order, context, default);
@@ -260,20 +256,19 @@ public async Task Validate_HandlesNullValues_Appropriately()
260256
[])
261257
]);
262258

259+
var person = new Person
260+
{
261+
Name = null,
262+
Address = null
263+
};
263264
var context = new ValidateContext
264265
{
265266
ValidationOptions = new TestValidationOptions(new Dictionary<Type, ValidatableTypeInfo>
266267
{
267268
{ typeof(Person), personType }
268-
})
269-
};
270-
271-
var person = new Person
272-
{
273-
Name = null,
274-
Address = null
269+
}),
270+
ValidationContext = new ValidationContext(person)
275271
};
276-
context.ValidationContext = new ValidationContext(person);
277272

278273
// Act
279274
await personType.ValidateAsync(person, context, default);
@@ -305,12 +300,6 @@ [new RequiredAttribute()]),
305300
});
306301
validationOptions.MaxDepth = 3; // Set a small max depth to trigger the limit
307302

308-
var context = new ValidateContext
309-
{
310-
ValidationOptions = validationOptions,
311-
ValidationErrors = []
312-
};
313-
314303
// Create a deep tree with circular references
315304
var rootNode = new TreeNode { Name = "Root" };
316305
var level1 = new TreeNode { Name = "Level1", Parent = rootNode };
@@ -328,7 +317,12 @@ [new RequiredAttribute()]),
328317
// Add a circular reference
329318
level5.Children.Add(rootNode);
330319

331-
context.ValidationContext = new ValidationContext(rootNode);
320+
var context = new ValidateContext
321+
{
322+
ValidationOptions = validationOptions,
323+
ValidationErrors = [],
324+
ValidationContext = new ValidationContext(rootNode)
325+
};
332326

333327
// Act + Assert
334328
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
@@ -349,17 +343,16 @@ public async Task Validate_HandlesCustomValidationAttributes()
349343
CreatePropertyInfo(typeof(Product), typeof(string), "SKU", "SKU", [new RequiredAttribute(), new CustomSkuValidationAttribute()]),
350344
]);
351345

346+
var product = new Product { SKU = "INVALID-SKU" };
352347
var context = new ValidateContext
353348
{
354349
ValidationOptions = new TestValidationOptions(new Dictionary<Type, ValidatableTypeInfo>
355350
{
356351
{ typeof(Product), productType }
357-
})
352+
}),
353+
ValidationContext = new ValidationContext(product)
358354
};
359355

360-
var product = new Product { SKU = "INVALID-SKU" };
361-
context.ValidationContext = new ValidationContext(product);
362-
363356
// Act
364357
await productType.ValidateAsync(product, context, default);
365358

@@ -385,17 +378,16 @@ public async Task Validate_HandlesMultipleErrorsOnSameProperty()
385378
])
386379
]);
387380

381+
var user = new User { Password = "abc" }; // Too short and not complex enough
388382
var context = new ValidateContext
389383
{
390384
ValidationOptions = new TestValidationOptions(new Dictionary<Type, ValidatableTypeInfo>
391385
{
392386
{ typeof(User), userType }
393-
})
387+
}),
388+
ValidationContext = new ValidationContext(user)
394389
};
395390

396-
var user = new User { Password = "abc" }; // Too short and not complex enough
397-
context.ValidationContext = new ValidationContext(user);
398-
399391
// Act
400392
await userType.ValidateAsync(user, context, default);
401393

@@ -429,23 +421,22 @@ public async Task Validate_HandlesMultiLevelInheritance()
429421
CreatePropertyInfo(typeof(DerivedEntity), typeof(string), "Name", "Name", [new RequiredAttribute()])
430422
]);
431423

424+
var entity = new DerivedEntity
425+
{
426+
Name = "", // Invalid: required
427+
CreatedAt = DateTime.Now.AddDays(1) // Invalid: future date
428+
};
432429
var context = new ValidateContext
433430
{
434431
ValidationOptions = new TestValidationOptions(new Dictionary<Type, ValidatableTypeInfo>
435432
{
436433
{ typeof(BaseEntity), baseType },
437434
{ typeof(IntermediateEntity), intermediateType },
438435
{ typeof(DerivedEntity), derivedType }
439-
})
436+
}),
437+
ValidationContext = new ValidationContext(entity)
440438
};
441439

442-
var entity = new DerivedEntity
443-
{
444-
Name = "", // Invalid: required
445-
CreatedAt = DateTime.Now.AddDays(1) // Invalid: future date
446-
};
447-
context.ValidationContext = new ValidationContext(entity);
448-
449440
// Act
450441
await derivedType.ValidateAsync(entity, context, default);
451442

@@ -475,17 +466,16 @@ public async Task Validate_RequiredOnPropertyShortCircuitsOtherValidations()
475466
[new RequiredAttribute(), new PasswordComplexityAttribute()])
476467
]);
477468

469+
var user = new User { Password = null }; // Invalid: required
478470
var context = new ValidateContext
479471
{
480472
ValidationOptions = new TestValidationOptions(new Dictionary<Type, ValidatableTypeInfo>
481473
{
482474
{ typeof(User), userType }
483-
})
475+
}),
476+
ValidationContext = new ValidationContext(user) // Invalid: required
484477
};
485478

486-
var user = new User { Password = null }; // Invalid: required
487-
context.ValidationContext = new ValidationContext(user);
488-
489479
// Act
490480
await userType.ValidateAsync(user, context, default);
491481

@@ -503,18 +493,17 @@ public async Task Validate_IValidatableObject_WithZeroAndMultipleMemberNames_Beh
503493
var globalType = new TestValidatableTypeInfo(
504494
typeof(GlobalErrorObject),
505495
[]); // no properties – nothing sets MemberName
496+
var globalErrorInstance = new GlobalErrorObject { Data = -1 };
506497

507498
var context = new ValidateContext
508499
{
509500
ValidationOptions = new TestValidationOptions(new Dictionary<Type, ValidatableTypeInfo>
510501
{
511502
{ typeof(GlobalErrorObject), globalType }
512-
})
503+
}),
504+
ValidationContext = new ValidationContext(globalErrorInstance)
513505
};
514506

515-
var globalErrorInstance = new GlobalErrorObject { Data = -1 };
516-
context.ValidationContext = new ValidationContext(globalErrorInstance);
517-
518507
await globalType.ValidateAsync(globalErrorInstance, context, default);
519508

520509
Assert.NotNull(context.ValidationErrors);

0 commit comments

Comments
 (0)