Skip to content

Commit 8bf4267

Browse files
committed
Sync latest changes
1 parent 8bb4aa5 commit 8bf4267

File tree

5 files changed

+283
-42
lines changed

5 files changed

+283
-42
lines changed

src/Mvc/Mvc.Abstractions/src/ModelBinding/ModelStateDictionary.cs

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,14 @@ public class ModelStateDictionary : IReadOnlyDictionary<string, ModelStateEntry>
2424
/// </summary>
2525
public static readonly int DefaultMaxAllowedErrors = 200;
2626

27-
private const int DefaultMaxRecursionDepth = 32;
27+
// internal for testing
28+
internal const int DefaultMaxRecursionDepth = 32;
29+
2830
private const char DelimiterDot = '.';
2931
private const char DelimiterOpen = '[';
3032

3133
private readonly ModelStateNode _root;
3234
private int _maxAllowedErrors;
33-
private int? _maxValidationDepth;
3435

3536
/// <summary>
3637
/// Initializes a new instance of the <see cref="ModelStateDictionary"/> class.
@@ -44,17 +45,18 @@ public ModelStateDictionary()
4445
/// Initializes a new instance of the <see cref="ModelStateDictionary"/> class.
4546
/// </summary>
4647
public ModelStateDictionary(int maxAllowedErrors)
47-
: this(maxAllowedErrors, DefaultMaxRecursionDepth)
48+
: this(maxAllowedErrors, maxValidationDepth: DefaultMaxRecursionDepth, maxStateDepth: DefaultMaxRecursionDepth)
4849
{
4950
}
5051

5152
/// <summary>
5253
/// Initializes a new instance of the <see cref="ModelStateDictionary"/> class.
5354
/// </summary>
54-
private ModelStateDictionary(int maxAllowedErrors, int maxValidationDepth)
55+
private ModelStateDictionary(int maxAllowedErrors, int maxValidationDepth, int maxStateDepth)
5556
{
5657
MaxAllowedErrors = maxAllowedErrors;
5758
MaxValidationDepth = maxValidationDepth;
59+
MaxStateDepth = maxStateDepth;
5860
var emptySegment = new StringSegment(buffer: string.Empty);
5961
_root = new ModelStateNode(subKey: emptySegment)
6062
{
@@ -68,7 +70,9 @@ private ModelStateDictionary(int maxAllowedErrors, int maxValidationDepth)
6870
/// </summary>
6971
/// <param name="dictionary">The <see cref="ModelStateDictionary"/> to copy values from.</param>
7072
public ModelStateDictionary(ModelStateDictionary dictionary)
71-
: this(dictionary?.MaxAllowedErrors ?? DefaultMaxAllowedErrors, dictionary?.MaxValidationDepth ?? DefaultMaxRecursionDepth)
73+
: this(dictionary?.MaxAllowedErrors ?? DefaultMaxAllowedErrors,
74+
dictionary?.MaxValidationDepth ?? DefaultMaxRecursionDepth,
75+
dictionary?.MaxStateDepth ?? DefaultMaxRecursionDepth)
7276
{
7377
if (dictionary == null)
7478
{
@@ -184,21 +188,9 @@ public ModelStateEntry this[string key]
184188
// Flag that indicates if TooManyModelErrorException has already been added to this dictionary.
185189
private bool HasRecordedMaxModelError { get; set; }
186190

187-
internal int? MaxValidationDepth
188-
{
189-
get
190-
{
191-
return _maxValidationDepth;
192-
}
193-
set
194-
{
195-
if (value < 0)
196-
{
197-
throw new ArgumentOutOfRangeException(nameof(value));
198-
}
199-
_maxValidationDepth = value;
200-
}
201-
}
191+
internal int? MaxValidationDepth { get; set; }
192+
193+
internal int? MaxStateDepth { get; set; }
202194

203195
/// <summary>
204196
/// Adds the specified <paramref name="exception"/> to the <see cref="ModelStateEntry.Errors"/> instance
@@ -243,7 +235,6 @@ public bool TryAddModelException(string key, Exception exception)
243235
return false;
244236
}
245237

246-
ErrorCount++;
247238
AddModelErrorCore(key, exception);
248239
return true;
249240
}
@@ -352,7 +343,6 @@ public bool TryAddModelError(string key, Exception exception, ModelMetadata meta
352343
return TryAddModelError(key, exception.Message);
353344
}
354345

355-
ErrorCount++;
356346
AddModelErrorCore(key, exception);
357347
return true;
358348
}
@@ -410,13 +400,13 @@ public bool TryAddModelError(string key, string errorMessage)
410400
return false;
411401
}
412402

413-
ErrorCount++;
414403
var modelState = GetOrAddNode(key);
415404
Count += !modelState.IsContainerNode ? 0 : 1;
416405
modelState.ValidationState = ModelValidationState.Invalid;
417406
modelState.MarkNonContainerNode();
418407
modelState.Errors.Add(errorMessage);
419408

409+
ErrorCount++;
420410
return true;
421411
}
422412

@@ -638,11 +628,18 @@ private ModelStateNode GetOrAddNode(string key)
638628
var current = _root;
639629
if (key.Length > 0)
640630
{
631+
var currentDepth = 0;
641632
var match = default(MatchResult);
642633
do
643634
{
635+
if (MaxStateDepth != null && currentDepth >= MaxStateDepth)
636+
{
637+
throw new InvalidOperationException(Resources.FormatModelStateDictionary_MaxModelStateDepth(MaxStateDepth));
638+
}
639+
644640
var subKey = FindNext(key, ref match);
645641
current = current.GetOrAddNode(subKey);
642+
currentDepth++;
646643

647644
} while (match.Type != Delimiter.None);
648645

@@ -742,7 +739,6 @@ private void EnsureMaxErrorsReachedRecorded()
742739
var exception = new TooManyModelErrorsException(Resources.ModelStateDictionary_MaxModelStateErrors);
743740
AddModelErrorCore(string.Empty, exception);
744741
HasRecordedMaxModelError = true;
745-
ErrorCount++;
746742
}
747743
}
748744

@@ -753,6 +749,8 @@ private void AddModelErrorCore(string key, Exception exception)
753749
modelState.ValidationState = ModelValidationState.Invalid;
754750
modelState.MarkNonContainerNode();
755751
modelState.Errors.Add(exception);
752+
753+
ErrorCount++;
756754
}
757755

758756
/// <summary>

src/Mvc/Mvc.Abstractions/src/Resources.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,4 +177,7 @@
177177
<data name="BinderType_MustBeIModelBinder" xml:space="preserve">
178178
<value>The type '{0}' must implement '{1}' to be used as a model binder.</value>
179179
</data>
180+
<data name="ModelStateDictionary_MaxModelStateDepth" xml:space="preserve">
181+
<value>The specified key exceeded the maximum ModelState depth: {0}</value>
182+
</data>
180183
</root>

src/Mvc/Mvc.Abstractions/test/ModelBinding/ModelStateDictionaryTest.cs

Lines changed: 156 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -694,23 +694,6 @@ public void GetFieldValidity_ReturnsUnvalidated_IfAnyItemInSubtreeIsInvalid()
694694
Assert.Equal(ModelValidationState.Unvalidated, validationState);
695695
}
696696

697-
[Fact]
698-
public void GetFieldValidity_ReturnsUnvalidated_IfTreeHeightIsGreaterThanLimit()
699-
{
700-
// Arrange
701-
var dictionary = new ModelStateDictionary();
702-
var stackLimit = 32;
703-
var key = string.Join(".", Enumerable.Repeat("foo", stackLimit + 1));
704-
dictionary.MaxValidationDepth = stackLimit;
705-
dictionary.MarkFieldValid(key);
706-
707-
// Act
708-
var validationState = dictionary.GetFieldValidationState("foo");
709-
710-
// Assert
711-
Assert.Equal(ModelValidationState.Unvalidated, validationState);
712-
}
713-
714697
[Theory]
715698
[InlineData("")]
716699
[InlineData("user")]
@@ -1619,6 +1602,162 @@ public void GetModelStateForProperty_ReturnsModelStateForIndexedChildren()
16191602
Assert.Equal("value1", property.RawValue);
16201603
}
16211604

1605+
[Fact]
1606+
public void GetFieldValidationState_ReturnsUnvalidated_IfTreeHeightIsGreaterThanLimit()
1607+
{
1608+
// Arrange
1609+
var stackLimit = 5;
1610+
var dictionary = new ModelStateDictionary();
1611+
var key = string.Join(".", Enumerable.Repeat("foo", stackLimit + 1));
1612+
dictionary.MaxValidationDepth = stackLimit;
1613+
dictionary.MaxStateDepth = null;
1614+
dictionary.MarkFieldValid(key);
1615+
1616+
// Act
1617+
var validationState = dictionary.GetFieldValidationState("foo");
1618+
1619+
// Assert
1620+
Assert.Equal(ModelValidationState.Unvalidated, validationState);
1621+
}
1622+
1623+
[Fact]
1624+
public void IsValidProperty_ReturnsTrue_IfTreeHeightIsGreaterThanLimit()
1625+
{
1626+
// Arrange
1627+
var stackLimit = 5;
1628+
var dictionary = new ModelStateDictionary();
1629+
var key = string.Join(".", Enumerable.Repeat("foo", stackLimit + 1));
1630+
dictionary.MaxValidationDepth = stackLimit;
1631+
dictionary.MaxStateDepth = null;
1632+
dictionary.AddModelError(key, "some error");
1633+
1634+
// Act
1635+
var isValid = dictionary.IsValid;
1636+
var validationState = dictionary.ValidationState;
1637+
1638+
// Assert
1639+
Assert.True(isValid);
1640+
Assert.Equal(ModelValidationState.Valid, validationState);
1641+
}
1642+
1643+
[Fact]
1644+
public void TryAddModelException_Throws_IfKeyHasTooManySegments()
1645+
{
1646+
// Arrange
1647+
var exception = new TestException();
1648+
1649+
var stateDepth = 5;
1650+
var dictionary = new ModelStateDictionary();
1651+
var key = string.Join(".", Enumerable.Repeat("foo", stateDepth + 1));
1652+
dictionary.MaxStateDepth = stateDepth;
1653+
1654+
// Act
1655+
var invalidException = Assert.Throws<InvalidOperationException>(() => dictionary.TryAddModelException(key, exception));
1656+
1657+
// Assert
1658+
Assert.Equal(
1659+
$"The specified key exceeded the maximum ModelState depth: {dictionary.MaxStateDepth}",
1660+
invalidException.Message);
1661+
}
1662+
1663+
[Fact]
1664+
public void TryAddModelError_Throws_IfKeyHasTooManySegments()
1665+
{
1666+
// Arrange
1667+
var stateDepth = 5;
1668+
var dictionary = new ModelStateDictionary();
1669+
var key = string.Join(".", Enumerable.Repeat("foo", stateDepth + 1));
1670+
dictionary.MaxStateDepth = stateDepth;
1671+
1672+
// Act
1673+
var invalidException = Assert.Throws<InvalidOperationException>(() => dictionary.TryAddModelError(key, "errorMessage"));
1674+
1675+
// Assert
1676+
Assert.Equal(
1677+
$"The specified key exceeded the maximum ModelState depth: {dictionary.MaxStateDepth}",
1678+
invalidException.Message);
1679+
}
1680+
1681+
[Fact]
1682+
public void SetModelValue_Throws_IfKeyHasTooManySegments()
1683+
{
1684+
var stateDepth = 5;
1685+
var dictionary = new ModelStateDictionary();
1686+
var key = string.Join(".", Enumerable.Repeat("foo", stateDepth + 1));
1687+
dictionary.MaxStateDepth = stateDepth;
1688+
1689+
// Act
1690+
var invalidException = Assert.Throws<InvalidOperationException>(() => dictionary.SetModelValue(key, string.Empty, string.Empty));
1691+
1692+
// Assert
1693+
Assert.Equal(
1694+
$"The specified key exceeded the maximum ModelState depth: {dictionary.MaxStateDepth}",
1695+
invalidException.Message);
1696+
}
1697+
1698+
[Fact]
1699+
public void MarkFieldValid_Throws_IfKeyHasTooManySegments()
1700+
{
1701+
// Arrange
1702+
var stateDepth = 5;
1703+
var source = new ModelStateDictionary();
1704+
var key = string.Join(".", Enumerable.Repeat("foo", stateDepth + 1));
1705+
source.MaxStateDepth = stateDepth;
1706+
1707+
// Act
1708+
var exception = Assert.Throws<InvalidOperationException>(() => source.MarkFieldValid(key));
1709+
1710+
// Assert
1711+
Assert.Equal(
1712+
$"The specified key exceeded the maximum ModelState depth: {source.MaxStateDepth}",
1713+
exception.Message);
1714+
}
1715+
1716+
[Fact]
1717+
public void MarkFieldSkipped_Throws_IfKeyHasTooManySegments()
1718+
{
1719+
// Arrange
1720+
var stateDepth = 5;
1721+
var source = new ModelStateDictionary();
1722+
var key = string.Join(".", Enumerable.Repeat("foo", stateDepth + 1));
1723+
source.MaxStateDepth = stateDepth;
1724+
1725+
// Act
1726+
var exception = Assert.Throws<InvalidOperationException>(() => source.MarkFieldSkipped(key));
1727+
1728+
// Assert
1729+
Assert.Equal(
1730+
$"The specified key exceeded the maximum ModelState depth: {source.MaxStateDepth}",
1731+
exception.Message);
1732+
}
1733+
1734+
[Fact]
1735+
public void Constructor_SetsDefaultRecursionDepth()
1736+
{
1737+
// Arrange && Act
1738+
var dictionary = new ModelStateDictionary();
1739+
1740+
// Assert
1741+
Assert.Equal(ModelStateDictionary.DefaultMaxRecursionDepth, dictionary.MaxValidationDepth);
1742+
Assert.Equal(ModelStateDictionary.DefaultMaxRecursionDepth, dictionary.MaxStateDepth);
1743+
}
1744+
1745+
[Fact]
1746+
public void CopyConstructor_PreservesRecursionDepth()
1747+
{
1748+
// Arrange
1749+
var dictionary = new ModelStateDictionary();
1750+
dictionary.MaxValidationDepth = 5;
1751+
dictionary.MaxStateDepth = 4;
1752+
1753+
// Act
1754+
var newDictionary = new ModelStateDictionary(dictionary);
1755+
1756+
// Assert
1757+
Assert.Equal(dictionary.MaxValidationDepth, newDictionary.MaxValidationDepth);
1758+
Assert.Equal(dictionary.MaxStateDepth, newDictionary.MaxStateDepth);
1759+
}
1760+
16221761
private class OptionsAccessor : IOptions<MvcOptions>
16231762
{
16241763
public MvcOptions Value { get; } = new MvcOptions();

src/Mvc/Mvc.Core/src/Infrastructure/ControllerActionInvokerProvider.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ internal class ControllerActionInvokerProvider : IActionInvokerProvider
1919
private readonly IReadOnlyList<IValueProviderFactory> _valueProviderFactories;
2020
private readonly int _maxModelValidationErrors;
2121
private readonly int? _maxValidationDepth;
22+
private readonly int _maxModelBindingRecursionDepth;
2223
private readonly ILogger _logger;
2324
private readonly DiagnosticListener _diagnosticListener;
2425
private readonly IActionResultTypeMapper _mapper;
@@ -46,6 +47,7 @@ public ControllerActionInvokerProvider(
4647
_valueProviderFactories = optionsAccessor.Value.ValueProviderFactories.ToArray();
4748
_maxModelValidationErrors = optionsAccessor.Value.MaxModelValidationErrors;
4849
_maxValidationDepth = optionsAccessor.Value.MaxValidationDepth;
50+
_maxModelBindingRecursionDepth = optionsAccessor.Value.MaxModelBindingRecursionDepth;
4951
_logger = loggerFactory.CreateLogger<ControllerActionInvoker>();
5052
_diagnosticListener = diagnosticListener;
5153
_mapper = mapper;
@@ -70,7 +72,8 @@ public void OnProvidersExecuting(ActionInvokerProviderContext context)
7072
ValueProviderFactories = new CopyOnWriteList<IValueProviderFactory>(_valueProviderFactories)
7173
};
7274
controllerContext.ModelState.MaxAllowedErrors = _maxModelValidationErrors;
73-
controllerContext.ModelState.MaxValidationDepth = _maxModelBindingRecursionDepth;
75+
controllerContext.ModelState.MaxValidationDepth = _maxValidationDepth;
76+
controllerContext.ModelState.MaxStateDepth = _maxModelBindingRecursionDepth;
7477

7578
var (cacheEntry, filters) = _controllerActionInvokerCache.GetCachedResult(controllerContext);
7679

0 commit comments

Comments
 (0)