Limit the maximum number of Model errors to a reasonable value.

Fixes #490
This commit is contained in:
Pranav K 2014-08-27 18:39:24 -07:00
parent 9befa6e3a2
commit 646c0d704d
22 changed files with 680 additions and 36 deletions

View File

@ -148,7 +148,7 @@ namespace Microsoft.AspNet.Mvc
errorHandler = (sender, e) =>
{
var exception = e.ErrorContext.Error;
context.ActionContext.ModelState.AddModelError(e.ErrorContext.Path, e.ErrorContext.Error);
context.ActionContext.ModelState.TryAddModelError(e.ErrorContext.Path, e.ErrorContext.Error);
// Error must always be marked as handled
// Failure to do so can cause the exception to be rethrown at every recursive level and
// overflow the stack for x64 CLR processes

View File

@ -17,6 +17,7 @@ namespace Microsoft.AspNet.Mvc
{
private AntiForgeryOptions _antiForgeryOptions = new AntiForgeryOptions();
private RazorViewEngineOptions _viewEngineOptions = new RazorViewEngineOptions();
private int _maxModelStateErrors = 200;
public MvcOptions()
{
@ -94,6 +95,25 @@ namespace Microsoft.AspNet.Mvc
}
/// <summary>
/// Gets or sets the maximum number of validation errors that are allowed by this application before further
/// errors are ignored.
/// </summary>
public int MaxModelValidationErrors
{
get { return _maxModelStateErrors; }
set
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
_maxModelStateErrors = value;
}
}
/// <summary>
/// Get a list of the <see cref="ModelBinderDescriptor" /> used by the
/// Gets a list of the <see cref="ModelBinderDescriptor" /> used by the
/// <see cref="ModelBinding.CompositeModelBinder" />.
/// </summary>

View File

@ -11,6 +11,7 @@ using Microsoft.AspNet.Mvc.Logging;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Logging;
using Microsoft.Framework.OptionsModel;
namespace Microsoft.AspNet.Mvc
{
@ -32,7 +33,7 @@ namespace Microsoft.AspNet.Mvc
public async Task RouteAsync([NotNull] RouteContext context)
{
var services = context.HttpContext.RequestServices;
// Verify if AddMvc was done before calling UseMvc
// We use the MvcMarkerService to make sure if all the services were added.
MvcServicesHelper.ThrowIfMvcNotRegistered(services);
@ -64,19 +65,22 @@ namespace Microsoft.AspNet.Mvc
return;
}
if (actionDescriptor.RouteValueDefaults != null)
{
foreach (var kvp in actionDescriptor.RouteValueDefaults)
if (actionDescriptor.RouteValueDefaults != null)
{
if (!context.RouteData.Values.ContainsKey(kvp.Key))
foreach (var kvp in actionDescriptor.RouteValueDefaults)
{
context.RouteData.Values.Add(kvp.Key, kvp.Value);
if (!context.RouteData.Values.ContainsKey(kvp.Key))
{
context.RouteData.Values.Add(kvp.Key, kvp.Value);
}
}
}
}
var actionContext = new ActionContext(context.HttpContext, context.RouteData, actionDescriptor);
var optionsAccessor = services.GetService<IOptionsAccessor<MvcOptions>>();
actionContext.ModelState.MaxAllowedErrors = optionsAccessor.Options.MaxModelValidationErrors;
var contextAccessor = services.GetService<IContextAccessor<ActionContext>>();
using (contextAccessor.SetContextSource(() => actionContext, PreventExchange))
{

View File

@ -65,8 +65,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Only perform validation at the root of the object graph. ValidationNode will recursively walk the graph.
// Ignore ComplexModelDto since it essentially wraps the primary object.
if (newBindingContext.ModelMetadata.ContainerType == null &&
newBindingContext.ModelMetadata.ModelType != typeof(ComplexModelDto))
if (IsBindingAtRootOfObjectGraph(newBindingContext))
{
// run validation and return the model
// If we fell back to an empty prefix above and are dealing with simple types,
@ -92,7 +91,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return true;
}
private async Task<bool> TryBind([NotNull] ModelBindingContext bindingContext)
private async Task<bool> TryBind(ModelBindingContext bindingContext)
{
// TODO: RuntimeHelpers.EnsureSufficientExecutionStack does not exist in the CoreCLR.
// Protects against stack overflow for deeply nested model binding
@ -110,6 +109,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return false;
}
private static bool IsBindingAtRootOfObjectGraph(ModelBindingContext bindingContext)
{
// We're at the root of the object graph if the model does does not have a container.
// This statement is true for complex types at the root twice over - once with the actual model
// and once when when it is represented by a ComplexModelDto. Ignore the latter case.
return bindingContext.ModelMetadata.ContainerType == null &&
bindingContext.ModelMetadata.ModelType != typeof(ComplexModelDto);
}
private static ModelBindingContext CreateNewBindingContext(ModelBindingContext oldBindingContext,
string modelName,
bool reuseValidationNode)

View File

@ -127,10 +127,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// var errorMessage = ModelBinderConfig.ValueRequiredErrorMessageProvider(e.ValidationContext,
// modelMetadata,
// incomingValue);
var errorMessage = "A value is required.";
var errorMessage = Resources.ModelBinderConfig_ValueRequired;
if (errorMessage != null)
{
modelState.AddModelError(validationNode.ModelStateKey, errorMessage);
modelState.TryAddModelError(validationNode.ModelStateKey, errorMessage);
}
}
};
@ -232,7 +232,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// (oddly) succeeded.
if (!addedError)
{
bindingContext.ModelState.AddModelError(
bindingContext.ModelState.TryAddModelError(
modelStateKey,
Resources.FormatMissingRequiredMember(missingRequiredProperty));
}
@ -285,7 +285,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var validationContext = new ModelValidationContext(bindingContext, propertyMetadata);
foreach (var validationResult in requiredValidator.Validate(validationContext))
{
bindingContext.ModelState.AddModelError(modelStateKey, validationResult.Message);
bindingContext.ModelState.TryAddModelError(modelStateKey, validationResult.Message);
}
}
}
@ -337,7 +337,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var addedError = false;
foreach (var validationResult in validator.Validate(validationContext))
{
bindingContext.ModelState.AddModelError(modelStateKey, validationResult.Message);
bindingContext.ModelState.TryAddModelError(modelStateKey, validationResult.Message);
addedError = true;
}

View File

@ -98,11 +98,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Internal
{
if (IsFormatException(ex))
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex.Message);
bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, ex.Message);
}
else
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex);
bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, ex);
}
}

View File

@ -9,53 +9,101 @@ using Microsoft.AspNet.Mvc.ModelBinding.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// Represents the state of an attempt to bind values from an HTTP Request to an action method, which includes
/// validation information.
/// </summary>
public class ModelStateDictionary : IDictionary<string, ModelState>
{
private readonly IDictionary<string, ModelState> _innerDictionary;
/// <summary>
/// Initializes a new instance of the <see cref="ModelStateDictionary"/> class.
/// </summary>
public ModelStateDictionary()
{
_innerDictionary = new Dictionary<string, ModelState>(StringComparer.OrdinalIgnoreCase);
}
/// <summary>
/// Initializes a new instance of the <see cref="ModelStateDictionary"/> class by using values that are copied
/// from the specified <paramref name="dictionary"/>.
/// </summary>
/// <param name="dictionary">The <see cref="ModelStateDictionary"/> to copy values from.</param>
public ModelStateDictionary([NotNull] ModelStateDictionary dictionary)
{
_innerDictionary = new CopyOnWriteDictionary<string, ModelState>(dictionary,
StringComparer.OrdinalIgnoreCase);
MaxAllowedErrors = dictionary.MaxAllowedErrors;
ErrorCount = dictionary.ErrorCount;
HasRecordedMaxModelError = dictionary.HasRecordedMaxModelError;
}
#region IDictionary properties
/// <summary>
/// Gets or sets the maximum allowed errors in this instance of <see cref="ModelStateDictionary"/>.
/// Defaults to <see cref="int.MaxValue"/>.
/// </summary>
/// <remarks>
/// The value of this property is used to track the total number of calls to
/// <see cref="AddModelError(string, Exception)"/> and <see cref="AddModelError(string, string)"/> after which
/// an error is thrown for further invocations. Errors added via modifying <see cref="ModelState"/> do not
/// count towards this limit.
/// </remarks>
public int MaxAllowedErrors { get; set; } = int.MaxValue;
/// <summary>
/// Gets a flag that determines if the total number of added errors (given by <see cref="ErrorCount"/>) is
/// fewer than <see cref="MaxAllowedErrors"/>.
/// </summary>
public bool CanAddErrors
{
get { return ErrorCount < MaxAllowedErrors; }
}
/// <summary>
/// Gets the number of errors added to this instance of <see cref="ModelStateDictionary"/> via
/// <see cref="AddModelError(string, Exception)"/> and <see cref="AddModelError(string, string)"/>.
/// </summary>
public int ErrorCount { get; private set; }
/// <inheritdoc />
public int Count
{
get { return _innerDictionary.Count; }
}
/// <inheritdoc />
public bool IsReadOnly
{
get { return _innerDictionary.IsReadOnly; }
}
/// <inheritdoc />
public ICollection<string> Keys
{
get { return _innerDictionary.Keys; }
}
/// <inheritdoc />
public ICollection<ModelState> Values
{
get { return _innerDictionary.Values; }
}
#endregion
/// <inheritdoc />
public bool IsValid
{
get { return ValidationState == ModelValidationState.Valid; }
}
/// <inheritdoc />
public ModelValidationState ValidationState
{
get { return GetValidity(_innerDictionary); }
}
/// <inheritdoc />
public ModelState this[[NotNull] string key]
{
get
@ -80,20 +128,94 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
get { return _innerDictionary; }
}
// Flag that indiciates if TooManyModelErrorException has already been added to this dictionary.
private bool HasRecordedMaxModelError { get; set; }
/// <summary>
/// Adds the specified <paramref name="exception"/> to the <see cref="ModelState.Errors"/> instance
/// that is associated with the specified <paramref name="key"/>.
/// </summary>
/// <param name="key">The key of the <see cref="ModelState"/> to add errors to.</param>
/// <param name="exception">The <see cref="Exception"/> to add.</param>
public void AddModelError([NotNull] string key, [NotNull] Exception exception)
{
var modelState = GetModelStateForKey(key);
modelState.ValidationState = ModelValidationState.Invalid;
modelState.Errors.Add(exception);
TryAddModelError(key, exception);
}
/// <summary>
/// Attempts to add the specified <paramref name="exception"/> to the <see cref="ModelState.Errors"/>
/// instance that is associated with the specified <paramref name="key"/>. If the maximum number of allowed
/// errors has already been recorded, records a <see cref="TooManyModelErrorsException"/> exception instead.
/// </summary>
/// <param name="key">The key of the <see cref="ModelState"/> to add errors to.</param>
/// <param name="exception">The <see cref="Exception"/> to add.</param>
/// <returns>True if the error was added, false if the dictionary has already recorded
/// at least <see cref="MaxAllowedErrors"/> number of errors.</returns>
/// <remarks>
/// This method only allows adding up to <see cref="MaxAllowedErrors"/> - 1. <see cref="MaxAllowedErrors"/>nt
/// invocation would result in adding a <see cref="TooManyModelErrorsException"/> to the dictionary.
/// </remarks>
public bool TryAddModelError([NotNull] string key, [NotNull] Exception exception)
{
if (ErrorCount >= MaxAllowedErrors - 1)
{
EnsureMaxErrorsReachedRecorded();
return false;
}
ErrorCount++;
AddModelErrorCore(key, exception);
return true;
}
/// <summary>
/// Adds the specified <paramref name="errorMessage"/> to the <see cref="ModelState.Errors"/> instance
/// that is associated with the specified <paramref name="key"/>.
/// </summary>
/// <param name="key">The key of the <see cref="ModelState"/> to add errors to.</param>
/// <param name="errorMessage">The error message to add.</param>
public void AddModelError([NotNull] string key, [NotNull] string errorMessage)
{
TryAddModelError(key, errorMessage);
}
/// <summary>
/// Attempts to add the specified <paramref name="errorMessage"/> to the <see cref="ModelState.Errors"/>
/// instance that is associated with the specified <paramref name="key"/>. If the maximum number of allowed
/// errors has already been recorded, records a <see cref="TooManyModelErrorsException"/> exception instead.
/// </summary>
/// <param name="key">The key of the <see cref="ModelState"/> to add errors to.</param>
/// <param name="errorMessage">The error message to add.</param>
/// <returns>True if the error was added, false if the dictionary has already recorded
/// at least <see cref="MaxAllowedErrors"/> number of errors.</returns>
/// <remarks>
/// This method only allows adding up to <see cref="MaxAllowedErrors"/> - 1. <see cref="MaxAllowedErrors"/>nt
/// invocation would result in adding a <see cref="TooManyModelErrorsException"/> to the dictionary.
/// </remarks>
public bool TryAddModelError([NotNull] string key, [NotNull] string errorMessage)
{
if (ErrorCount >= MaxAllowedErrors - 1)
{
EnsureMaxErrorsReachedRecorded();
return false;
}
ErrorCount++;
var modelState = GetModelStateForKey(key);
modelState.ValidationState = ModelValidationState.Invalid;
modelState.Errors.Add(errorMessage);
return true;
}
/// <summary>
/// Returns the aggregate <see cref="ModelValidationState"/> for items starting with the
/// specified <paramref name="key"/>.
/// </summary>
/// <param name="key">The key to look up model state errors for.</param>
/// <returns>Returns <see cref="ModelValidationState.Unvalidated"/> if no entries are found for the specified
/// key, <see cref="ModelValidationState.Invalid"/> if at least one instance is found with one or more model
/// state errors; <see cref="ModelValidationState.Valid"/> otherwise.</returns>
public ModelValidationState GetFieldValidationState([NotNull] string key)
{
var entries = DictionaryHelper.FindKeysWithPrefix(this, key);
@ -105,6 +227,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return GetValidity(entries);
}
/// <summary>
/// Marks the <see cref="ModelState.ValidationState"/> for the entry with the specified <paramref name="key"/>
/// as <see cref="ModelValidationState.Valid"/>.
/// </summary>
/// <param name="key">The key of the <see cref="ModelState"/> to mark as valid.</param>
public void MarkFieldValid([NotNull] string key)
{
var modelState = GetModelStateForKey(key);
@ -116,6 +243,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
modelState.ValidationState = ModelValidationState.Valid;
}
/// <summary>
/// Copies the values from the specified <paramref name="dictionary"/> into this instance, overwriting
/// existing values if keys are the same.
/// </summary>
/// <param name="dictionary">The <see cref="ModelStateDictionary"/> to copy values from.</param>
public void Merge(ModelStateDictionary dictionary)
{
if (dictionary == null)
@ -129,6 +261,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
}
/// <summary>
/// Sets the value for the <see cref="ModelState"/> with the specified <paramref name="key"/> to the
/// specified <paramref name="value"/>.
/// </summary>
/// <param name="key">The key for the <see cref="ModelState"/> entry.</param>
/// <param name="value">The value to assign.</param>
public void SetModelValue([NotNull] string key, [NotNull] ValueProviderResult value)
{
GetModelStateForKey(key).Value = value;
@ -165,61 +303,88 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return validationState;
}
#region IDictionary members
private void EnsureMaxErrorsReachedRecorded()
{
if (!HasRecordedMaxModelError)
{
var exception = new TooManyModelErrorsException(Resources.ModelStateDictionary_MaxModelStateErrors);
AddModelErrorCore(string.Empty, exception);
HasRecordedMaxModelError = true;
ErrorCount++;
}
}
private void AddModelErrorCore(string key, Exception exception)
{
var modelState = GetModelStateForKey(key);
modelState.ValidationState = ModelValidationState.Invalid;
modelState.Errors.Add(exception);
}
/// <inheritdoc />
public void Add(KeyValuePair<string, ModelState> item)
{
Add(item.Key, item.Value);
}
/// <inheritdoc />
public void Add([NotNull] string key, [NotNull] ModelState value)
{
_innerDictionary.Add(key, value);
}
/// <inheritdoc />
public void Clear()
{
_innerDictionary.Clear();
}
/// <inheritdoc />
public bool Contains(KeyValuePair<string, ModelState> item)
{
return _innerDictionary.Contains(item);
}
/// <inheritdoc />
public bool ContainsKey([NotNull] string key)
{
return _innerDictionary.ContainsKey(key);
}
/// <inheritdoc />
public void CopyTo([NotNull] KeyValuePair<string, ModelState>[] array, int arrayIndex)
{
_innerDictionary.CopyTo(array, arrayIndex);
}
/// <inheritdoc />
public bool Remove(KeyValuePair<string, ModelState> item)
{
return _innerDictionary.Remove(item);
}
/// <inheritdoc />
public bool Remove([NotNull] string key)
{
return _innerDictionary.Remove(key);
}
/// <inheritdoc />
public bool TryGetValue([NotNull] string key, out ModelState value)
{
return _innerDictionary.TryGetValue(key, out value);
}
/// <inheritdoc />
public IEnumerator<KeyValuePair<string, ModelState>> GetEnumerator()
{
return _innerDictionary.GetEnumerator();
}
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
#endregion
}
}

View File

@ -394,6 +394,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return string.Format(CultureInfo.CurrentCulture, GetString("DataAnnotationsModelMetadataProvider_UnreadableProperty"), p0, p1);
}
/// <summary>
/// The maximum number of allowed model errors has been reached.
/// </summary>
internal static string ModelStateDictionary_MaxModelStateErrors
{
get { return GetString("ModelStateDictionary_MaxModelStateErrors"); }
}
/// <summary>
/// The maximum number of allowed model errors has been reached.
/// </summary>
internal static string FormatModelStateDictionary_MaxModelStateErrors()
{
return GetString("ModelStateDictionary_MaxModelStateErrors");
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -189,4 +189,7 @@
<data name="DataAnnotationsModelMetadataProvider_UnreadableProperty" xml:space="preserve">
<value>{0} has a DisplayColumn attribute for {1}, but property {1} does not have a public 'get' method.</value>
</data>
<data name="ModelStateDictionary_MaxModelStateErrors" xml:space="preserve">
<value>The maximum number of allowed model errors has been reached.</value>
</data>
</root>

View File

@ -0,0 +1,23 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// The <see cref="Exception"/> that is thrown when too many model errors are encountered.
/// </summary>
public class TooManyModelErrorsException : Exception
{
/// <summary>
/// Creates a new instance of <see cref="TooManyModelErrorsException"/> with the specified
/// exception <paramref name="message"/>.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public TooManyModelErrorsException([NotNull] string message)
: base(message)
{
}
}
}

View File

@ -102,9 +102,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public void Validate([NotNull] ModelValidationContext validationContext, ModelValidationNode parentNode)
{
if (SuppressValidation)
if (SuppressValidation || !validationContext.ModelState.CanAddErrors)
{
// no-op
// Short circuit if validation does not need to be applied or if we've reached the max number of validation errors.
return;
}
@ -171,7 +171,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
var thisErrorKey = ModelBindingHelper.CreatePropertyModelName(propertyKeyRoot,
propertyResult.MemberName);
modelState.AddModelError(thisErrorKey, propertyResult.Message);
modelState.TryAddModelError(thisErrorKey, propertyResult.Message);
}
}
}
@ -194,7 +194,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
ModelMetadata.GetDisplayName());
if (parentNode == null && ModelMetadata.Model == null)
{
modelState.AddModelError(modelStateKey, Resources.Validation_ValueNotFound);
modelState.TryAddModelError(modelStateKey, Resources.Validation_ValueNotFound);
return;
}
@ -207,7 +207,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
var currentModelStateKey = ModelBindingHelper.CreatePropertyModelName(ModelStateKey,
validationResult.MemberName);
modelState.AddModelError(currentModelStateKey, validationResult.Message);
modelState.TryAddModelError(currentModelStateKey, validationResult.Message);
}
}
}

View File

@ -144,6 +144,30 @@ namespace Microsoft.AspNet.Mvc
actionContext.ModelState["Age"].Errors[0].Exception.Message);
}
[Fact]
public async Task ReadAsync_UsesTryAddModelValidationErrorsToModelState_WhenCaptureErrorsIsSet()
{
// Arrange
var content = "{name: 'Person Name', Age: 'not-an-age'}";
var formatter = new JsonInputFormatter { CaptureDeserilizationErrors = true };
var contentBytes = Encoding.UTF8.GetBytes(content);
var actionContext = GetActionContext(contentBytes);
var metadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(User));
var context = new InputFormatterContext(actionContext, metadata.ModelType);
actionContext.ModelState.MaxAllowedErrors = 3;
actionContext.ModelState.AddModelError("key1", "error1");
actionContext.ModelState.AddModelError("key2", "error2");
// Act
var model = await formatter.ReadAsync(context);
// Assert
Assert.False(actionContext.ModelState.ContainsKey("age"));
var error = Assert.Single(actionContext.ModelState[""].Errors);
Assert.IsType<TooManyModelErrorsException>(error.Exception);
}
private static ActionContext GetActionContext(byte[] contentBytes,
string contentType = "application/xml")
{

View File

@ -16,8 +16,19 @@ namespace Microsoft.AspNet.Mvc.Core.Test
// Act & Assert
var ex = Assert.Throws<ArgumentNullException>(() => options.AntiForgeryOptions = null);
Assert.Equal("The 'AntiForgeryOptions' property of 'Microsoft.AspNet.Mvc.MvcOptions' must not be null." +
Assert.Equal("The 'AntiForgeryOptions' property of 'Microsoft.AspNet.Mvc.MvcOptions' must not be null." +
"\r\nParameter name: value", ex.Message);
}
[Fact]
public void MaxValidationError_ThrowsIfValueIsOutOfRange()
{
// Arrange
var options = new MvcOptions();
// Act & Assert
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => options.MaxModelValidationErrors = -1);
Assert.Equal("value", ex.ParamName);
}
}
}

View File

@ -10,6 +10,7 @@ using Microsoft.AspNet.Mvc.Logging;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Logging;
using Microsoft.Framework.OptionsModel;
using Moq;
using Xunit;
@ -18,7 +19,7 @@ namespace Microsoft.AspNet.Mvc
public class MvcRouteHandlerTests
{
[Fact]
public async void RouteAsync_Success_LogsCorrectValues()
public async Task RouteAsync_Success_LogsCorrectValues()
{
// Arrange
var sink = new TestSink();
@ -56,7 +57,7 @@ namespace Microsoft.AspNet.Mvc
}
[Fact]
public async void RouteAsync_FailOnNoAction_LogsCorrectValues()
public async Task RouteAsync_FailOnNoAction_LogsCorrectValues()
{
// Arrange
var sink = new TestSink();
@ -100,7 +101,7 @@ namespace Microsoft.AspNet.Mvc
}
[Fact]
public async void RouteAsync_FailOnNoInvoker_LogsCorrectValues()
public async Task RouteAsync_FailOnNoInvoker_LogsCorrectValues()
{
// Arrange
var sink = new TestSink();
@ -144,10 +145,47 @@ namespace Microsoft.AspNet.Mvc
Assert.Equal(false, values.Handled);
}
[Fact]
public async Task RouteAsync_SetsMaxErrorCountOnModelStateDictionary()
{
// Arrange
var expected = 199;
var optionsAccessor = new Mock<IOptionsAccessor<MvcOptions>>();
var options = new MvcOptions
{
MaxModelValidationErrors = expected
};
optionsAccessor.SetupGet(o => o.Options)
.Returns(options);
var invoked = false;
var mockInvokerFactory = new Mock<IActionInvokerFactory>();
mockInvokerFactory.Setup(f => f.CreateInvoker(It.IsAny<ActionContext>()))
.Callback<ActionContext>(c =>
{
Assert.Equal(expected, c.ModelState.MaxAllowedErrors);
invoked = true;
})
.Returns(Mock.Of<IActionInvoker>());
var context = CreateRouteContext(
invokerFactory: mockInvokerFactory.Object,
optionsAccessor: optionsAccessor.Object);
var handler = new MvcRouteHandler();
// Act
await handler.RouteAsync(context);
// Assert
Assert.True(invoked);
}
private RouteContext CreateRouteContext(
IActionSelector actionSelector = null,
IActionInvokerFactory invokerFactory = null,
ILoggerFactory loggerFactory = null)
ILoggerFactory loggerFactory = null,
IOptionsAccessor<MvcOptions> optionsAccessor = null)
{
var mockContextAccessor = new Mock<IContextAccessor<ActionContext>>();
mockContextAccessor.Setup(c => c.SetContextSource(
@ -185,6 +223,15 @@ namespace Microsoft.AspNet.Mvc
loggerFactory = NullLoggerFactory.Instance;
}
if (optionsAccessor == null)
{
var mockOptionsAccessor = new Mock<IOptionsAccessor<MvcOptions>>();
mockOptionsAccessor.SetupGet(o => o.Options)
.Returns(new MvcOptions());
optionsAccessor = mockOptionsAccessor.Object;
}
var httpContext = new Mock<HttpContext>();
httpContext.Setup(h => h.RequestServices.GetService(typeof(IContextAccessor<ActionContext>)))
.Returns(mockContextAccessor.Object);
@ -196,6 +243,8 @@ namespace Microsoft.AspNet.Mvc
.Returns(loggerFactory);
httpContext.Setup(h => h.RequestServices.GetService(typeof(IEnumerable<MvcMarkerService>)))
.Returns(new List<MvcMarkerService> { new MvcMarkerService() });
httpContext.Setup(h => h.RequestServices.GetService(typeof(IOptionsAccessor<MvcOptions>)))
.Returns(optionsAccessor);
return new RouteContext(httpContext.Object);
}

View File

@ -2,10 +2,13 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.TestHost;
using Newtonsoft.Json;
using Xunit;
namespace Microsoft.AspNet.Mvc.FunctionalTests
@ -44,5 +47,48 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("\0", await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task ModelBinding_LimitsErrorsToMaxErrorCount()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
var queryString = string.Join("=&", Enumerable.Range(0, 10).Select(i => "field" + i));
// Act
var response = await client.GetStringAsync("http://localhost/Home/ModelWithTooManyValidationErrors?" + queryString);
//Assert
var json = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
// 8 is the value of MaxModelValidationErrors for the application being tested.
Assert.Equal(8, json.Count);
Assert.Equal("The Field1 field is required.", json["Field1.Field1"]);
Assert.Equal("The Field2 field is required.", json["Field1.Field2"]);
Assert.Equal("The Field3 field is required.", json["Field1.Field3"]);
Assert.Equal("The Field1 field is required.", json["Field2.Field1"]);
Assert.Equal("The Field2 field is required.", json["Field2.Field2"]);
Assert.Equal("The Field3 field is required.", json["Field2.Field3"]);
Assert.Equal("The Field1 field is required.", json["Field3.Field1"]);
Assert.Equal("The maximum number of allowed model errors has been reached.", json[""]);
}
[Fact]
public async Task ModelBinding_ValidatesAllPropertiesInModel()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetStringAsync("http://localhost/Home/ModelWithFewValidationErrors?model=");
//Assert
var json = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
Assert.Equal(3, json.Count);
Assert.Equal("The Field1 field is required.", json["model.Field1"]);
Assert.Equal("The Field2 field is required.", json["model.Field2"]);
Assert.Equal("The Field3 field is required.", json["model.Field3"]);
}
}
}

View File

@ -266,6 +266,37 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.Equal("Password does not meet complexity requirements.", error.ErrorMessage);
}
[Fact]
public async Task BindModel_UsesTryAddModelError()
{
// Arrange
var validatorProvider = new DataAnnotationsModelValidatorProvider();
var binder = CreateBinderWithDefaults();
var valueProvider = new SimpleHttpValueProvider
{
{ "user.password", "password" },
{ "user.confirmpassword", "password2" },
};
var bindingContext = CreateBindingContext(binder, valueProvider, typeof(User), validatorProvider);
bindingContext.ModelState.MaxAllowedErrors = 2;
bindingContext.ModelState.AddModelError("key1", "error1");
bindingContext.ModelName = "user";
// Act
await binder.BindModelAsync(bindingContext);
// Assert
var modelState = bindingContext.ModelState["user.confirmpassword"];
Assert.Empty(modelState.Errors);
modelState = bindingContext.ModelState["user"];
Assert.Empty(modelState.Errors);
var error = Assert.Single(bindingContext.ModelState[""].Errors);
Assert.IsType<TooManyModelErrorsException>(error.Exception);
}
private static ModelBindingContext CreateBindingContext(IModelBinder binder,
IValueProvider valueProvider,
Type type,

View File

@ -26,6 +26,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var target = new ModelStateDictionary(source);
// Assert
Assert.Equal(0, target.ErrorCount);
Assert.Equal(1, target.Count);
Assert.Same(modelState, target["key"]);
Assert.IsType<CopyOnWriteDictionary<string, ModelState>>(target.InnerDictionary);
@ -41,6 +42,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
dictionary.AddModelError("some key", "some error");
// Assert
Assert.Equal(1, dictionary.ErrorCount);
var kvp = Assert.Single(dictionary);
Assert.Equal("some key", kvp.Key);
var error = Assert.Single(kvp.Value.Errors);
@ -59,6 +61,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
dictionary.AddModelError("some key", ex);
// Assert
Assert.Equal(2, dictionary.ErrorCount);
var kvp = Assert.Single(dictionary);
Assert.Equal("some key", kvp.Key);
@ -365,6 +368,137 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.Equal(ModelValidationState.Valid, validationState);
}
[Fact]
public void AddModelError_WithErrorString_AddsTooManyModelErrors_WhenMaxErrorsIsReached()
{
// Arrange
var expected = "The maximum number of allowed model errors has been reached.";
var dictionary = new ModelStateDictionary
{
MaxAllowedErrors = 5
};
dictionary.AddModelError("key1", "error1");
dictionary.AddModelError("key2", new Exception());
dictionary.AddModelError("key3", new Exception());
dictionary.AddModelError("key4", "error4");
dictionary.AddModelError("key5", "error5");
dictionary.AddModelError("key6", "error6");
// Act and Assert
Assert.False(dictionary.CanAddErrors);
Assert.Equal(5, dictionary.ErrorCount);
var error = Assert.Single(dictionary[""].Errors);
Assert.IsType<TooManyModelErrorsException>(error.Exception);
Assert.Equal(expected, error.Exception.Message);
}
[Fact]
public void TryAddModelError_WithErrorString_ReturnsFalse_AndAddsMaxModelErrorMessage()
{
// Arrange
var expected = "The maximum number of allowed model errors has been reached.";
var dictionary = new ModelStateDictionary
{
MaxAllowedErrors = 3
};
// Act and Assert
var result = dictionary.TryAddModelError("key1", "error1");
Assert.True(result);
result = dictionary.TryAddModelError("key2", new Exception());
Assert.True(result);
result = dictionary.TryAddModelError("key3", "error3");
Assert.False(result);
result = dictionary.TryAddModelError("key4", "error4");
Assert.False(result);
Assert.False(dictionary.CanAddErrors);
Assert.Equal(3, dictionary.ErrorCount);
Assert.Equal(3, dictionary.Count);
var error = Assert.Single(dictionary[""].Errors);
Assert.IsType<TooManyModelErrorsException>(error.Exception);
Assert.Equal(expected, error.Exception.Message);
}
[Fact]
public void AddModelError_WithException_AddsTooManyModelError_WhenMaxErrorIsReached()
{
// Arrange
var expected = "The maximum number of allowed model errors has been reached.";
var dictionary = new ModelStateDictionary
{
MaxAllowedErrors = 4
};
dictionary.AddModelError("key1", new Exception());
dictionary.AddModelError("key2", "error2");
dictionary.AddModelError("key3", "error3");
dictionary.AddModelError("key3", new Exception());
dictionary.AddModelError("key4", new InvalidOperationException());
dictionary.AddModelError("key5", new FormatException());
// Act and Assert
Assert.False(dictionary.CanAddErrors);
Assert.Equal(4, dictionary.ErrorCount);
Assert.Equal(4, dictionary.Count);
var error = Assert.Single(dictionary[""].Errors);
Assert.IsType<TooManyModelErrorsException>(error.Exception);
Assert.Equal(expected, error.Exception.Message);
}
[Fact]
public void TryAddModelError_WithException_ReturnsFalse_AndAddsMaxModelErrorMessage()
{
// Arrange
var expected = "The maximum number of allowed model errors has been reached.";
var dictionary = new ModelStateDictionary
{
MaxAllowedErrors = 3
};
// Act and Assert
var result = dictionary.TryAddModelError("key1", "error1");
Assert.True(result);
result = dictionary.TryAddModelError("key2", new Exception());
Assert.True(result);
result = dictionary.TryAddModelError("key3", new Exception());
Assert.False(result);
Assert.Equal(3, dictionary.Count);
var error = Assert.Single(dictionary[""].Errors);
Assert.IsType<TooManyModelErrorsException>(error.Exception);
Assert.Equal(expected, error.Exception.Message);
}
[Fact]
public void ModelStateDictionary_TracksAddedErrorsOverCopyConstructor()
{
// Arrange
var expected = "The maximum number of allowed model errors has been reached.";
var dictionary = new ModelStateDictionary
{
MaxAllowedErrors = 3
};
// Act
dictionary.AddModelError("key1", "error1");
dictionary.TryAddModelError("key3", new Exception());
var copy = new ModelStateDictionary(dictionary);
copy.AddModelError("key2", "error2");
// Assert
Assert.Equal(3, copy.Count);
var error = Assert.Single(copy[""].Errors);
Assert.IsType<TooManyModelErrorsException>(error.Exception);
Assert.Equal(expected, error.Exception.Message);
}
private static ValueProviderResult GetValueProviderResult(object rawValue = null, string attemptedValue = null)
{
return new ValueProviderResult(rawValue ?? "some value",

View File

@ -278,6 +278,39 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.False(context.ModelState.ContainsKey("theKey"));
}
[Fact]
[ReplaceCulture]
public void Validate_ShortCircuits_IfModelStateHasReachedMaxNumberOfErrors()
{
// Arrange
var model = new ValidateAllPropertiesModel
{
RequiredString = null /* error */,
RangedInt = 0 /* error */,
ValidString = "cat" /* error */
};
var modelMetadata = GetModelMetadata(model);
var node = new ModelValidationNode(modelMetadata, "theKey")
{
ValidateAllProperties = true
};
var context = CreateContext(modelMetadata);
context.ModelState.MaxAllowedErrors = 3;
context.ModelState.AddModelError("somekey", "error text");
// Act
node.Validate(context);
// Assert
Assert.Equal(3, context.ModelState.Count);
Assert.IsType<TooManyModelErrorsException>(context.ModelState[""].Errors[0].Exception);
Assert.Equal("The RequiredString field is required.",
context.ModelState["theKey.RequiredString"].Errors[0].ErrorMessage);
Assert.False(context.ModelState.ContainsKey("theKey.RangedInt"));
Assert.False(context.ModelState.ContainsKey("theKey.ValidString"));
}
private static ModelMetadata GetModelMetadata()
{
return new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(object));

View File

@ -1,7 +1,10 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNet.Mvc;
using System.Linq;
using ModelBindingWebSite.Models;
namespace ModelBindingWebSite.Controllers
{
@ -12,5 +15,32 @@ namespace ModelBindingWebSite.Controllers
{
return Content(System.Text.Encoding.UTF8.GetString(byteValues));
}
public object ModelWithTooManyValidationErrors(LargeModelWithValidation model)
{
return CreateValidationDictionary();
}
public object ModelWithFewValidationErrors(ModelWithValidation model)
{
return CreateValidationDictionary();
}
private Dictionary<string, string> CreateValidationDictionary()
{
var result = new Dictionary<string, string>();
foreach (var item in ModelState)
{
var error = item.Value.Errors.SingleOrDefault();
if (error != null)
{
var value = error.Exception != null ? error.Exception.Message :
error.ErrorMessage;
result.Add(item.Key, value);
}
}
return result;
}
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.ComponentModel.DataAnnotations;
namespace ModelBindingWebSite.Models
{
public class LargeModelWithValidation
{
[Required]
public ModelWithValidation Field1 { get; set; }
[Required]
public ModelWithValidation Field2 { get; set; }
[Required]
public ModelWithValidation Field3 { get; set; }
[Required]
public ModelWithValidation Field4 { get; set; }
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.ComponentModel.DataAnnotations;
namespace ModelBindingWebSite
{
public class ModelWithValidation
{
[Required]
public string Field1 { get; set; }
[Required]
public string Field2 { get; set; }
[Required]
public string Field3 { get; set; }
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.DependencyInjection;
@ -17,7 +18,11 @@ namespace ModelBindingWebSite
app.UseServices(services =>
{
// Add MVC services to the services container
services.AddMvc(configuration);
services.AddMvc(configuration)
.SetupOptions<MvcOptions>(m =>
{
m.MaxModelValidationErrors = 8;
});
});
// Add MVC to the request pipeline