Add overload for AddModelError without ModelMetadata (#7407)

Addresses #6102
This commit is contained in:
Jass Bagga 2018-03-02 11:03:46 -08:00 committed by GitHub
parent e35d0bc43f
commit 8d1c85ab74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 277 additions and 12 deletions

View File

@ -174,10 +174,54 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// <summary>
/// Adds the specified <paramref name="exception"/> to the <see cref="ModelStateEntry.Errors"/> instance
/// that is associated with the specified <paramref name="key"/>.
/// that is associated with the specified <paramref name="key"/>. If the maximum number of allowed
/// errors has already been recorded, ensures that a <see cref="TooManyModelErrorsException"/> exception is
/// recorded instead.
/// </summary>
/// <remarks>
/// This method allows adding the <paramref name="exception"/> to the current <see cref="ModelStateDictionary"/>
/// when <see cref="ModelMetadata"/> is not available or the exact <paramref name="exception"/>
/// must be maintained for later use (even if it is for example a <see cref="FormatException"/>).
/// Where <see cref="ModelMetadata"/> is available, use <see cref="AddModelError(string, Exception, ModelMetadata)"/> instead.
/// </remarks>
/// <param name="key">The key of the <see cref="ModelStateEntry"/> to add errors to.</param>
/// <param name="exception">The <see cref="Exception"/> to add.</param>
/// <returns>
/// <c>True</c> if the given error was added, <c>false</c> if the error was ignored.
/// See <see cref="MaxAllowedErrors"/>.
/// </returns>
public bool TryAddModelException(string key, Exception exception)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}
if (exception == null)
{
throw new ArgumentNullException(nameof(exception));
}
if (ErrorCount >= MaxAllowedErrors - 1)
{
EnsureMaxErrorsReachedRecorded();
return false;
}
ErrorCount++;
AddModelErrorCore(key, exception);
return true;
}
/// <summary>
/// Adds the specified <paramref name="exception"/> to the <see cref="ModelStateEntry.Errors"/> instance
/// that is associated with the specified <paramref name="key"/>. If the maximum number of allowed
/// errors has already been recorded, ensures that a <see cref="TooManyModelErrorsException"/> exception is
/// recorded instead.
/// </summary>
/// <param name="key">The key of the <see cref="ModelStateEntry"/> to add errors to.</param>
/// <param name="exception">The <see cref="Exception"/> to add. Some exception types will be replaced with
/// a descriptive error message.</param>
/// <param name="metadata">The <see cref="ModelMetadata"/> associated with the model.</param>
public void AddModelError(string key, Exception exception, ModelMetadata metadata)
{
@ -202,10 +246,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// <summary>
/// Attempts to add the specified <paramref name="exception"/> to the <see cref="ModelStateEntry.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.
/// errors has already been recorded, ensures that a <see cref="TooManyModelErrorsException"/> exception is
/// recorded instead.
/// </summary>
/// <param name="key">The key of the <see cref="ModelStateEntry"/> to add errors to.</param>
/// <param name="exception">The <see cref="Exception"/> to add.</param>
/// <param name="exception">The <see cref="Exception"/> to add. Some exception types will be replaced with
/// a descriptive error message.</param>
/// <param name="metadata">The <see cref="ModelMetadata"/> associated with the model.</param>
/// <returns>
/// <c>True</c> if the given error was added, <c>false</c> if the error was ignored.
@ -277,7 +323,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// <summary>
/// Adds the specified <paramref name="errorMessage"/> to the <see cref="ModelStateEntry.Errors"/> instance
/// that is associated with the specified <paramref name="key"/>.
/// that is associated with the specified <paramref name="key"/>. If the maximum number of allowed
/// errors has already been recorded, ensures that a <see cref="TooManyModelErrorsException"/> exception is
/// recorded instead.
/// </summary>
/// <param name="key">The key of the <see cref="ModelStateEntry"/> to add errors to.</param>
/// <param name="errorMessage">The error message to add.</param>
@ -299,7 +347,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// <summary>
/// Attempts to add the specified <paramref name="errorMessage"/> to the <see cref="ModelStateEntry.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.
/// errors has already been recorded, ensures that a <see cref="TooManyModelErrorsException"/> exception is
/// recorded instead.
/// </summary>
/// <param name="key">The key of the <see cref="ModelStateEntry"/> to add errors to.</param>
/// <param name="errorMessage">The error message to add.</param>

View File

@ -15,7 +15,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
/// <summary>
/// Adds the specified <paramref name="errorMessage"/> to the <see cref="ModelStateEntry.Errors"/> instance
/// that is associated with the specified <paramref name="expression"/>.
/// that is associated with the specified <paramref name="expression"/>. If the maximum number of allowed
/// errors has already been recorded, ensures that a <see cref="TooManyModelErrorsException"/> exception is
/// recorded instead.
/// </summary>
/// <typeparam name="TModel">The type of the model.</typeparam>
/// <param name="modelState">The <see cref="ModelStateDictionary"/> instance this method extends.</param>
@ -46,7 +48,42 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// <summary>
/// Adds the specified <paramref name="exception"/> to the <see cref="ModelStateEntry.Errors"/> instance
/// that is associated with the specified <paramref name="expression"/>.
/// that is associated with the specified <paramref name="expression"/>. If the maximum number of allowed
/// errors has already been recorded, ensures that a <see cref="TooManyModelErrorsException"/> exception is
/// recorded instead.
/// </summary>
/// <remarks>
/// This method allows adding the <paramref name="exception"/> to the current <see cref="ModelStateDictionary"/>
/// when <see cref="ModelMetadata"/> is not available or the exact <paramref name="exception"/>
/// must be maintained for later use (even if it is for example a <see cref="FormatException"/>).
/// </remarks>
/// <typeparam name="TModel">The type of the model.</typeparam>
/// <param name="modelState">The <see cref="ModelStateDictionary"/> instance this method extends.</param>
/// <param name="expression">An expression to be evaluated against an item in the current model.</param>
/// <param name="exception">The <see cref="Exception"/> to add.</param>
public static void TryAddModelException<TModel>(
this ModelStateDictionary modelState,
Expression<Func<TModel, object>> expression,
Exception exception)
{
if (modelState == null)
{
throw new ArgumentNullException(nameof(modelState));
}
if (expression == null)
{
throw new ArgumentNullException(nameof(expression));
}
modelState.TryAddModelException(GetExpressionText(expression), exception);
}
/// <summary>
/// Adds the specified <paramref name="exception"/> to the <see cref="ModelStateEntry.Errors"/> instance
/// that is associated with the specified <paramref name="expression"/>. If the maximum number of allowed
/// errors has already been recorded, ensures that a <see cref="TooManyModelErrorsException"/> exception is
/// recorded instead.
/// </summary>
/// <typeparam name="TModel">The type of the model.</typeparam>
/// <param name="modelState">The <see cref="ModelStateDictionary"/> instance this method extends.</param>

View File

@ -285,6 +285,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
Assert.Equal(entry.ValidationState, actual.ValidationState);
}
[Fact]
public void TryAddModelException_Succeeds()
{
// Arrange
var dictionary = new ModelStateDictionary();
var exception = new TestException();
// Act
dictionary.TryAddModelException("some key", exception);
// Assert
var kvp = Assert.Single(dictionary);
Assert.Equal("some key", kvp.Key);
var error = Assert.Single(kvp.Value.Errors);
Assert.Same(exception, error.Exception);
}
[Fact]
public void AddModelErrorCreatesModelStateIfNotPresent()
{
@ -695,13 +712,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
};
var provider = new EmptyModelMetadataProvider();
var metadata = provider.GetMetadataForProperty(typeof(string), nameof(string.Length));
// Act
dictionary.AddModelError("key1", "error1");
dictionary.AddModelError("key2", new Exception(), metadata);
dictionary.AddModelError("key3", new Exception(), metadata);
dictionary.AddModelError("key4", "error4");
dictionary.AddModelError("key5", "error5");
// Act and Assert
// Assert
Assert.True(dictionary.HasReachedMaxErrors);
Assert.Equal(5, dictionary.ErrorCount);
var error = Assert.Single(dictionary[string.Empty].Errors);
@ -712,6 +731,35 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
Assert.DoesNotContain("key5", dictionary.Keys);
}
[Fact]
public void TryAddModelException_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.TryAddModelException("key2", new Exception());
Assert.True(result);
result = dictionary.TryAddModelException("key3", new Exception());
Assert.False(result);
Assert.Equal(3, dictionary.Count);
var error = Assert.Single(dictionary[string.Empty].Errors);
Assert.IsType<TooManyModelErrorsException>(error.Exception);
Assert.Equal(expected, error.Exception.Message);
// TooManyModelErrorsException added instead of key3 exception.
Assert.DoesNotContain("key3", dictionary.Keys);
}
[Fact]
public void TryAddModelError_WithErrorString_ReturnsFalse_AndAddsMaxModelErrorMessage()
{
@ -864,6 +912,21 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
Assert.Equal(expected, canAdd);
}
[Fact]
public void ModelStateDictionary_ReturnExceptionMessage_WhenModelStateNotSet()
{
// Arrange
var dictionary = new ModelStateDictionary();
var exception = new FormatException("The supplied value is invalid for Length.");
// Act
dictionary.TryAddModelException("key", exception);
// Assert
var error = Assert.Single(dictionary["key"].Errors);
Assert.Same(exception, error.Exception);
}
[Fact]
public void ModelStateDictionary_ReturnGenericErrorMessage_WhenModelStateNotSet()
{
@ -933,6 +996,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
Assert.Equal(expected, error.ErrorMessage);
}
[Fact]
public void TryAddModelException_ReturnExceptionMessage_WhenModelStateSet()
{
// Arrange
var dictionary = new ModelStateDictionary();
dictionary.SetModelValue("key", new string[] { "some value" }, "some value");
var exception = new FormatException("The value 'some value' is not valid for Length.");
// Act
dictionary.TryAddModelException("key", exception);
// Assert
var error = Assert.Single(dictionary["key"].Errors);
Assert.Same(exception, error.Exception);
}
[Fact]
public void ModelStateDictionary_ReturnSpecificErrorMessage_WhenModelStateSet()
{
@ -1022,6 +1101,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
Assert.Empty(error.ErrorMessage);
}
[Fact]
public void TryAddModelException_AddsErrorMessage_ForInputFormatterException()
{
// Arrange
var dictionary = new ModelStateDictionary();
var exception = new InputFormatterException("This is an InputFormatterException.");
// Act
dictionary.TryAddModelException("key", exception);
// Assert
var entry = Assert.Single(dictionary);
Assert.Equal("key", entry.Key);
var error = Assert.Single(entry.Value.Errors);
Assert.Same(exception, error.Exception);
}
[Fact]
public void ModelStateDictionary_AddsErrorMessage_ForInputFormatterException()
{
@ -1371,4 +1467,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public MvcOptions Value { get; } = new MvcOptions();
}
}
internal class TestException : Exception
{
public TestException()
{
Message = "This is a test exception";
}
public override string Message { get; }
}
}

View File

@ -78,7 +78,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
[Fact]
public void AddModelError_ForSingleExpression_AddsExpectedException()
public void TryAddModelException_ForSingleExpression_AddsExpectedException()
{
// Arrange
var dictionary = new ModelStateDictionary();
var exception = new Exception();
// Act
dictionary.TryAddModelException<TestModel>(model => model.Text, exception);
// Assert
var modelState = Assert.Single(dictionary);
var modelError = Assert.Single(modelState.Value.Errors);
Assert.Equal("Text", modelState.Key);
Assert.Same(exception, modelError.Exception);
}
[Fact]
public void AddModelError_ForSingleExpression_AddsExpectedException_WithModelMetadata()
{
// Arrange
var dictionary = new ModelStateDictionary();
@ -98,7 +116,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
[Fact]
public void AddModelError_ForRelationExpression_AddsExpectedException()
public void TryAddModelException_ForRelationExpression_AddsExpectedException()
{
// Arrange
var dictionary = new ModelStateDictionary();
var exception = new Exception();
// Act
dictionary.TryAddModelException<TestModel>(model => model.Child.Text, exception);
// Assert
var modelState = Assert.Single(dictionary);
var modelError = Assert.Single(modelState.Value.Errors);
Assert.Equal("Child.Text", modelState.Key);
Assert.Same(exception, modelError.Exception);
}
[Fact]
public void AddModelError_ForRelationExpression_AddsExpectedException_WithModelMetadata()
{
// Arrange
var dictionary = new ModelStateDictionary();
@ -118,7 +154,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
[Fact]
public void AddModelError_ForImplicitlyCastedToObjectExpression_AddsExpectedException()
public void TryAddModelException_ForImplicitlyCastedToObjectExpression_AddsExpectedException()
{
// Arrange
var dictionary = new ModelStateDictionary();
var exception = new Exception();
// Act
dictionary.TryAddModelException<TestModel>(model => model.Child.Value, exception);
// Assert
var modelState = Assert.Single(dictionary);
var modelError = Assert.Single(modelState.Value.Errors);
Assert.Equal("Child.Value", modelState.Key);
Assert.Same(exception, modelError.Exception);
}
[Fact]
public void AddModelError_ForImplicitlyCastedToObjectExpression_AddsExpectedException_WithModelMetadata()
{
// Arrange
var dictionary = new ModelStateDictionary();
@ -138,7 +192,26 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
[Fact]
public void AddModelError_ForNotModelsExpression_AddsExpectedException()
public void TryAddModelException_ForNotModelsExpression_AddsExpectedException()
{
// Arrange
var dictionary = new ModelStateDictionary();
var variable = "Test";
var exception = new Exception();
// Act
dictionary.TryAddModelException<TestModel>(model => variable, exception);
// Assert
var modelState = Assert.Single(dictionary);
var modelError = Assert.Single(modelState.Value.Errors);
Assert.Equal("variable", modelState.Key);
Assert.Same(exception, modelError.Exception);
}
[Fact]
public void AddModelError_ForNotModelsExpression_AddsExpectedException_WithModelMetadata()
{
// Arrange
var dictionary = new ModelStateDictionary();