Skip to content

Commit 970c924

Browse files
authored
ModelValidation: Fixed MaxDepth exception when there are more items left then MaxValidationDepth after reaching MaxAllowedErrors (#18212)
* Consolidated similar tests in a theory * Fix for InvalidOperationException when reached maxAllowedErrors and there are more items left then max depth * Refactored Visitor logic to reduce the risk of a missing Pop. * Added comment on when it can throw * Updated reference assembly * Reverted publid API change
1 parent 0483e2d commit 970c924

File tree

2 files changed

+42
-23
lines changed

2 files changed

+42
-23
lines changed

src/Mvc/Mvc.Core/src/ModelBinding/Validation/ValidationVisitor.cs

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -223,13 +223,25 @@ protected virtual bool Visit(ModelMetadata metadata, string key, object model)
223223
return true;
224224
}
225225

226+
bool result;
227+
try
228+
{
229+
// Throws InvalidOperationException if the object graph is too deep
230+
result = VisitImplementation(ref metadata, ref key, model);
231+
}
232+
finally
233+
{
234+
_currentPath.Pop(model);
235+
}
236+
return result;
237+
}
238+
239+
private bool VisitImplementation(ref ModelMetadata metadata, ref string key, object model)
240+
{
226241
if (MaxValidationDepth != null && _currentPath.Count > MaxValidationDepth)
227242
{
228243
// Non cyclic but too deep an object graph.
229244

230-
// Pop the current model to make ValidationStack.Dispose happy
231-
_currentPath.Pop(model);
232-
233245
string message;
234246
switch (metadata.MetadataKind)
235247
{
@@ -264,7 +276,6 @@ protected virtual bool Visit(ModelMetadata metadata, string key, object model)
264276
{
265277
// Use the key on the entry, because we might not have entries in model state.
266278
SuppressValidation(entry.Key);
267-
_currentPath.Pop(model);
268279
return true;
269280
}
270281
// If the metadata indicates that no validators exist AND the aggregate state for the key says that the model graph
@@ -282,7 +293,6 @@ protected virtual bool Visit(ModelMetadata metadata, string key, object model)
282293
}
283294
}
284295

285-
_currentPath.Pop(model);
286296
return true;
287297
}
288298

@@ -401,7 +411,6 @@ protected virtual ValidationStateEntry GetValidationEntry(object model)
401411
private readonly string _key;
402412
private readonly ModelMetadata _metadata;
403413
private readonly object _model;
404-
private readonly object _newModel;
405414
private readonly IValidationStrategy _strategy;
406415

407416
public static StateManager Recurse(
@@ -411,7 +420,7 @@ public static StateManager Recurse(
411420
object model,
412421
IValidationStrategy strategy)
413422
{
414-
var recursifier = new StateManager(visitor, model);
423+
var recursifier = new StateManager(visitor, null);
415424

416425
visitor.Container = visitor.Model;
417426
visitor.Key = key;
@@ -425,7 +434,6 @@ public static StateManager Recurse(
425434
public StateManager(ValidationVisitor visitor, object newModel)
426435
{
427436
_visitor = visitor;
428-
_newModel = newModel;
429437

430438
_container = _visitor.Container;
431439
_key = _visitor.Key;
@@ -441,8 +449,6 @@ public void Dispose()
441449
_visitor.Metadata = _metadata;
442450
_visitor.Model = _model;
443451
_visitor.Strategy = _strategy;
444-
445-
_visitor._currentPath.Pop(_newModel);
446452
}
447453
}
448454
}

src/Mvc/Mvc.Core/test/ModelBinding/Validation/DefaultObjectValidatorTests.cs

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System.ComponentModel.DataAnnotations;
77
using System.IO;
88
using System.Linq;
9-
using System.Reflection;
109
using System.Text;
1110
using Microsoft.AspNetCore.Http;
1211
using Microsoft.AspNetCore.Mvc.Infrastructure;
@@ -1264,11 +1263,12 @@ public void Validate_SuppressValidation_AfterHasReachedMaxErrors_Invalid()
12641263
});
12651264
}
12661265

1267-
[Fact]
1268-
public void Validate_Throws_IfValidationDepthExceedsMaxDepth()
1266+
[Theory]
1267+
[InlineData(1)]
1268+
[InlineData(5)]
1269+
public void Validate_Throws_IfValidationDepthExceedsMaxDepth(int maxDepth)
12691270
{
12701271
// Arrange
1271-
var maxDepth = 5;
12721272
var expected = $"ValidationVisitor exceeded the maximum configured validation depth '{maxDepth}' when validating property '{nameof(DepthObject.Depth)}' on type '{typeof(DepthObject)}'. " +
12731273
"This may indicate a very deep or infinitely recursive object graph. Consider modifying 'MvcOptions.MaxValidationDepth' or suppressing validation on the model type.";
12741274
_options.MaxValidationDepth = maxDepth;
@@ -1283,6 +1283,7 @@ public void Validate_Throws_IfValidationDepthExceedsMaxDepth()
12831283
// Act & Assert
12841284
var ex = Assert.Throws<InvalidOperationException>(() => validator.Validate(actionContext, validationState, prefix: string.Empty, model));
12851285
Assert.Equal(expected, ex.Message);
1286+
Assert.Equal("https://aka.ms/AA21ue1", ex.HelpLink);
12861287
}
12871288

12881289
[Fact]
@@ -1305,26 +1306,32 @@ public void Validate_WorksIfObjectGraphIsSmallerThanMaxDepth()
13051306
}
13061307

13071308
[Fact]
1308-
public void Validate_Throws_WithMaxDepth_1()
1309+
public void Validate_DoesNotThrow_IfNumberOfErrorsAfterReachingMaxAllowedErrors_GoesOverMaxDepth()
13091310
{
13101311
// Arrange
1311-
var maxDepth = 1;
1312-
var expected = $"ValidationVisitor exceeded the maximum configured validation depth '{maxDepth}' when validating property '{nameof(DepthObject.Depth)}' on type '{typeof(DepthObject)}'. " +
1313-
"This may indicate a very deep or infinitely recursive object graph. Consider modifying 'MvcOptions.MaxValidationDepth' or suppressing validation on the model type.";
1312+
var maxDepth = 4;
13141313
_options.MaxValidationDepth = maxDepth;
13151314
var actionContext = new ActionContext();
1315+
actionContext.ModelState.MaxAllowedErrors = 2;
13161316
var validator = CreateValidator();
1317-
var model = new DepthObject(maxDepth + 1);
1317+
var model = new List<ModelWithRequiredProperty>
1318+
{
1319+
new ModelWithRequiredProperty(), new ModelWithRequiredProperty(),
1320+
// After the first 2 items we will reach MaxAllowedErrors
1321+
// If we add items without popping after having reached max validation,
1322+
// with 4 more items (on top of the list) we would go over max depth of 4
1323+
new ModelWithRequiredProperty(), new ModelWithRequiredProperty(),
1324+
new ModelWithRequiredProperty(), new ModelWithRequiredProperty(),
1325+
};
1326+
13181327
var validationState = new ValidationStateDictionary
13191328
{
13201329
{ model, new ValidationStateEntry() }
13211330
};
1322-
var method = GetType().GetMethod(nameof(Validate_Throws_ForTopLevelMetadataData), BindingFlags.NonPublic | BindingFlags.Instance);
13231331

13241332
// Act & Assert
1325-
var ex = Assert.Throws<InvalidOperationException>(() => validator.Validate(actionContext, validationState, prefix: string.Empty, model));
1326-
Assert.Equal(expected, ex.Message);
1327-
Assert.NotNull(ex.HelpLink);
1333+
validator.Validate(actionContext, validationState, prefix: string.Empty, model);
1334+
Assert.False(actionContext.ModelState.IsValid);
13281335
}
13291336

13301337
[Theory]
@@ -1443,6 +1450,12 @@ public void Validate_TypeWithoutValidators_DoesNotResetInvalidState()
14431450
Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
14441451
}
14451452

1453+
public class ModelWithRequiredProperty
1454+
{
1455+
[Required]
1456+
public string MyProperty { get; set; }
1457+
}
1458+
14461459
private class ModelWithoutValidation
14471460
{
14481461
public string Property1 { get; set; }

0 commit comments

Comments
 (0)