Updated formatters to wrap exceptions in InputFormatException for invalid input

This commit is contained in:
Kiran Challa 2017-09-14 10:11:54 -07:00
parent f24d71136a
commit 83c3ac62fb
18 changed files with 731 additions and 85 deletions

View File

@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Mvc.Formatters
{
/// <summary>
/// A policy which <see cref="IInputFormatter"/>s can implement to indicate if they want the body model binder
/// to handle all exceptions. By default, all default <see cref="IInputFormatter"/>s implement this interface and
/// have a default value of <see cref="InputFormatterExceptionModelStatePolicy.MalformedInputExceptions"/>.
/// </summary>
public interface IInputFormatterExceptionPolicy
{
/// <summary>
/// Gets the flag to indicate if the body model binder should handle all exceptions. If an exception is handled,
/// the body model binder converts the exception into model state errors, else the exception is allowed to propagate.
/// </summary>
InputFormatterExceptionModelStatePolicy ExceptionPolicy { get; }
}
}

View File

@ -0,0 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Mvc.Formatters
{
public enum InputFormatterExceptionModelStatePolicy
{
AllExceptions = 0,
MalformedInputExceptions = 1,
}
}

View File

@ -0,0 +1,27 @@
// Copyright (c) .NET Foundation. 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.AspNetCore.Mvc.Formatters
{
/// <summary>
/// Exception thrown by <see cref="IInputFormatter"/> when the input is not in an expected format.
/// </summary>
public class InputFormatterException : Exception
{
public InputFormatterException()
{
}
public InputFormatterException(string message)
: base(message)
{
}
public InputFormatterException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

View File

@ -177,10 +177,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
bindingContext.ModelState.AddModelError(modelBindingKey, message); bindingContext.ModelState.AddModelError(modelBindingKey, message);
} }
} }
catch (Exception ex) catch (Exception exception) when (exception is InputFormatterException || ShouldHandleException(formatter))
{ {
bindingContext.ModelState.AddModelError(modelBindingKey, ex, bindingContext.ModelMetadata); bindingContext.ModelState.AddModelError(modelBindingKey, exception, bindingContext.ModelMetadata);
} }
} }
private bool ShouldHandleException(IInputFormatter formatter)
{
var policy = _options.InputFormatterExceptionModelStatePolicy;
// Any explicit policy on the formatters takes precedence over the global policy on MvcOptions
if (formatter is IInputFormatterExceptionPolicy exceptionPolicy)
{
policy = exceptionPolicy.ExceptionPolicy;
}
return policy == InputFormatterExceptionModelStatePolicy.AllExceptions;
}
} }
} }

View File

@ -168,5 +168,13 @@ namespace Microsoft.AspNetCore.Mvc
/// <see langword="false"/> by default. /// <see langword="false"/> by default.
/// </summary> /// </summary>
public bool AllowBindingUndefinedValueToEnumType { get; set; } public bool AllowBindingUndefinedValueToEnumType { get; set; }
/// <summary>
/// Gets or sets the option to determine if model binding should convert all exceptions(including ones not related to bad input)
/// that occur during deserialization in <see cref="IInputFormatter"/>s into model state errors.
/// This option applies only to custom <see cref="IInputFormatter"/>s.
/// Default is <see cref="InputFormatterExceptionModelStatePolicy.AllExceptions"/>.
/// </summary>
public InputFormatterExceptionModelStatePolicy InputFormatterExceptionModelStatePolicy { get; set; }
} }
} }

View File

@ -5,23 +5,24 @@ using System;
using System.Buffers; using System.Buffers;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Runtime.ExceptionServices;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.Formatters.Json.Internal; using Microsoft.AspNetCore.Mvc.Formatters.Json.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.ObjectPool;
using Microsoft.AspNetCore.WebUtilities;
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Threading;
namespace Microsoft.AspNetCore.Mvc.Formatters namespace Microsoft.AspNetCore.Mvc.Formatters
{ {
/// <summary> /// <summary>
/// A <see cref="TextInputFormatter"/> for JSON content. /// A <see cref="TextInputFormatter"/> for JSON content.
/// </summary> /// </summary>
public class JsonInputFormatter : TextInputFormatter public class JsonInputFormatter : TextInputFormatter, IInputFormatterExceptionPolicy
{ {
private readonly IArrayPool<char> _charPool; private readonly IArrayPool<char> _charPool;
private readonly ILogger _logger; private readonly ILogger _logger;
@ -104,6 +105,19 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyJsonSyntax); SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyJsonSyntax);
} }
/// <inheritdoc />
public virtual InputFormatterExceptionModelStatePolicy ExceptionPolicy
{
get
{
if (GetType() == typeof(JsonInputFormatter))
{
return InputFormatterExceptionModelStatePolicy.MalformedInputExceptions;
}
return InputFormatterExceptionModelStatePolicy.AllExceptions;
}
}
/// <summary> /// <summary>
/// Gets the <see cref="JsonSerializerSettings"/> used to configure the <see cref="JsonSerializer"/>. /// Gets the <see cref="JsonSerializerSettings"/> used to configure the <see cref="JsonSerializer"/>.
/// </summary> /// </summary>
@ -149,7 +163,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
jsonReader.CloseInput = false; jsonReader.CloseInput = false;
var successful = true; var successful = true;
Exception exception = null;
void ErrorHandler(object sender, Newtonsoft.Json.Serialization.ErrorEventArgs eventArgs) void ErrorHandler(object sender, Newtonsoft.Json.Serialization.ErrorEventArgs eventArgs)
{ {
successful = false; successful = false;
@ -177,6 +191,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
_logger.JsonInputException(eventArgs.ErrorContext.Error); _logger.JsonInputException(eventArgs.ErrorContext.Error);
exception = eventArgs.ErrorContext.Error;
// Error must always be marked as handled // Error must always be marked as handled
// Failure to do so can cause the exception to be rethrown at every recursive level and // Failure to do so can cause the exception to be rethrown at every recursive level and
// overflow the stack for x64 CLR processes // overflow the stack for x64 CLR processes
@ -214,6 +230,12 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
} }
} }
if (!(exception is JsonException || exception is OverflowException))
{
var exceptionDispatchInfo = ExceptionDispatchInfo.Capture(exception);
exceptionDispatchInfo.Throw();
}
return InputFormatterResult.Failure(); return InputFormatterResult.Failure();
} }
} }

View File

@ -63,6 +63,19 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationJsonPatch); SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationJsonPatch);
} }
/// <inheritdoc />
public override InputFormatterExceptionModelStatePolicy ExceptionPolicy
{
get
{
if (GetType() == typeof(JsonPatchInputFormatter))
{
return InputFormatterExceptionModelStatePolicy.MalformedInputExceptions;
}
return InputFormatterExceptionModelStatePolicy.AllExceptions;
}
}
/// <inheritdoc /> /// <inheritdoc />
public async override Task<InputFormatterResult> ReadRequestBodyAsync( public async override Task<InputFormatterResult> ReadRequestBodyAsync(
InputFormatterContext context, InputFormatterContext context,

View File

@ -24,6 +24,20 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
internal static string FormatEnumerableWrapperProvider_InvalidSourceEnumerableOfT(object p0) internal static string FormatEnumerableWrapperProvider_InvalidSourceEnumerableOfT(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("EnumerableWrapperProvider_InvalidSourceEnumerableOfT"), p0); => string.Format(CultureInfo.CurrentCulture, GetString("EnumerableWrapperProvider_InvalidSourceEnumerableOfT"), p0);
/// <summary>
/// An error occured while deserializing input data.
/// </summary>
internal static string ErrorDeserializingInputData
{
get => GetString("ErrorDeserializingInputData");
}
/// <summary>
/// An error occured while deserializing input data.
/// </summary>
internal static string FormatErrorDeserializingInputData()
=> GetString("ErrorDeserializingInputData");
/// <summary> /// <summary>
/// {0} does not recognize '{1}', so instead use '{2}' with '{3}' set to '{4}' for value type property '{5}' on type '{6}'. /// {0} does not recognize '{1}', so instead use '{2}' with '{3}' set to '{4}' for value type property '{5}' on type '{6}'.
/// </summary> /// </summary>

View File

@ -120,6 +120,9 @@
<data name="EnumerableWrapperProvider_InvalidSourceEnumerableOfT" xml:space="preserve"> <data name="EnumerableWrapperProvider_InvalidSourceEnumerableOfT" xml:space="preserve">
<value>The type must be an interface and must be or derive from '{0}'.</value> <value>The type must be an interface and must be or derive from '{0}'.</value>
</data> </data>
<data name="ErrorDeserializingInputData" xml:space="preserve">
<value>An error occured while deserializing input data.</value>
</data>
<data name="RequiredProperty_MustHaveDataMemberRequired" xml:space="preserve"> <data name="RequiredProperty_MustHaveDataMemberRequired" xml:space="preserve">
<value>{0} does not recognize '{1}', so instead use '{2}' with '{3}' set to '{4}' for value type property '{5}' on type '{6}'.</value> <value>{0} does not recognize '{1}', so instead use '{2}' with '{3}' set to '{4}' for value type property '{5}' on type '{6}'.</value>
</data> </data>

View File

@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
/// This class handles deserialization of input XML data /// This class handles deserialization of input XML data
/// to strongly-typed objects using <see cref="DataContractSerializer"/>. /// to strongly-typed objects using <see cref="DataContractSerializer"/>.
/// </summary> /// </summary>
public class XmlDataContractSerializerInputFormatter : TextInputFormatter public class XmlDataContractSerializerInputFormatter : TextInputFormatter, IInputFormatterExceptionPolicy
{ {
private readonly ConcurrentDictionary<Type, object> _serializerCache = new ConcurrentDictionary<Type, object>(); private readonly ConcurrentDictionary<Type, object> _serializerCache = new ConcurrentDictionary<Type, object>();
private readonly XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.GetDefaultXmlReaderQuotas(); private readonly XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.GetDefaultXmlReaderQuotas();
@ -98,6 +98,19 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
} }
} }
/// <inheritdoc />
public virtual InputFormatterExceptionModelStatePolicy ExceptionPolicy
{
get
{
if (GetType() == typeof(XmlDataContractSerializerInputFormatter))
{
return InputFormatterExceptionModelStatePolicy.MalformedInputExceptions;
}
return InputFormatterExceptionModelStatePolicy.AllExceptions;
}
}
/// <inheritdoc /> /// <inheritdoc />
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding) public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
{ {
@ -124,23 +137,30 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
request.Body.Seek(0L, SeekOrigin.Begin); request.Body.Seek(0L, SeekOrigin.Begin);
} }
using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body), encoding)) try
{ {
var type = GetSerializableType(context.ModelType); using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body), encoding))
var serializer = GetCachedSerializer(type);
var deserializedObject = serializer.ReadObject(xmlReader);
// Unwrap only if the original type was wrapped.
if (type != context.ModelType)
{ {
if (deserializedObject is IUnwrappable unwrappable) var type = GetSerializableType(context.ModelType);
{ var serializer = GetCachedSerializer(type);
deserializedObject = unwrappable.Unwrap(declaredType: context.ModelType);
}
}
return InputFormatterResult.Success(deserializedObject); var deserializedObject = serializer.ReadObject(xmlReader);
// Unwrap only if the original type was wrapped.
if (type != context.ModelType)
{
if (deserializedObject is IUnwrappable unwrappable)
{
deserializedObject = unwrappable.Unwrap(declaredType: context.ModelType);
}
}
return InputFormatterResult.Success(deserializedObject);
}
}
catch (SerializationException exception)
{
throw new InputFormatterException(Resources.ErrorDeserializingInputData, exception);
} }
} }

View File

@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
/// This class handles deserialization of input XML data /// This class handles deserialization of input XML data
/// to strongly-typed objects using <see cref="XmlSerializer"/> /// to strongly-typed objects using <see cref="XmlSerializer"/>
/// </summary> /// </summary>
public class XmlSerializerInputFormatter : TextInputFormatter public class XmlSerializerInputFormatter : TextInputFormatter, IInputFormatterExceptionPolicy
{ {
private readonly ConcurrentDictionary<Type, object> _serializerCache = new ConcurrentDictionary<Type, object>(); private readonly ConcurrentDictionary<Type, object> _serializerCache = new ConcurrentDictionary<Type, object>();
private readonly XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.GetDefaultXmlReaderQuotas(); private readonly XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.GetDefaultXmlReaderQuotas();
@ -77,6 +77,19 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
/// </summary> /// </summary>
public XmlDictionaryReaderQuotas XmlDictionaryReaderQuotas => _readerQuotas; public XmlDictionaryReaderQuotas XmlDictionaryReaderQuotas => _readerQuotas;
/// <inheritdoc />
public virtual InputFormatterExceptionModelStatePolicy ExceptionPolicy
{
get
{
if (GetType() == typeof(XmlSerializerInputFormatter))
{
return InputFormatterExceptionModelStatePolicy.MalformedInputExceptions;
}
return InputFormatterExceptionModelStatePolicy.AllExceptions;
}
}
/// <inheritdoc /> /// <inheritdoc />
public override async Task<InputFormatterResult> ReadRequestBodyAsync( public override async Task<InputFormatterResult> ReadRequestBodyAsync(
InputFormatterContext context, InputFormatterContext context,
@ -105,24 +118,33 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
request.Body.Seek(0L, SeekOrigin.Begin); request.Body.Seek(0L, SeekOrigin.Begin);
} }
using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body), encoding)) try
{ {
var type = GetSerializableType(context.ModelType); using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body), encoding))
var serializer = GetCachedSerializer(type);
var deserializedObject = serializer.Deserialize(xmlReader);
// Unwrap only if the original type was wrapped.
if (type != context.ModelType)
{ {
if (deserializedObject is IUnwrappable unwrappable) var type = GetSerializableType(context.ModelType);
{
deserializedObject = unwrappable.Unwrap(declaredType: context.ModelType);
}
}
return InputFormatterResult.Success(deserializedObject); var serializer = GetCachedSerializer(type);
var deserializedObject = serializer.Deserialize(xmlReader);
// Unwrap only if the original type was wrapped.
if (type != context.ModelType)
{
if (deserializedObject is IUnwrappable unwrappable)
{
deserializedObject = unwrappable.Unwrap(declaredType: context.ModelType);
}
}
return InputFormatterResult.Success(deserializedObject);
}
}
// XmlSerializer wraps actual exceptions (like FormatException or XmlException) into an InvalidOperationException
// https://github.com/dotnet/corefx/blob/master/src/System.Private.Xml/src/System/Xml/Serialization/XmlSerializer.cs#L652
catch (InvalidOperationException exception) when (exception.InnerException is FormatException || exception.InnerException is XmlException)
{
throw new InputFormatterException(Resources.ErrorDeserializingInputData, exception);
} }
} }

View File

@ -165,7 +165,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
=> string.Format(CultureInfo.CurrentCulture, GetString("FormActionTagHelper_CannotOverrideFormAction"), p0, p1, p2, p3, p4, p5, p6, p7, p8, p9); => string.Format(CultureInfo.CurrentCulture, GetString("FormActionTagHelper_CannotOverrideFormAction"), p0, p1, p2, p3, p4, p5, p6, p7, p8, p9);
/// <summary> /// <summary>
/// Value cannot contain whitespace. /// Value cannot contain HTML space characters.
/// </summary> /// </summary>
internal static string ArgumentCannotContainHtmlSpace internal static string ArgumentCannotContainHtmlSpace
{ {
@ -173,7 +173,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
} }
/// <summary> /// <summary>
/// Value cannot contain whitespace. /// Value cannot contain HTML space characters.
/// </summary> /// </summary>
internal static string FormatArgumentCannotContainHtmlSpace() internal static string FormatArgumentCannotContainHtmlSpace()
=> GetString("ArgumentCannotContainHtmlSpace"); => GetString("ArgumentCannotContainHtmlSpace");

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -9,9 +10,13 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using Moq; using Moq;
using Newtonsoft.Json;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
@ -189,23 +194,33 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Times.Once); Times.Once);
} }
[Fact] // Throwing InputFormatterException
public async Task CustomFormatterDeserializationException_AddedToModelState() [Theory]
[InlineData(InputFormatterExceptionModelStatePolicy.AllExceptions)]
[InlineData(InputFormatterExceptionModelStatePolicy.MalformedInputExceptions)]
public async Task BindModel_CustomFormatter_ThrowingInputFormatterException_AddsErrorToModelState(
InputFormatterExceptionModelStatePolicy inputFormatterExceptionModelStatePolicy)
{ {
// Arrange // Arrange
var httpContext = new DefaultHttpContext(); var httpContext = new DefaultHttpContext();
httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("Bad data!")); httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("Bad data!"));
httpContext.Request.ContentType = "text/xyz"; httpContext.Request.ContentType = "text/xyz";
var provider = new TestModelMetadataProvider(); var metadataProvider = new TestModelMetadataProvider();
provider.ForType<Person>().BindingDetails(d => d.BindingSource = BindingSource.Body); metadataProvider.ForType<Person>().BindingDetails(d => d.BindingSource = BindingSource.Body);
var bindingContext = GetBindingContext( var expectedFormatException = new FormatException("bad format!");
typeof(Person), var bindingContext = GetBindingContext(typeof(Person), httpContext, metadataProvider);
httpContext: httpContext, var formatter = new XyzFormatter((inputFormatterContext, encoding) =>
metadataProvider: provider); {
throw new InputFormatterException("Bad input!!", expectedFormatException);
var binder = CreateBinder(new[] { new XyzFormatter() }); });
var binder = CreateBinder(
new[] { formatter },
new MvcOptions()
{
InputFormatterExceptionModelStatePolicy = inputFormatterExceptionModelStatePolicy
});
// Act // Act
await binder.BindModelAsync(bindingContext); await binder.BindModelAsync(bindingContext);
@ -218,7 +233,339 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
var entry = Assert.Single(bindingContext.ModelState); var entry = Assert.Single(bindingContext.ModelState);
Assert.Equal(string.Empty, entry.Key); Assert.Equal(string.Empty, entry.Key);
var errorMessage = Assert.Single(entry.Value.Errors).Exception.Message; var errorMessage = Assert.Single(entry.Value.Errors).Exception.Message;
Assert.Equal("Your input is bad!", errorMessage); Assert.Equal("Bad input!!", errorMessage);
var formatException = Assert.IsType<FormatException>(entry.Value.Errors[0].Exception.InnerException);
Assert.Same(expectedFormatException, formatException);
}
public static TheoryData<IInputFormatter, InputFormatterExceptionModelStatePolicy> BuiltInFormattersThrowingInputFormatterException
{
get
{
return new TheoryData<IInputFormatter, InputFormatterExceptionModelStatePolicy>()
{
{ new XmlSerializerInputFormatter(), InputFormatterExceptionModelStatePolicy.AllExceptions },
{ new XmlSerializerInputFormatter(), InputFormatterExceptionModelStatePolicy.MalformedInputExceptions },
{ new XmlDataContractSerializerInputFormatter(), InputFormatterExceptionModelStatePolicy.AllExceptions },
{ new XmlDataContractSerializerInputFormatter(), InputFormatterExceptionModelStatePolicy.MalformedInputExceptions },
};
}
}
[Theory]
[MemberData(nameof(BuiltInFormattersThrowingInputFormatterException))]
public async Task BindModel_BuiltInXmlInputFormatters_ThrowingInputFormatterException_AddsErrorToModelState(
IInputFormatter formatter,
InputFormatterExceptionModelStatePolicy inputFormatterExceptionModelStatePolicy)
{
// Arrange
var httpContext = new DefaultHttpContext();
httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("Bad data!"));
httpContext.Request.ContentType = "application/xml";
var metadataProvider = new TestModelMetadataProvider();
metadataProvider.ForType<Person>().BindingDetails(d => d.BindingSource = BindingSource.Body);
var bindingContext = GetBindingContext(typeof(Person), httpContext, metadataProvider);
var binder = CreateBinder(new[] { formatter }, new MvcOptions()
{
InputFormatterExceptionModelStatePolicy = inputFormatterExceptionModelStatePolicy
});
// Act
await binder.BindModelAsync(bindingContext);
// Assert
Assert.False(bindingContext.Result.IsModelSet);
Assert.Null(bindingContext.Result.Model);
// Key is the empty string because this was a top-level binding.
var entry = Assert.Single(bindingContext.ModelState);
Assert.Equal(string.Empty, entry.Key);
var errorMessage = Assert.Single(entry.Value.Errors).Exception.Message;
Assert.Equal("An error occured while deserializing input data.", errorMessage);
Assert.IsType<InputFormatterException>(entry.Value.Errors[0].Exception);
}
[Theory]
[InlineData(InputFormatterExceptionModelStatePolicy.AllExceptions)]
[InlineData(InputFormatterExceptionModelStatePolicy.MalformedInputExceptions)]
public async Task BindModel_BuiltInJsonInputFormatter_ThrowingInputFormatterException_AddsErrorToModelState(
InputFormatterExceptionModelStatePolicy inputFormatterExceptionModelStatePolicy)
{
// Arrange
var httpContext = new DefaultHttpContext();
httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("Bad data!"));
httpContext.Request.ContentType = "application/json";
var metadataProvider = new TestModelMetadataProvider();
metadataProvider.ForType<Person>().BindingDetails(d => d.BindingSource = BindingSource.Body);
var bindingContext = GetBindingContext(typeof(Person), httpContext, metadataProvider);
var binder = CreateBinder(
new[] { new TestableJsonInputFormatter(throwNonInputFormatterException: false) },
new MvcOptions()
{
InputFormatterExceptionModelStatePolicy = inputFormatterExceptionModelStatePolicy
});
// Act
await binder.BindModelAsync(bindingContext);
// Assert
Assert.False(bindingContext.Result.IsModelSet);
Assert.Null(bindingContext.Result.Model);
// Key is the empty string because this was a top-level binding.
var entry = Assert.Single(bindingContext.ModelState);
Assert.Equal(string.Empty, entry.Key);
Assert.IsType<JsonReaderException>(entry.Value.Errors[0].Exception);
}
public static TheoryData<IInputFormatter, InputFormatterExceptionModelStatePolicy> DerivedFormattersThrowingInputFormatterException
{
get
{
return new TheoryData<IInputFormatter, InputFormatterExceptionModelStatePolicy>()
{
{ new DerivedXmlSerializerInputFormatter(throwNonInputFormatterException: false), InputFormatterExceptionModelStatePolicy.AllExceptions },
{ new DerivedXmlSerializerInputFormatter(throwNonInputFormatterException: false), InputFormatterExceptionModelStatePolicy.MalformedInputExceptions },
{ new DerivedXmlDataContractSerializerInputFormatter(throwNonInputFormatterException: false), InputFormatterExceptionModelStatePolicy.AllExceptions },
{ new DerivedXmlDataContractSerializerInputFormatter(throwNonInputFormatterException: false), InputFormatterExceptionModelStatePolicy.MalformedInputExceptions },
};
}
}
[Theory]
[MemberData(nameof(DerivedFormattersThrowingInputFormatterException))]
public async Task BindModel_DerivedXmlInputFormatters_AddsErrorToModelState_(
IInputFormatter formatter,
InputFormatterExceptionModelStatePolicy inputFormatterExceptionModelStatePolicy)
{
// Arrange
var httpContext = new DefaultHttpContext();
httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("Bad data!"));
httpContext.Request.ContentType = "application/xml";
var metadataProvider = new TestModelMetadataProvider();
metadataProvider.ForType<Person>().BindingDetails(d => d.BindingSource = BindingSource.Body);
var bindingContext = GetBindingContext(typeof(Person), httpContext, metadataProvider);
var binder = CreateBinder(new[] { formatter }, new MvcOptions()
{
InputFormatterExceptionModelStatePolicy = inputFormatterExceptionModelStatePolicy
});
// Act
await binder.BindModelAsync(bindingContext);
// Assert
Assert.False(bindingContext.Result.IsModelSet);
Assert.Null(bindingContext.Result.Model);
// Key is the empty string because this was a top-level binding.
var entry = Assert.Single(bindingContext.ModelState);
Assert.Equal(string.Empty, entry.Key);
var errorMessage = Assert.Single(entry.Value.Errors).Exception.Message;
Assert.Equal("An error occured while deserializing input data.", errorMessage);
Assert.IsType<InputFormatterException>(entry.Value.Errors[0].Exception);
}
[Theory]
[InlineData(InputFormatterExceptionModelStatePolicy.AllExceptions)]
[InlineData(InputFormatterExceptionModelStatePolicy.MalformedInputExceptions)]
public async Task BindModel_DerivedJsonInputFormatter_AddsErrorToModelState(
InputFormatterExceptionModelStatePolicy inputFormatterExceptionModelStatePolicy)
{
// Arrange
var httpContext = new DefaultHttpContext();
httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("Bad data!"));
httpContext.Request.ContentType = "application/json";
var metadataProvider = new TestModelMetadataProvider();
metadataProvider.ForType<Person>().BindingDetails(d => d.BindingSource = BindingSource.Body);
var bindingContext = GetBindingContext(typeof(Person), httpContext, metadataProvider);
var binder = CreateBinder(
new[] { new DerivedJsonInputFormatter(throwNonInputFormatterException: false) },
new MvcOptions()
{
InputFormatterExceptionModelStatePolicy = inputFormatterExceptionModelStatePolicy
});
// Act
await binder.BindModelAsync(bindingContext);
// Assert
Assert.False(bindingContext.Result.IsModelSet);
Assert.Null(bindingContext.Result.Model);
// Key is the empty string because this was a top-level binding.
var entry = Assert.Single(bindingContext.ModelState);
Assert.Equal(string.Empty, entry.Key);
Assert.IsType<JsonReaderException>(entry.Value.Errors[0].Exception);
}
// Throwing Non-InputFormatterException
public static TheoryData<IInputFormatter, string, InputFormatterExceptionModelStatePolicy> BuiltInFormattersThrowingNonInputFormatterException
{
get
{
return new TheoryData<IInputFormatter, string, InputFormatterExceptionModelStatePolicy>()
{
{ new TestableXmlSerializerInputFormatter(throwNonInputFormatterException: true), "text/xml", InputFormatterExceptionModelStatePolicy.AllExceptions },
{ new TestableXmlSerializerInputFormatter(throwNonInputFormatterException: true), "text/xml", InputFormatterExceptionModelStatePolicy.MalformedInputExceptions },
{ new TestableXmlDataContractSerializerInputFormatter(throwNonInputFormatterException: true), "text/xml", InputFormatterExceptionModelStatePolicy.AllExceptions },
{ new TestableXmlDataContractSerializerInputFormatter(throwNonInputFormatterException: true), "text/xml", InputFormatterExceptionModelStatePolicy.MalformedInputExceptions },
{ new TestableJsonInputFormatter(throwNonInputFormatterException: true), "text/json", InputFormatterExceptionModelStatePolicy.AllExceptions },
{ new TestableJsonInputFormatter(throwNonInputFormatterException: true), "text/json", InputFormatterExceptionModelStatePolicy.MalformedInputExceptions },
};
}
}
[Theory]
[MemberData(nameof(BuiltInFormattersThrowingNonInputFormatterException))]
public async Task BindModel_BuiltInInputFormatters_ThrowingNonInputFormatterException_Throws(
IInputFormatter formatter,
string contentType,
InputFormatterExceptionModelStatePolicy inputFormatterExceptionModelStatePolicy)
{
// Arrange
var httpContext = new DefaultHttpContext();
httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("valid data!"));
httpContext.Request.ContentType = contentType;
var metadataProvider = new TestModelMetadataProvider();
metadataProvider.ForType<Person>().BindingDetails(d => d.BindingSource = BindingSource.Body);
var bindingContext = GetBindingContext(typeof(Person), httpContext, metadataProvider);
var binder = CreateBinder(new[] { formatter }, new MvcOptions()
{
InputFormatterExceptionModelStatePolicy = inputFormatterExceptionModelStatePolicy
});
// Act & Assert
var exception = await Assert.ThrowsAsync<IOException>(() => binder.BindModelAsync(bindingContext));
Assert.Equal("Unable to read input stream!!", exception.Message);
}
public static TheoryData<IInputFormatter, string, InputFormatterExceptionModelStatePolicy> DerivedInputFormattersThrowingNonInputFormatterException
{
get
{
return new TheoryData<IInputFormatter, string, InputFormatterExceptionModelStatePolicy>()
{
{ new DerivedXmlSerializerInputFormatter(throwNonInputFormatterException: true), "text/xml", InputFormatterExceptionModelStatePolicy.AllExceptions },
{ new DerivedXmlSerializerInputFormatter(throwNonInputFormatterException: true), "text/xml", InputFormatterExceptionModelStatePolicy.MalformedInputExceptions },
{ new DerivedXmlDataContractSerializerInputFormatter(throwNonInputFormatterException: true), "text/xml", InputFormatterExceptionModelStatePolicy.AllExceptions },
{ new DerivedXmlDataContractSerializerInputFormatter(throwNonInputFormatterException: true), "text/xml", InputFormatterExceptionModelStatePolicy.MalformedInputExceptions },
{ new DerivedJsonInputFormatter(throwNonInputFormatterException: true), "text/json", InputFormatterExceptionModelStatePolicy.AllExceptions },
{ new DerivedJsonInputFormatter(throwNonInputFormatterException: true), "text/json", InputFormatterExceptionModelStatePolicy.MalformedInputExceptions },
};
}
}
[Theory]
[MemberData(nameof(DerivedInputFormattersThrowingNonInputFormatterException))]
public async Task BindModel_DerivedXmlInputFormatters_ThrowingNonInputFormatingException_AddsErrorToModelState(
IInputFormatter formatter,
string contentType,
InputFormatterExceptionModelStatePolicy inputFormatterExceptionModelStatePolicy)
{
// Arrange
var httpContext = new DefaultHttpContext();
httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("valid data!"));
httpContext.Request.ContentType = contentType;
var metadataProvider = new TestModelMetadataProvider();
metadataProvider.ForType<Person>().BindingDetails(d => d.BindingSource = BindingSource.Body);
var bindingContext = GetBindingContext(typeof(Person), httpContext, metadataProvider);
var binder = CreateBinder(new[] { formatter }, new MvcOptions()
{
InputFormatterExceptionModelStatePolicy = inputFormatterExceptionModelStatePolicy
});
// Act
await binder.BindModelAsync(bindingContext);
// Assert
Assert.False(bindingContext.Result.IsModelSet);
Assert.Null(bindingContext.Result.Model);
// Key is the empty string because this was a top-level binding.
var entry = Assert.Single(bindingContext.ModelState);
Assert.Equal(string.Empty, entry.Key);
var errorMessage = Assert.Single(entry.Value.Errors).Exception.Message;
Assert.Equal("Unable to read input stream!!", errorMessage);
Assert.IsType<IOException>(entry.Value.Errors[0].Exception);
}
[Fact]
public async Task BindModel_CustomFormatter_ThrowingNonInputFormatterException_Throws()
{
// Arrange
var httpContext = new DefaultHttpContext();
httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("valid data"));
httpContext.Request.ContentType = "text/xyz";
var metadataProvider = new TestModelMetadataProvider();
metadataProvider.ForType<Person>().BindingDetails(d => d.BindingSource = BindingSource.Body);
var bindingContext = GetBindingContext(typeof(Person), httpContext, metadataProvider);
var formatter = new XyzFormatter((inputFormatterContext, encoding) =>
{
throw new IOException("Unable to read input stream!!");
});
var binder = CreateBinder(
new[] { formatter },
new MvcOptions()
{
InputFormatterExceptionModelStatePolicy = InputFormatterExceptionModelStatePolicy.MalformedInputExceptions
});
// Act
var exception = await Assert.ThrowsAsync<IOException>(
() => binder.BindModelAsync(bindingContext));
Assert.Equal("Unable to read input stream!!", exception.Message);
}
[Fact]
public async Task BindModel_CustomFormatter_ThrowingNonInputFormatterException_AddsErrorToModelState()
{
// Arrange
var httpContext = new DefaultHttpContext();
httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("Bad data!"));
httpContext.Request.ContentType = "text/xyz";
var metadataProvider = new TestModelMetadataProvider();
metadataProvider.ForType<Person>().BindingDetails(d => d.BindingSource = BindingSource.Body);
var bindingContext = GetBindingContext(typeof(Person), httpContext, metadataProvider);
var formatter = new XyzFormatter((inputFormatterContext, encoding) =>
{
throw new IOException("Unable to read input stream!!");
});
var binder = CreateBinder(
new[] { formatter },
new MvcOptions()
{
InputFormatterExceptionModelStatePolicy = InputFormatterExceptionModelStatePolicy.AllExceptions
});
// Act
await binder.BindModelAsync(bindingContext);
// Assert
Assert.False(bindingContext.Result.IsModelSet);
Assert.Null(bindingContext.Result.Model);
// Key is the empty string because this was a top-level binding.
var entry = Assert.Single(bindingContext.ModelState);
Assert.Equal(string.Empty, entry.Key);
var errorMessage = Assert.Single(entry.Value.Errors).Exception.Message;
Assert.Equal("Unable to read input stream!!", errorMessage);
Assert.IsType<IOException>(entry.Value.Errors[0].Exception);
} }
[Fact] [Fact]
@ -392,23 +739,26 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
private static BodyModelBinder CreateBinder(IList<IInputFormatter> formatters, bool treatEmptyInputAsDefaultValueOption = false) private static BodyModelBinder CreateBinder(IList<IInputFormatter> formatters, bool treatEmptyInputAsDefaultValueOption = false)
{ {
var sink = new TestSink();
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
var options = new MvcOptions { AllowEmptyInputInBodyModelBinding = treatEmptyInputAsDefaultValueOption }; var options = new MvcOptions { AllowEmptyInputInBodyModelBinding = treatEmptyInputAsDefaultValueOption };
return new BodyModelBinder(formatters, new TestHttpRequestStreamReaderFactory(), loggerFactory, options); return CreateBinder(formatters, options);
} }
private class Person private static BodyModelBinder CreateBinder(IList<IInputFormatter> formatters, MvcOptions mvcOptions)
{ {
public string Name { get; set; } var sink = new TestSink();
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
return new BodyModelBinder(formatters, new TestHttpRequestStreamReaderFactory(), loggerFactory, mvcOptions);
} }
private class XyzFormatter : TextInputFormatter private class XyzFormatter : TextInputFormatter
{ {
public XyzFormatter() private readonly Func<InputFormatterContext, Encoding, Task<InputFormatterResult>> _readRequestBodyAsync;
public XyzFormatter(Func<InputFormatterContext, Encoding, Task<InputFormatterResult>> readRequestBodyAsync)
{ {
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/xyz")); SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/xyz"));
SupportedEncodings.Add(Encoding.UTF8); SupportedEncodings.Add(Encoding.UTF8);
_readRequestBodyAsync = readRequestBodyAsync;
} }
protected override bool CanReadType(Type type) protected override bool CanReadType(Type type)
@ -420,7 +770,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
InputFormatterContext context, InputFormatterContext context,
Encoding effectiveEncoding) Encoding effectiveEncoding)
{ {
throw new InvalidOperationException("Your input is bad!"); return _readRequestBodyAsync(context, effectiveEncoding);
} }
} }
@ -443,5 +793,144 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
return InputFormatterResult.SuccessAsync(this); return InputFormatterResult.SuccessAsync(this);
} }
} }
private class TestableJsonInputFormatter : JsonInputFormatter
{
private readonly bool _throwNonInputFormatterException;
public TestableJsonInputFormatter(bool throwNonInputFormatterException)
: base(GetLogger(), new JsonSerializerSettings(), ArrayPool<char>.Shared, new DefaultObjectPoolProvider())
{
_throwNonInputFormatterException = throwNonInputFormatterException;
}
public override InputFormatterExceptionModelStatePolicy ExceptionPolicy => InputFormatterExceptionModelStatePolicy.MalformedInputExceptions;
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
{
if (_throwNonInputFormatterException)
{
throw new IOException("Unable to read input stream!!");
}
return base.ReadRequestBodyAsync(context, encoding);
}
}
private class TestableXmlSerializerInputFormatter : XmlSerializerInputFormatter
{
private bool _throwNonInputFormatterException;
public TestableXmlSerializerInputFormatter(bool throwNonInputFormatterException)
{
_throwNonInputFormatterException = throwNonInputFormatterException;
}
public override InputFormatterExceptionModelStatePolicy ExceptionPolicy => InputFormatterExceptionModelStatePolicy.MalformedInputExceptions;
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
{
if (_throwNonInputFormatterException)
{
throw new IOException("Unable to read input stream!!");
}
return base.ReadRequestBodyAsync(context, encoding);
}
}
private class TestableXmlDataContractSerializerInputFormatter : XmlDataContractSerializerInputFormatter
{
private bool _throwNonInputFormatterException;
public TestableXmlDataContractSerializerInputFormatter(bool throwNonInputFormatterException)
{
_throwNonInputFormatterException = throwNonInputFormatterException;
}
public override InputFormatterExceptionModelStatePolicy ExceptionPolicy => InputFormatterExceptionModelStatePolicy.MalformedInputExceptions;
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
{
if (_throwNonInputFormatterException)
{
throw new IOException("Unable to read input stream!!");
}
return base.ReadRequestBodyAsync(context, encoding);
}
}
private class DerivedJsonInputFormatter : JsonInputFormatter
{
private readonly bool _throwNonInputFormatterException;
public DerivedJsonInputFormatter(bool throwNonInputFormatterException)
: base(GetLogger(), new JsonSerializerSettings(), ArrayPool<char>.Shared, new DefaultObjectPoolProvider())
{
_throwNonInputFormatterException = throwNonInputFormatterException;
}
public override InputFormatterExceptionModelStatePolicy ExceptionPolicy => InputFormatterExceptionModelStatePolicy.AllExceptions;
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
{
if (_throwNonInputFormatterException)
{
throw new IOException("Unable to read input stream!!");
}
return base.ReadRequestBodyAsync(context, encoding);
}
}
private class DerivedXmlSerializerInputFormatter : XmlSerializerInputFormatter
{
private bool _throwNonInputFormatterException;
public DerivedXmlSerializerInputFormatter(bool throwNonInputFormatterException)
{
_throwNonInputFormatterException = throwNonInputFormatterException;
}
public override InputFormatterExceptionModelStatePolicy ExceptionPolicy => InputFormatterExceptionModelStatePolicy.AllExceptions;
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
{
if (_throwNonInputFormatterException)
{
throw new IOException("Unable to read input stream!!");
}
return base.ReadRequestBodyAsync(context, encoding);
}
}
private class DerivedXmlDataContractSerializerInputFormatter : XmlDataContractSerializerInputFormatter
{
private bool _throwNonInputFormatterException;
public DerivedXmlDataContractSerializerInputFormatter(bool throwNonInputFormatterException)
{
_throwNonInputFormatterException = throwNonInputFormatterException;
}
public override InputFormatterExceptionModelStatePolicy ExceptionPolicy => InputFormatterExceptionModelStatePolicy.AllExceptions;
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
{
if (_throwNonInputFormatterException)
{
throw new IOException("Unable to read input stream!!");
}
return base.ReadRequestBodyAsync(context, encoding);
}
}
private static ILogger GetLogger()
{
return NullLogger.Instance;
}
// 'public' as XmlSerializer does not like non-public types
public class Person
{
public string Name { get; set; }
}
} }
} }

View File

@ -307,7 +307,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo)); var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo));
// Act & Assert // Act & Assert
await Assert.ThrowsAsync<SerializationException>(async () => await formatter.ReadAsync(context)); await Assert.ThrowsAsync<InputFormatterException>(async () => await formatter.ReadAsync(context));
} }
[Fact] [Fact]
@ -324,7 +324,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo)); var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo));
// Act & Assert // Act & Assert
await Assert.ThrowsAsync<SerializationException>(async () => await formatter.ReadAsync(context)); await Assert.ThrowsAsync<InputFormatterException>(async () => await formatter.ReadAsync(context));
} }
[Fact] [Fact]
@ -496,7 +496,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
var context = GetInputFormatterContext(contentBytes, typeof(DummyClass)); var context = GetInputFormatterContext(contentBytes, typeof(DummyClass));
// Act & Assert // Act & Assert
await Assert.ThrowsAsync<SerializationException>(async () => await formatter.ReadAsync(context)); await Assert.ThrowsAsync<InputFormatterException>(async () => await formatter.ReadAsync(context));
} }
[Fact] [Fact]
@ -553,7 +553,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
var context = GetInputFormatterContext(contentBytes, typeof(DummyClass)); var context = GetInputFormatterContext(contentBytes, typeof(DummyClass));
// Act & Assert // Act & Assert
await Assert.ThrowsAsync<SerializationException>(async () => await formatter.ReadAsync(context)); await Assert.ThrowsAsync<InputFormatterException>(async () => await formatter.ReadAsync(context));
} }
[Fact] [Fact]

View File

@ -340,7 +340,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo)); var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo));
// Act & Assert // Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(() => formatter.ReadAsync(context)); await Assert.ThrowsAsync<InputFormatterException>(() => formatter.ReadAsync(context));
} }
[ConditionalFact] [ConditionalFact]
@ -361,7 +361,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo)); var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo));
// Act & Assert // Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(() => formatter.ReadAsync(context)); await Assert.ThrowsAsync<InputFormatterException>(() => formatter.ReadAsync(context));
} }
[Fact] [Fact]

View File

@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
var data = await response.Content.ReadAsStringAsync(); var data = await response.Content.ReadAsStringAsync();
Assert.Contains( Assert.Contains(
string.Format( string.Format(
":There was an error deserializing the object of type {0}.", "There was an error deserializing the object of type {0}.",
typeof(DummyClass).FullName), typeof(DummyClass).FullName),
data); data);
} }

View File

@ -52,7 +52,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
// Assert // Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var data = await response.Content.ReadAsStringAsync(); var data = await response.Content.ReadAsStringAsync();
Assert.Contains(":There is an error in XML document", data); Assert.Contains("There is an error in XML document", data);
} }
} }
} }

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
@ -34,26 +35,10 @@ namespace XmlFormattersWebSite
var errors = keyModelStatePair.Value.Errors; var errors = keyModelStatePair.Value.Errors;
if (errors != null && errors.Count > 0) if (errors != null && errors.Count > 0)
{ {
string errorMessage = null; allErrorMessages.Add(
foreach (var modelError in errors) string.Join(
{ ",",
if (string.IsNullOrEmpty(modelError.ErrorMessage)) errors.Select(modelError => $"ErrorMessage:{modelError.ErrorMessage};Exception:{modelError.Exception}")));
{
if (modelError.Exception != null)
{
errorMessage = modelError.Exception.Message;
}
}
else
{
errorMessage = modelError.ErrorMessage;
}
if (errorMessage != null)
{
allErrorMessages.Add(string.Format("{0}:{1}", key, errorMessage));
}
}
} }
} }