From 83c3ac62fb9187c75689a26e9749046825cdd04b Mon Sep 17 00:00:00 2001 From: Kiran Challa Date: Thu, 14 Sep 2017 10:11:54 -0700 Subject: [PATCH] Updated formatters to wrap exceptions in InputFormatException for invalid input --- .../IInputFormatterExceptionPolicy.cs | 19 + ...InputFormatterExceptionModelStatePolicy.cs | 11 + .../InputFormatterException.cs | 27 + .../ModelBinding/Binders/BodyModelBinder.cs | 17 +- .../MvcOptions.cs | 8 + .../JsonInputFormatter.cs | 30 +- .../JsonPatchInputFormatter.cs | 13 + .../Properties/Resources.Designer.cs | 14 + .../Resources.resx | 3 + ...XmlDataContractSerializerInputFormatter.cs | 50 +- .../XmlSerializerInputFormatter.cs | 54 +- .../Properties/Resources.Designer.cs | 4 +- .../Binders/BodyModelBinderTests.cs | 525 +++++++++++++++++- ...ataContractSerializerInputFormatterTest.cs | 8 +- .../XmlSerializerInputFormatterTest.cs | 4 +- ...ataContractSerializerInputFormatterTest.cs | 2 +- .../XmlSerializerInputFormatterTests.cs | 2 +- .../Controllers/HomeController.cs | 25 +- 18 files changed, 731 insertions(+), 85 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.Abstractions/Formatters/IInputFormatterExceptionPolicy.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Abstractions/Formatters/InputFormatterExceptionModelStatePolicy.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/InputFormatterException.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/Formatters/IInputFormatterExceptionPolicy.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/Formatters/IInputFormatterExceptionPolicy.cs new file mode 100644 index 0000000000..2c5694b4c6 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/Formatters/IInputFormatterExceptionPolicy.cs @@ -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 +{ + /// + /// A policy which s can implement to indicate if they want the body model binder + /// to handle all exceptions. By default, all default s implement this interface and + /// have a default value of . + /// + public interface IInputFormatterExceptionPolicy + { + /// + /// 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. + /// + InputFormatterExceptionModelStatePolicy ExceptionPolicy { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/Formatters/InputFormatterExceptionModelStatePolicy.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/Formatters/InputFormatterExceptionModelStatePolicy.cs new file mode 100644 index 0000000000..3daf6438e0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/Formatters/InputFormatterExceptionModelStatePolicy.cs @@ -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, + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/InputFormatterException.cs b/src/Microsoft.AspNetCore.Mvc.Core/InputFormatterException.cs new file mode 100644 index 0000000000..135d71d701 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/InputFormatterException.cs @@ -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 +{ + /// + /// Exception thrown by when the input is not in an expected format. + /// + public class InputFormatterException : Exception + { + public InputFormatterException() + { + } + + public InputFormatterException(string message) + : base(message) + { + } + + public InputFormatterException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BodyModelBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BodyModelBinder.cs index 2ed481d7b8..9d22fd0de3 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BodyModelBinder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BodyModelBinder.cs @@ -177,10 +177,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders 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; + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs b/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs index 2244af1c51..1f17afa385 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs @@ -168,5 +168,13 @@ namespace Microsoft.AspNetCore.Mvc /// by default. /// public bool AllowBindingUndefinedValueToEnumType { get; set; } + + /// + /// 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 s into model state errors. + /// This option applies only to custom s. + /// Default is . + /// + public InputFormatterExceptionModelStatePolicy InputFormatterExceptionModelStatePolicy { get; set; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonInputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonInputFormatter.cs index 803eec6b17..6ce44c0876 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonInputFormatter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonInputFormatter.cs @@ -5,23 +5,24 @@ using System; using System.Buffers; using System.Diagnostics; using System.IO; +using System.Runtime.ExceptionServices; using System.Text; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Mvc.Formatters.Json.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; using Microsoft.Extensions.ObjectPool; -using Microsoft.AspNetCore.WebUtilities; using Newtonsoft.Json; -using System.Threading; namespace Microsoft.AspNetCore.Mvc.Formatters { /// /// A for JSON content. /// - public class JsonInputFormatter : TextInputFormatter + public class JsonInputFormatter : TextInputFormatter, IInputFormatterExceptionPolicy { private readonly IArrayPool _charPool; private readonly ILogger _logger; @@ -104,6 +105,19 @@ namespace Microsoft.AspNetCore.Mvc.Formatters SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyJsonSyntax); } + /// + public virtual InputFormatterExceptionModelStatePolicy ExceptionPolicy + { + get + { + if (GetType() == typeof(JsonInputFormatter)) + { + return InputFormatterExceptionModelStatePolicy.MalformedInputExceptions; + } + return InputFormatterExceptionModelStatePolicy.AllExceptions; + } + } + /// /// Gets the used to configure the . /// @@ -149,7 +163,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters jsonReader.CloseInput = false; var successful = true; - + Exception exception = null; void ErrorHandler(object sender, Newtonsoft.Json.Serialization.ErrorEventArgs eventArgs) { successful = false; @@ -177,6 +191,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters _logger.JsonInputException(eventArgs.ErrorContext.Error); + exception = eventArgs.ErrorContext.Error; + // Error must always be marked as handled // Failure to do so can cause the exception to be rethrown at every recursive level and // overflow the stack for x64 CLR processes @@ -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(); } } diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonPatchInputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonPatchInputFormatter.cs index 81fc385480..b04522111b 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonPatchInputFormatter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonPatchInputFormatter.cs @@ -63,6 +63,19 @@ namespace Microsoft.AspNetCore.Mvc.Formatters SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationJsonPatch); } + /// + public override InputFormatterExceptionModelStatePolicy ExceptionPolicy + { + get + { + if (GetType() == typeof(JsonPatchInputFormatter)) + { + return InputFormatterExceptionModelStatePolicy.MalformedInputExceptions; + } + return InputFormatterExceptionModelStatePolicy.AllExceptions; + } + } + /// public async override Task ReadRequestBodyAsync( InputFormatterContext context, diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Properties/Resources.Designer.cs index 6928f7f0bd..9396dad37e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Properties/Resources.Designer.cs @@ -24,6 +24,20 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml internal static string FormatEnumerableWrapperProvider_InvalidSourceEnumerableOfT(object p0) => string.Format(CultureInfo.CurrentCulture, GetString("EnumerableWrapperProvider_InvalidSourceEnumerableOfT"), p0); + /// + /// An error occured while deserializing input data. + /// + internal static string ErrorDeserializingInputData + { + get => GetString("ErrorDeserializingInputData"); + } + + /// + /// An error occured while deserializing input data. + /// + internal static string FormatErrorDeserializingInputData() + => GetString("ErrorDeserializingInputData"); + /// /// {0} does not recognize '{1}', so instead use '{2}' with '{3}' set to '{4}' for value type property '{5}' on type '{6}'. /// diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Resources.resx b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Resources.resx index 730dd8c289..b3e8d858f3 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Resources.resx @@ -120,6 +120,9 @@ The type must be an interface and must be or derive from '{0}'. + + An error occured while deserializing input data. + {0} does not recognize '{1}', so instead use '{2}' with '{3}' set to '{4}' for value type property '{5}' on type '{6}'. diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerInputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerInputFormatter.cs index b57aa46dad..e2dde401b1 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerInputFormatter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerInputFormatter.cs @@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// This class handles deserialization of input XML data /// to strongly-typed objects using . /// - public class XmlDataContractSerializerInputFormatter : TextInputFormatter + public class XmlDataContractSerializerInputFormatter : TextInputFormatter, IInputFormatterExceptionPolicy { private readonly ConcurrentDictionary _serializerCache = new ConcurrentDictionary(); private readonly XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.GetDefaultXmlReaderQuotas(); @@ -98,6 +98,19 @@ namespace Microsoft.AspNetCore.Mvc.Formatters } } + /// + public virtual InputFormatterExceptionModelStatePolicy ExceptionPolicy + { + get + { + if (GetType() == typeof(XmlDataContractSerializerInputFormatter)) + { + return InputFormatterExceptionModelStatePolicy.MalformedInputExceptions; + } + return InputFormatterExceptionModelStatePolicy.AllExceptions; + } + } + /// public override async Task ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding) { @@ -124,23 +137,30 @@ namespace Microsoft.AspNetCore.Mvc.Formatters request.Body.Seek(0L, SeekOrigin.Begin); } - using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body), encoding)) + try { - var type = GetSerializableType(context.ModelType); - var serializer = GetCachedSerializer(type); - - var deserializedObject = serializer.ReadObject(xmlReader); - - // Unwrap only if the original type was wrapped. - if (type != context.ModelType) + using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body), encoding)) { - if (deserializedObject is IUnwrappable unwrappable) - { - deserializedObject = unwrappable.Unwrap(declaredType: context.ModelType); - } - } + var type = GetSerializableType(context.ModelType); + var serializer = GetCachedSerializer(type); - 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); } } diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerInputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerInputFormatter.cs index 4ce0807982..02cc381aaf 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerInputFormatter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerInputFormatter.cs @@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// This class handles deserialization of input XML data /// to strongly-typed objects using /// - public class XmlSerializerInputFormatter : TextInputFormatter + public class XmlSerializerInputFormatter : TextInputFormatter, IInputFormatterExceptionPolicy { private readonly ConcurrentDictionary _serializerCache = new ConcurrentDictionary(); private readonly XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.GetDefaultXmlReaderQuotas(); @@ -77,6 +77,19 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// public XmlDictionaryReaderQuotas XmlDictionaryReaderQuotas => _readerQuotas; + /// + public virtual InputFormatterExceptionModelStatePolicy ExceptionPolicy + { + get + { + if (GetType() == typeof(XmlSerializerInputFormatter)) + { + return InputFormatterExceptionModelStatePolicy.MalformedInputExceptions; + } + return InputFormatterExceptionModelStatePolicy.AllExceptions; + } + } + /// public override async Task ReadRequestBodyAsync( InputFormatterContext context, @@ -105,24 +118,33 @@ namespace Microsoft.AspNetCore.Mvc.Formatters request.Body.Seek(0L, SeekOrigin.Begin); } - using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body), encoding)) + try { - var type = GetSerializableType(context.ModelType); - - var serializer = GetCachedSerializer(type); - - var deserializedObject = serializer.Deserialize(xmlReader); - - // Unwrap only if the original type was wrapped. - if (type != context.ModelType) + using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body), encoding)) { - if (deserializedObject is IUnwrappable unwrappable) - { - deserializedObject = unwrappable.Unwrap(declaredType: context.ModelType); - } - } + var type = GetSerializableType(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); } } diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs index f51ad6bac7..411940c2a9 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Properties/Resources.Designer.cs @@ -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); /// - /// Value cannot contain whitespace. + /// Value cannot contain HTML space characters. /// internal static string ArgumentCannotContainHtmlSpace { @@ -173,7 +173,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } /// - /// Value cannot contain whitespace. + /// Value cannot contain HTML space characters. /// internal static string FormatArgumentCannotContainHtmlSpace() => GetString("ArgumentCannotContainHtmlSpace"); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BodyModelBinderTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BodyModelBinderTests.cs index 3368825329..addf26f1d2 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BodyModelBinderTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BodyModelBinderTests.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Buffers; using System.Collections.Generic; using System.IO; using System.Linq; @@ -9,9 +10,13 @@ using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.ObjectPool; using Microsoft.Net.Http.Headers; using Moq; +using Newtonsoft.Json; using Xunit; namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders @@ -189,23 +194,33 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders Times.Once); } - [Fact] - public async Task CustomFormatterDeserializationException_AddedToModelState() + // Throwing InputFormatterException + [Theory] + [InlineData(InputFormatterExceptionModelStatePolicy.AllExceptions)] + [InlineData(InputFormatterExceptionModelStatePolicy.MalformedInputExceptions)] + public async Task BindModel_CustomFormatter_ThrowingInputFormatterException_AddsErrorToModelState( + InputFormatterExceptionModelStatePolicy inputFormatterExceptionModelStatePolicy) { // Arrange var httpContext = new DefaultHttpContext(); httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("Bad data!")); httpContext.Request.ContentType = "text/xyz"; - var provider = new TestModelMetadataProvider(); - provider.ForType().BindingDetails(d => d.BindingSource = BindingSource.Body); + var metadataProvider = new TestModelMetadataProvider(); + metadataProvider.ForType().BindingDetails(d => d.BindingSource = BindingSource.Body); - var bindingContext = GetBindingContext( - typeof(Person), - httpContext: httpContext, - metadataProvider: provider); - - var binder = CreateBinder(new[] { new XyzFormatter() }); + var expectedFormatException = new FormatException("bad format!"); + var bindingContext = GetBindingContext(typeof(Person), httpContext, metadataProvider); + var formatter = new XyzFormatter((inputFormatterContext, encoding) => + { + throw new InputFormatterException("Bad input!!", expectedFormatException); + }); + var binder = CreateBinder( + new[] { formatter }, + new MvcOptions() + { + InputFormatterExceptionModelStatePolicy = inputFormatterExceptionModelStatePolicy + }); // Act await binder.BindModelAsync(bindingContext); @@ -218,7 +233,339 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders var entry = Assert.Single(bindingContext.ModelState); Assert.Equal(string.Empty, entry.Key); 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(entry.Value.Errors[0].Exception.InnerException); + Assert.Same(expectedFormatException, formatException); + } + + public static TheoryData BuiltInFormattersThrowingInputFormatterException + { + get + { + return new TheoryData() + { + { 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().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(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().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(entry.Value.Errors[0].Exception); + } + + public static TheoryData DerivedFormattersThrowingInputFormatterException + { + get + { + return new TheoryData() + { + { 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().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(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().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(entry.Value.Errors[0].Exception); + } + + // Throwing Non-InputFormatterException + public static TheoryData BuiltInFormattersThrowingNonInputFormatterException + { + get + { + return new TheoryData() + { + { 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().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(() => binder.BindModelAsync(bindingContext)); + Assert.Equal("Unable to read input stream!!", exception.Message); + } + + public static TheoryData DerivedInputFormattersThrowingNonInputFormatterException + { + get + { + return new TheoryData() + { + { 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().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(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().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( + () => 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().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(entry.Value.Errors[0].Exception); } [Fact] @@ -392,23 +739,26 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders private static BodyModelBinder CreateBinder(IList formatters, bool treatEmptyInputAsDefaultValueOption = false) { - var sink = new TestSink(); - var loggerFactory = new TestLoggerFactory(sink, enabled: true); 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 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 { - public XyzFormatter() + private readonly Func> _readRequestBodyAsync; + + public XyzFormatter(Func> readRequestBodyAsync) { SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/xyz")); SupportedEncodings.Add(Encoding.UTF8); + _readRequestBodyAsync = readRequestBodyAsync; } protected override bool CanReadType(Type type) @@ -420,7 +770,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders InputFormatterContext context, 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); } } + + private class TestableJsonInputFormatter : JsonInputFormatter + { + private readonly bool _throwNonInputFormatterException; + + public TestableJsonInputFormatter(bool throwNonInputFormatterException) + : base(GetLogger(), new JsonSerializerSettings(), ArrayPool.Shared, new DefaultObjectPoolProvider()) + { + _throwNonInputFormatterException = throwNonInputFormatterException; + } + + public override InputFormatterExceptionModelStatePolicy ExceptionPolicy => InputFormatterExceptionModelStatePolicy.MalformedInputExceptions; + + public override Task 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 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 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.Shared, new DefaultObjectPoolProvider()) + { + _throwNonInputFormatterException = throwNonInputFormatterException; + } + + public override InputFormatterExceptionModelStatePolicy ExceptionPolicy => InputFormatterExceptionModelStatePolicy.AllExceptions; + + public override Task 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 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 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; } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/XmlDataContractSerializerInputFormatterTest.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/XmlDataContractSerializerInputFormatterTest.cs index 4cc8f27279..4833c0dea9 100644 --- a/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/XmlDataContractSerializerInputFormatterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/XmlDataContractSerializerInputFormatterTest.cs @@ -307,7 +307,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo)); // Act & Assert - await Assert.ThrowsAsync(async () => await formatter.ReadAsync(context)); + await Assert.ThrowsAsync(async () => await formatter.ReadAsync(context)); } [Fact] @@ -324,7 +324,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo)); // Act & Assert - await Assert.ThrowsAsync(async () => await formatter.ReadAsync(context)); + await Assert.ThrowsAsync(async () => await formatter.ReadAsync(context)); } [Fact] @@ -496,7 +496,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml var context = GetInputFormatterContext(contentBytes, typeof(DummyClass)); // Act & Assert - await Assert.ThrowsAsync(async () => await formatter.ReadAsync(context)); + await Assert.ThrowsAsync(async () => await formatter.ReadAsync(context)); } [Fact] @@ -553,7 +553,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml var context = GetInputFormatterContext(contentBytes, typeof(DummyClass)); // Act & Assert - await Assert.ThrowsAsync(async () => await formatter.ReadAsync(context)); + await Assert.ThrowsAsync(async () => await formatter.ReadAsync(context)); } [Fact] diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/XmlSerializerInputFormatterTest.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/XmlSerializerInputFormatterTest.cs index 1cc85cfa7c..e8b80b3582 100644 --- a/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/XmlSerializerInputFormatterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/XmlSerializerInputFormatterTest.cs @@ -340,7 +340,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo)); // Act & Assert - await Assert.ThrowsAsync(() => formatter.ReadAsync(context)); + await Assert.ThrowsAsync(() => formatter.ReadAsync(context)); } [ConditionalFact] @@ -361,7 +361,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml var context = GetInputFormatterContext(contentBytes, typeof(TestLevelTwo)); // Act & Assert - await Assert.ThrowsAsync(() => formatter.ReadAsync(context)); + await Assert.ThrowsAsync(() => formatter.ReadAsync(context)); } [Fact] diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/XmlDataContractSerializerInputFormatterTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/XmlDataContractSerializerInputFormatterTest.cs index 87cfc67238..a11073b0b4 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/XmlDataContractSerializerInputFormatterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/XmlDataContractSerializerInputFormatterTest.cs @@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests var data = await response.Content.ReadAsStringAsync(); Assert.Contains( 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), data); } diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/XmlSerializerInputFormatterTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/XmlSerializerInputFormatterTests.cs index ff11635f23..0dfa1016ce 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/XmlSerializerInputFormatterTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/XmlSerializerInputFormatterTests.cs @@ -52,7 +52,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); 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); } } } \ No newline at end of file diff --git a/test/WebSites/XmlFormattersWebSite/Controllers/HomeController.cs b/test/WebSites/XmlFormattersWebSite/Controllers/HomeController.cs index 70ba5ac3ce..9cf910399d 100644 --- a/test/WebSites/XmlFormattersWebSite/Controllers/HomeController.cs +++ b/test/WebSites/XmlFormattersWebSite/Controllers/HomeController.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; +using System.Linq; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -34,26 +35,10 @@ namespace XmlFormattersWebSite var errors = keyModelStatePair.Value.Errors; if (errors != null && errors.Count > 0) { - string errorMessage = null; - foreach (var modelError in errors) - { - if (string.IsNullOrEmpty(modelError.ErrorMessage)) - { - if (modelError.Exception != null) - { - errorMessage = modelError.Exception.Message; - } - } - else - { - errorMessage = modelError.ErrorMessage; - } - - if (errorMessage != null) - { - allErrorMessages.Add(string.Format("{0}:{1}", key, errorMessage)); - } - } + allErrorMessages.Add( + string.Join( + ",", + errors.Select(modelError => $"ErrorMessage:{modelError.ErrorMessage};Exception:{modelError.Exception}"))); } }