Updated formatters to wrap exceptions in InputFormatException for invalid input
This commit is contained in:
parent
f24d71136a
commit
83c3ac62fb
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue