diff --git a/src/Microsoft.AspNetCore.Mvc.Core/CompatibilityVersion.cs b/src/Microsoft.AspNetCore.Mvc.Core/CompatibilityVersion.cs index 155ee032d5..91372a0d19 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/CompatibilityVersion.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/CompatibilityVersion.cs @@ -51,7 +51,9 @@ namespace Microsoft.AspNetCore.Mvc /// /// ASP.NET Core MVC 2.1 introduces compatibility switches for the following: /// + /// /// + /// MvcJsonOptions.AllowInputFormatterExceptionMessages /// RazorPagesOptions.AllowAreas /// /// diff --git a/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs b/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs index 4e4ca3188a..4c164d162f 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs @@ -24,9 +24,11 @@ namespace Microsoft.AspNetCore.Mvc // See CompatibilitySwitch.cs for guide on how to implement these. private readonly CompatibilitySwitch _inputFormatterExceptionPolicy; private readonly CompatibilitySwitch _suppressBindingUndefinedValueToEnumType; - private readonly CompatibilitySwitch _suppressJsonDeserializationExceptionMessagesInModelState; private readonly ICompatibilitySwitch[] _switches; + /// + /// Creates a new instance of . + /// public MvcOptions() { CacheProfiles = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -43,12 +45,11 @@ namespace Microsoft.AspNetCore.Mvc _inputFormatterExceptionPolicy = new CompatibilitySwitch(nameof(InputFormatterExceptionPolicy), InputFormatterExceptionPolicy.AllExceptions); _suppressBindingUndefinedValueToEnumType = new CompatibilitySwitch(nameof(SuppressBindingUndefinedValueToEnumType)); - _suppressJsonDeserializationExceptionMessagesInModelState = new CompatibilitySwitch(nameof(SuppressJsonDeserializationExceptionMessagesInModelState)); + _switches = new ICompatibilitySwitch[] { _inputFormatterExceptionPolicy, _suppressBindingUndefinedValueToEnumType, - _suppressJsonDeserializationExceptionMessagesInModelState, }; } @@ -241,20 +242,6 @@ namespace Microsoft.AspNetCore.Mvc /// public bool RequireHttpsPermanent { get; set; } - - /// - /// Gets or sets a flag to determine whether, if an action receives invalid JSON in - /// the request body, the JSON deserialization exception message should be replaced - /// by a generic error message in model state. - /// by default, meaning that clients may receive details about - /// why the JSON they posted is considered invalid. - /// - public bool SuppressJsonDeserializationExceptionMessagesInModelState - { - get => _suppressJsonDeserializationExceptionMessagesInModelState.Value; - set => _suppressJsonDeserializationExceptionMessagesInModelState.Value = value; - } - IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_switches).GetEnumerator(); diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/DependencyInjection/MvcJsonMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/DependencyInjection/MvcJsonMvcCoreBuilderExtensions.cs index 598e38868b..accd9045eb 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/DependencyInjection/MvcJsonMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/DependencyInjection/MvcJsonMvcCoreBuilderExtensions.cs @@ -75,6 +75,8 @@ namespace Microsoft.Extensions.DependencyInjection { services.TryAddEnumerable( ServiceDescriptor.Transient, MvcJsonMvcOptionsSetup>()); + services.TryAddEnumerable( + ServiceDescriptor.Transient, MvcJsonOptionsConfigureCompatibilityOptions>()); services.TryAddEnumerable( ServiceDescriptor.Transient()); services.TryAddSingleton(); diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/MvcJsonMvcOptionsSetup.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/MvcJsonMvcOptionsSetup.cs index ad2af947ed..56f14f8185 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/MvcJsonMvcOptionsSetup.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/MvcJsonMvcOptionsSetup.cs @@ -9,7 +9,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace Microsoft.AspNetCore.Mvc.Formatters.Json.Internal @@ -20,7 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json.Internal public class MvcJsonMvcOptionsSetup : IConfigureOptions { private readonly ILoggerFactory _loggerFactory; - private readonly JsonSerializerSettings _jsonSerializerSettings; + private readonly MvcJsonOptions _jsonOptions; private readonly ArrayPool _charPool; private readonly ObjectPoolProvider _objectPoolProvider; @@ -51,14 +50,14 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json.Internal } _loggerFactory = loggerFactory; - _jsonSerializerSettings = jsonOptions.Value.SerializerSettings; + _jsonOptions = jsonOptions.Value; _charPool = charPool; _objectPoolProvider = objectPoolProvider; } public void Configure(MvcOptions options) { - options.OutputFormatters.Add(new JsonOutputFormatter(_jsonSerializerSettings, _charPool)); + options.OutputFormatters.Add(new JsonOutputFormatter(_jsonOptions.SerializerSettings, _charPool)); // Register JsonPatchInputFormatter before JsonInputFormatter, otherwise // JsonInputFormatter would consume "application/json-patch+json" requests @@ -66,18 +65,20 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json.Internal var jsonInputPatchLogger = _loggerFactory.CreateLogger(); options.InputFormatters.Add(new JsonPatchInputFormatter( jsonInputPatchLogger, - _jsonSerializerSettings, + _jsonOptions.SerializerSettings, _charPool, _objectPoolProvider, - options)); + options, + _jsonOptions)); var jsonInputLogger = _loggerFactory.CreateLogger(); options.InputFormatters.Add(new JsonInputFormatter( jsonInputLogger, - _jsonSerializerSettings, + _jsonOptions.SerializerSettings, _charPool, _objectPoolProvider, - options)); + options, + _jsonOptions)); options.FormatterMappings.SetMediaTypeMappingForFormat("json", MediaTypeHeaderValue.Parse("application/json")); diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonInputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonInputFormatter.cs index bb1c251186..8201e07634 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonInputFormatter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonInputFormatter.cs @@ -28,8 +28,12 @@ namespace Microsoft.AspNetCore.Mvc.Formatters private readonly ILogger _logger; private readonly ObjectPoolProvider _objectPoolProvider; private readonly MvcOptions _options; + private readonly MvcJsonOptions _jsonOptions; + + // These fields are used when one of the legacy constructors is called that doesn't provide the MvcOptions or + // MvcJsonOptions. private readonly bool _suppressInputFormatterBuffering; - private readonly bool _suppressJsonDeserializationExceptionMessages; + private readonly bool _allowInputFormatterExceptionMessages; private ObjectPool _jsonSerializerPool; @@ -74,10 +78,10 @@ namespace Microsoft.AspNetCore.Mvc.Formatters ArrayPool charPool, ObjectPoolProvider objectPoolProvider, bool suppressInputFormatterBuffering) - : this(logger, serializerSettings, charPool, objectPoolProvider, suppressInputFormatterBuffering, suppressJsonDeserializationExceptionMessages: false) + : this(logger, serializerSettings, charPool, objectPoolProvider, suppressInputFormatterBuffering, allowInputFormatterExceptionMessages: false) { - // This constructor by default treats JSON deserialization exceptions as safe - // because this is the default for applications generally + // This constructor by default treats JSON deserialization exceptions as unsafe + // because this is the default in 2.0 } /// @@ -92,7 +96,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// The . /// The . /// Flag to buffer entire request body before deserializing it. - /// If , JSON deserialization exception messages will replaced by a generic message in model state. + /// If , JSON deserialization exception messages will replaced by a generic message in model state. [Obsolete("This constructor is obsolete and will be removed in a future version.")] public JsonInputFormatter( ILogger logger, @@ -100,7 +104,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters ArrayPool charPool, ObjectPoolProvider objectPoolProvider, bool suppressInputFormatterBuffering, - bool suppressJsonDeserializationExceptionMessages) + bool allowInputFormatterExceptionMessages) { if (logger == null) { @@ -127,7 +131,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters _charPool = new JsonArrayPool(charPool); _objectPoolProvider = objectPoolProvider; _suppressInputFormatterBuffering = suppressInputFormatterBuffering; - _suppressJsonDeserializationExceptionMessages = suppressJsonDeserializationExceptionMessages; + _allowInputFormatterExceptionMessages = allowInputFormatterExceptionMessages; SupportedEncodings.Add(UTF8EncodingWithoutBOM); SupportedEncodings.Add(UTF16EncodingLittleEndian); @@ -149,12 +153,14 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// The . /// The . /// The . + /// The . public JsonInputFormatter( ILogger logger, JsonSerializerSettings serializerSettings, ArrayPool charPool, ObjectPoolProvider objectPoolProvider, - MvcOptions options) + MvcOptions options, + MvcJsonOptions jsonOptions) { if (logger == null) { @@ -181,6 +187,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters _charPool = new JsonArrayPool(charPool); _objectPoolProvider = objectPoolProvider; _options = options; + _jsonOptions = jsonOptions; SupportedEncodings.Add(UTF8EncodingWithoutBOM); SupportedEncodings.Add(UTF16EncodingLittleEndian); @@ -406,17 +413,26 @@ namespace Microsoft.AspNetCore.Mvc.Formatters private Exception WrapExceptionForModelState(Exception exception) { + // In 2.0 and earlier we always gave a generic error message for errors that come from JSON.NET + // We only allow it in 2.1 and newer if the app opts-in. + if (!(_jsonOptions?.AllowInputFormatterExceptionMessages ?? _allowInputFormatterExceptionMessages)) + { + // This app is not opted-in to JSON.NET messages, return the original exception. + return exception; + } + // It's not known that Json.NET currently ever raises error events with exceptions // other than these two types, but we're being conservative and limiting which ones // we regard as having safe messages to expose to clients - var isJsonExceptionType = - exception is JsonReaderException || exception is JsonSerializationException; - var suppressJsonDeserializationExceptionMessages = _options?.SuppressJsonDeserializationExceptionMessagesInModelState ?? _suppressJsonDeserializationExceptionMessages; - var suppressOriginalMessage = - suppressJsonDeserializationExceptionMessages || !isJsonExceptionType; - return suppressOriginalMessage - ? exception - : new InputFormatterException(exception.Message, exception); + if (exception is JsonReaderException || exception is JsonSerializationException) + { + // InputFormatterException specifies that the message is safe to return to a client, it will + // be added to model state. + return new InputFormatterException(exception.Message, exception); + } + + // Not a known exception type, so we're not going to assume that it's safe. + return exception; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonPatchInputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonPatchInputFormatter.cs index 58c5b3a4f3..96549d44d5 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonPatchInputFormatter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonPatchInputFormatter.cs @@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters ArrayPool charPool, ObjectPoolProvider objectPoolProvider, bool suppressInputFormatterBuffering) - : this(logger, serializerSettings, charPool, objectPoolProvider, suppressInputFormatterBuffering, suppressJsonDeserializationExceptionMessages: false) + : this(logger, serializerSettings, charPool, objectPoolProvider, suppressInputFormatterBuffering, allowInputFormatterExceptionMessages: false) { } @@ -75,7 +75,9 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// The . /// The . /// Flag to buffer entire request body before deserializing it. - /// If , JSON deserialization exception messages will replaced by a generic message in model state. + /// + /// If , JSON deserialization exception messages will replaced by a generic message in model state. + /// [Obsolete("This constructor is obsolete and will be removed in a future version.")] public JsonPatchInputFormatter( ILogger logger, @@ -83,8 +85,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters ArrayPool charPool, ObjectPoolProvider objectPoolProvider, bool suppressInputFormatterBuffering, - bool suppressJsonDeserializationExceptionMessages) - : base(logger, serializerSettings, charPool, objectPoolProvider, suppressInputFormatterBuffering, suppressJsonDeserializationExceptionMessages) + bool allowInputFormatterExceptionMessages) + : base(logger, serializerSettings, charPool, objectPoolProvider, suppressInputFormatterBuffering, allowInputFormatterExceptionMessages) { // Clear all values and only include json-patch+json value. SupportedMediaTypes.Clear(); @@ -104,13 +106,15 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// The . /// The . /// The . + /// The . public JsonPatchInputFormatter( ILogger logger, JsonSerializerSettings serializerSettings, ArrayPool charPool, ObjectPoolProvider objectPoolProvider, - MvcOptions options) - : base(logger, serializerSettings, charPool, objectPoolProvider, options) + MvcOptions options, + MvcJsonOptions jsonOptions) + : base(logger, serializerSettings, charPool, objectPoolProvider, options, jsonOptions) { // Clear all values and only include json-patch+json value. SupportedMediaTypes.Clear(); diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/MvcJsonOptions.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/MvcJsonOptions.cs index 3e674a58c9..9c94d7b55c 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/MvcJsonOptions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/MvcJsonOptions.cs @@ -1,7 +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. +using System.Collections; +using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Newtonsoft.Json; namespace Microsoft.AspNetCore.Mvc @@ -9,12 +13,69 @@ namespace Microsoft.AspNetCore.Mvc /// /// Provides programmatic configuration for JSON in the MVC framework. /// - public class MvcJsonOptions + public class MvcJsonOptions : IEnumerable { + private readonly CompatibilitySwitch _allowInputFormatterExceptionMessages; + private readonly ICompatibilitySwitch[] _switches; + + /// + /// Creates a new instance of . + /// + public MvcJsonOptions() + { + _allowInputFormatterExceptionMessages = new CompatibilitySwitch(nameof(AllowInputFormatterExceptionMessages)); + + _switches = new ICompatibilitySwitch[] + { + _allowInputFormatterExceptionMessages, + }; + } + + /// + /// Gets or sets a flag to determine whether error messsages from JSON deserialization by the + /// will be added to the . The default + /// value is false, meaning that a generic error message will be used instead. + /// + /// + /// + /// Error messages in the are often communicated to clients, either in HTML + /// or using . In effect, this setting controls whether clients can recieve + /// detailed error messages about submitted JSON data. + /// + /// + /// This property is associated with a compatibility switch and can provide a different behavior depending on + /// the configured compatibility version for the application. See for + /// guidance and examples of setting the application's compatibility version. + /// + /// + /// Configuring the desired of the value compatibility switch by calling this property's setter will take precedence + /// over the value implied by the application's . + /// + /// + /// If the application's compatibility version is set to then + /// this setting will have value false if not explicitly configured. + /// + /// + /// If the application's compatibility version is set to or + /// higher then this setting will have value true if not explicitly configured. + /// + /// + public bool AllowInputFormatterExceptionMessages + { + get => _allowInputFormatterExceptionMessages.Value; + set => _allowInputFormatterExceptionMessages.Value = value; + } + /// /// Gets the that are used by this application. /// - public JsonSerializerSettings SerializerSettings { get; } = - JsonSerializerSettingsProvider.CreateSerializerSettings(); + public JsonSerializerSettings SerializerSettings { get; } = JsonSerializerSettingsProvider.CreateSerializerSettings(); + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_switches).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => _switches.GetEnumerator(); } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/MvcJsonOptionsConfigureCompatibilityOptions.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/MvcJsonOptionsConfigureCompatibilityOptions.cs new file mode 100644 index 0000000000..f33e6dc108 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/MvcJsonOptionsConfigureCompatibilityOptions.cs @@ -0,0 +1,35 @@ +// 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.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc +{ + internal class MvcJsonOptionsConfigureCompatibilityOptions : ConfigureCompatibilityOptions + { + public MvcJsonOptionsConfigureCompatibilityOptions( + ILoggerFactory loggerFactory, + IOptions compatibilityOptions) + : base(loggerFactory, compatibilityOptions) + { + } + + protected override IReadOnlyDictionary DefaultValues + { + get + { + var values = new Dictionary(); + + if (Version >= CompatibilityVersion.Version_2_1) + { + values[nameof(MvcJsonOptions.AllowInputFormatterExceptionMessages)] = true; + } + + return values; + } + } + } +} 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 10e1641f26..2babd023db 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BodyModelBinderTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BodyModelBinderTests.cs @@ -799,7 +799,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders private readonly bool _throwNonInputFormatterException; public TestableJsonInputFormatter(bool throwNonInputFormatterException) - : base(GetLogger(), new JsonSerializerSettings(), ArrayPool.Shared, new DefaultObjectPoolProvider(), new MvcOptions()) + : base(GetLogger(), new JsonSerializerSettings(), ArrayPool.Shared, new DefaultObjectPoolProvider(), new MvcOptions(), new MvcJsonOptions() + { + // The tests that use this class rely on the 2.1 behavior of this formatter. + AllowInputFormatterExceptionMessages = true, + }) { _throwNonInputFormatterException = throwNonInputFormatterException; } @@ -865,7 +869,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders private readonly bool _throwNonInputFormatterException; public DerivedJsonInputFormatter(bool throwNonInputFormatterException) - : base(GetLogger(), new JsonSerializerSettings(), ArrayPool.Shared, new DefaultObjectPoolProvider(), new MvcOptions()) + : base(GetLogger(), new JsonSerializerSettings(), ArrayPool.Shared, new DefaultObjectPoolProvider(), new MvcOptions(), new MvcJsonOptions() + { + // The tests that use this class rely on the 2.1 behavior of this formatter. + AllowInputFormatterExceptionMessages = true, + }) { _throwNonInputFormatterException = throwNonInputFormatterException; } diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs index 88e712ae90..0441961c2a 100644 --- a/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs @@ -28,36 +28,32 @@ namespace Microsoft.AspNetCore.Mvc.Formatters private static readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings(); [Fact] - public async Task BuffersRequestBody_ByDefault() + public async Task Version_2_0_Constructor_BuffersRequestBody_ByDefault() { // Arrange - var content = "{name: 'Person Name', Age: '30'}"; - var logger = GetLogger(); #pragma warning disable CS0618 - var formatter = - new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider); + var formatter = new JsonInputFormatter( + GetLogger(), + _serializerSettings, + ArrayPool.Shared, + _objectPoolProvider); #pragma warning restore CS0618 - var contentBytes = Encoding.UTF8.GetBytes(content); - var modelState = new ModelStateDictionary(); + var content = "{name: 'Person Name', Age: '30'}"; + var contentBytes = Encoding.UTF8.GetBytes(content); var httpContext = new DefaultHttpContext(); httpContext.Features.Set(new TestResponseFeature()); httpContext.Request.Body = new NonSeekableReadStream(contentBytes); httpContext.Request.ContentType = "application/json"; - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(typeof(User)); - var context = new InputFormatterContext( - httpContext, - modelName: string.Empty, - modelState: modelState, - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); + + var formatterContext = CreateInputFormatterContext(typeof(User), httpContext); // Act - var result = await formatter.ReadAsync(context); + var result = await formatter.ReadAsync(formatterContext); // Assert Assert.False(result.HasError); + var userModel = Assert.IsType(result.Model); Assert.Equal("Person Name", userModel.Name); Assert.Equal(30, userModel.Age); @@ -65,48 +61,43 @@ namespace Microsoft.AspNetCore.Mvc.Formatters Assert.True(httpContext.Request.Body.CanSeek); httpContext.Request.Body.Seek(0L, SeekOrigin.Begin); - result = await formatter.ReadAsync(context); + result = await formatter.ReadAsync(formatterContext); // Assert Assert.False(result.HasError); + userModel = Assert.IsType(result.Model); Assert.Equal("Person Name", userModel.Name); Assert.Equal(30, userModel.Age); } [Fact] - public async Task BuffersRequestBody_UsingDefaultOptions() + public async Task Version_2_1_Constructor_BuffersRequestBody_UsingDefaultOptions() { // Arrange - var content = "{name: 'Person Name', Age: '30'}"; - var logger = GetLogger(); var formatter = new JsonInputFormatter( - logger, + GetLogger(), _serializerSettings, ArrayPool.Shared, _objectPoolProvider, - new MvcOptions()); - var contentBytes = Encoding.UTF8.GetBytes(content); + new MvcOptions(), + new MvcJsonOptions()); - var modelState = new ModelStateDictionary(); + var content = "{name: 'Person Name', Age: '30'}"; + var contentBytes = Encoding.UTF8.GetBytes(content); var httpContext = new DefaultHttpContext(); httpContext.Features.Set(new TestResponseFeature()); httpContext.Request.Body = new NonSeekableReadStream(contentBytes); httpContext.Request.ContentType = "application/json"; - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(typeof(User)); - var context = new InputFormatterContext( - httpContext, - modelName: string.Empty, - modelState: modelState, - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); + + var formatterContext = CreateInputFormatterContext(typeof(User), httpContext); // Act - var result = await formatter.ReadAsync(context); + var result = await formatter.ReadAsync(formatterContext); // Assert Assert.False(result.HasError); + var userModel = Assert.IsType(result.Model); Assert.Equal("Person Name", userModel.Name); Assert.Equal(30, userModel.Age); @@ -114,99 +105,50 @@ namespace Microsoft.AspNetCore.Mvc.Formatters Assert.True(httpContext.Request.Body.CanSeek); httpContext.Request.Body.Seek(0L, SeekOrigin.Begin); - result = await formatter.ReadAsync(context); + result = await formatter.ReadAsync(formatterContext); // Assert Assert.False(result.HasError); + userModel = Assert.IsType(result.Model); Assert.Equal("Person Name", userModel.Name); Assert.Equal(30, userModel.Age); } [Fact] - public async Task SuppressInputFormatterBufferingSetToTrue_DoesNotBufferRequestBody() + public async Task Version_2_0_Constructor_SuppressInputFormatterBufferingSetToTrue_DoesNotBufferRequestBody() { // Arrange - var content = "{name: 'Person Name', Age: '30'}"; - var logger = GetLogger(); #pragma warning disable CS0618 - var formatter = - new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, suppressInputFormatterBuffering: true); -#pragma warning restore CS0618 - var contentBytes = Encoding.UTF8.GetBytes(content); - - var modelState = new ModelStateDictionary(); - var httpContext = new DefaultHttpContext(); - httpContext.Features.Set(new TestResponseFeature()); - httpContext.Request.Body = new NonSeekableReadStream(contentBytes); - httpContext.Request.ContentType = "application/json"; - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(typeof(User)); - var context = new InputFormatterContext( - httpContext, - modelName: string.Empty, - modelState: modelState, - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); - - // Act - var result = await formatter.ReadAsync(context); - - // Assert - Assert.False(result.HasError); - var userModel = Assert.IsType(result.Model); - Assert.Equal("Person Name", userModel.Name); - Assert.Equal(30, userModel.Age); - - Assert.False(httpContext.Request.Body.CanSeek); - result = await formatter.ReadAsync(context); - - // Assert - Assert.False(result.HasError); - Assert.Null(result.Model); - } - - [Fact] - public async Task SuppressInputFormatterBufferingSetToTrue_UsingMvcOptions_DoesNotBufferRequestBody() - { - // Arrange - var content = "{name: 'Person Name', Age: '30'}"; - var logger = GetLogger(); - var mvcOptions = new MvcOptions(); - mvcOptions.SuppressInputFormatterBuffering = true; var formatter = new JsonInputFormatter( - logger, + GetLogger(), _serializerSettings, ArrayPool.Shared, _objectPoolProvider, - mvcOptions); - var contentBytes = Encoding.UTF8.GetBytes(content); + suppressInputFormatterBuffering: true); +#pragma warning restore CS0618 - var modelState = new ModelStateDictionary(); + var content = "{name: 'Person Name', Age: '30'}"; + var contentBytes = Encoding.UTF8.GetBytes(content); var httpContext = new DefaultHttpContext(); httpContext.Features.Set(new TestResponseFeature()); httpContext.Request.Body = new NonSeekableReadStream(contentBytes); httpContext.Request.ContentType = "application/json"; - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(typeof(User)); - var context = new InputFormatterContext( - httpContext, - modelName: string.Empty, - modelState: modelState, - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); + + var formatterContext = CreateInputFormatterContext(typeof(User), httpContext); // Act - var result = await formatter.ReadAsync(context); + var result = await formatter.ReadAsync(formatterContext); // Assert Assert.False(result.HasError); + var userModel = Assert.IsType(result.Model); Assert.Equal("Person Name", userModel.Name); Assert.Equal(30, userModel.Age); Assert.False(httpContext.Request.Body.CanSeek); - result = await formatter.ReadAsync(context); + result = await formatter.ReadAsync(formatterContext); // Assert Assert.False(result.HasError); @@ -214,44 +156,87 @@ namespace Microsoft.AspNetCore.Mvc.Formatters } [Fact] - public async Task SuppressInputFormatterBufferingSetToTrue_UsingMutatedOptions() + public async Task Version_2_1_Constructor_SuppressInputFormatterBuffering_UsingMvcOptions_DoesNotBufferRequestBody() { // Arrange - var content = "{name: 'Person Name', Age: '30'}"; - var logger = GetLogger(); - var mvcOptions = new MvcOptions(); - mvcOptions.SuppressInputFormatterBuffering = false; - var formatter = - new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, mvcOptions); - var contentBytes = Encoding.UTF8.GetBytes(content); + var mvcOptions = new MvcOptions() + { + SuppressInputFormatterBuffering = true, + }; + var formatter = new JsonInputFormatter( + GetLogger(), + _serializerSettings, + ArrayPool.Shared, + _objectPoolProvider, + mvcOptions, + new MvcJsonOptions()); - var modelState = new ModelStateDictionary(); + var content = "{name: 'Person Name', Age: '30'}"; + var contentBytes = Encoding.UTF8.GetBytes(content); var httpContext = new DefaultHttpContext(); httpContext.Features.Set(new TestResponseFeature()); httpContext.Request.Body = new NonSeekableReadStream(contentBytes); httpContext.Request.ContentType = "application/json"; - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(typeof(User)); - var context = new InputFormatterContext( - httpContext, - modelName: string.Empty, - modelState: modelState, - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); + + var formatterContext = CreateInputFormatterContext(typeof(User), httpContext); + + // Act + var result = await formatter.ReadAsync(formatterContext); + + // Assert + Assert.False(result.HasError); + + var userModel = Assert.IsType(result.Model); + Assert.Equal("Person Name", userModel.Name); + Assert.Equal(30, userModel.Age); + + Assert.False(httpContext.Request.Body.CanSeek); + result = await formatter.ReadAsync(formatterContext); + + // Assert + Assert.False(result.HasError); + Assert.Null(result.Model); + } + + [Fact] + public async Task Version_2_1_Constructor_SuppressInputFormatterBufferingSetToTrue_UsingMutatedOptions() + { + // Arrange + var mvcOptions = new MvcOptions() + { + SuppressInputFormatterBuffering = false, + }; + var formatter = new JsonInputFormatter( + GetLogger(), + _serializerSettings, + ArrayPool.Shared, + _objectPoolProvider, + mvcOptions, + new MvcJsonOptions()); + + var content = "{name: 'Person Name', Age: '30'}"; + var contentBytes = Encoding.UTF8.GetBytes(content); + var httpContext = new DefaultHttpContext(); + httpContext.Features.Set(new TestResponseFeature()); + httpContext.Request.Body = new NonSeekableReadStream(contentBytes); + httpContext.Request.ContentType = "application/json"; + + var formatterContext = CreateInputFormatterContext(typeof(User), httpContext); // Act // Mutate options after passing into the constructor to make sure that the value type is not store in the constructor mvcOptions.SuppressInputFormatterBuffering = true; - var result = await formatter.ReadAsync(context); + var result = await formatter.ReadAsync(formatterContext); // Assert Assert.False(result.HasError); + var userModel = Assert.IsType(result.Model); Assert.Equal("Person Name", userModel.Name); Assert.Equal(30, userModel.Age); Assert.False(httpContext.Request.Body.CanSeek); - result = await formatter.ReadAsync(context); + result = await formatter.ReadAsync(formatterContext); // Assert Assert.False(result.HasError); @@ -277,21 +262,12 @@ namespace Microsoft.AspNetCore.Mvc.Formatters public void CanRead_ReturnsTrueForAnySupportedContentType(string requestContentType, bool expectedCanRead) { // Arrange - var loggerMock = GetLogger(); + var formatter = CreateFormatter(); - var formatter = - new JsonInputFormatter(loggerMock, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); var contentBytes = Encoding.UTF8.GetBytes("content"); - var httpContext = GetHttpContext(contentBytes, contentType: requestContentType); - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(typeof(string)); - var formatterContext = new InputFormatterContext( - httpContext, - modelName: string.Empty, - modelState: new ModelStateDictionary(), - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); + + var formatterContext = CreateInputFormatterContext(typeof(string), httpContext); // Act var result = formatter.CanRead(formatterContext); @@ -304,9 +280,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters public void DefaultMediaType_ReturnsApplicationJson() { // Arrange - var loggerMock = GetLogger(); - var formatter = - new JsonInputFormatter(loggerMock, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); + var formatter = CreateFormatter(); // Act var mediaType = formatter.SupportedMediaTypes[0]; @@ -321,8 +295,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters { yield return new object[] { "100", typeof(int), 100 }; yield return new object[] { "'abcd'", typeof(string), "abcd" }; - yield return new object[] { "'2012-02-01 12:45 AM'", typeof(DateTime), - new DateTime(2012, 02, 01, 00, 45, 00) }; + yield return new object[] { "'2012-02-01 12:45 AM'", typeof(DateTime), new DateTime(2012, 02, 01, 00, 45, 00) }; } } @@ -331,23 +304,15 @@ namespace Microsoft.AspNetCore.Mvc.Formatters public async Task JsonFormatterReadsSimpleTypes(string content, Type type, object expected) { // Arrange - var logger = GetLogger(); - var formatter = - new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); - var contentBytes = Encoding.UTF8.GetBytes(content); + var formatter = CreateFormatter(); + var contentBytes = Encoding.UTF8.GetBytes(content); var httpContext = GetHttpContext(contentBytes); - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(type); - var context = new InputFormatterContext( - httpContext, - modelName: string.Empty, - modelState: new ModelStateDictionary(), - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); + + var formatterContext = CreateInputFormatterContext(type, httpContext); // Act - var result = await formatter.ReadAsync(context); + var result = await formatter.ReadAsync(formatterContext); // Assert Assert.False(result.HasError); @@ -358,24 +323,16 @@ namespace Microsoft.AspNetCore.Mvc.Formatters public async Task JsonFormatterReadsComplexTypes() { // Arrange - var content = "{name: 'Person Name', Age: '30'}"; - var logger = GetLogger(); - var formatter = - new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); - var contentBytes = Encoding.UTF8.GetBytes(content); + var formatter = CreateFormatter(); + var content = "{name: 'Person Name', Age: '30'}"; + var contentBytes = Encoding.UTF8.GetBytes(content); var httpContext = GetHttpContext(contentBytes); - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(typeof(User)); - var context = new InputFormatterContext( - httpContext, - modelName: string.Empty, - modelState: new ModelStateDictionary(), - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); + + var formatterContext = CreateInputFormatterContext(typeof(User), httpContext); // Act - var result = await formatter.ReadAsync(context); + var result = await formatter.ReadAsync(formatterContext); // Assert Assert.False(result.HasError); @@ -388,25 +345,16 @@ namespace Microsoft.AspNetCore.Mvc.Formatters public async Task ReadAsync_ReadsValidArray() { // Arrange - var content = "[0, 23, 300]"; - var logger = GetLogger(); - var formatter = - new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); - var contentBytes = Encoding.UTF8.GetBytes(content); + var formatter = CreateFormatter(); - var modelState = new ModelStateDictionary(); + var content = "[0, 23, 300]"; + var contentBytes = Encoding.UTF8.GetBytes(content); var httpContext = GetHttpContext(contentBytes); - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(typeof(int[])); - var context = new InputFormatterContext( - httpContext, - modelName: string.Empty, - modelState: modelState, - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); + + var formatterContext = CreateInputFormatterContext(typeof(int[]), httpContext); // Act - var result = await formatter.ReadAsync(context); + var result = await formatter.ReadAsync(formatterContext); // Assert Assert.False(result.HasError); @@ -422,25 +370,16 @@ namespace Microsoft.AspNetCore.Mvc.Formatters public async Task ReadAsync_ReadsValidArray_AsList(Type requestedType) { // Arrange - var content = "[0, 23, 300]"; - var logger = GetLogger(); - var formatter = - new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); - var contentBytes = Encoding.UTF8.GetBytes(content); + var formatter = CreateFormatter(); - var modelState = new ModelStateDictionary(); + var content = "[0, 23, 300]"; + var contentBytes = Encoding.UTF8.GetBytes(content); var httpContext = GetHttpContext(contentBytes); - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(requestedType); - var context = new InputFormatterContext( - httpContext, - modelName: string.Empty, - modelState: modelState, - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); + + var formatterContext = CreateInputFormatterContext(requestedType, httpContext); // Act - var result = await formatter.ReadAsync(context); + var result = await formatter.ReadAsync(formatterContext); // Assert Assert.False(result.HasError); @@ -452,126 +391,90 @@ namespace Microsoft.AspNetCore.Mvc.Formatters public async Task ReadAsync_AddsModelValidationErrorsToModelState() { // Arrange - var content = "{name: 'Person Name', Age: 'not-an-age'}"; - var logger = GetLogger(); - var formatter = - new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); - var contentBytes = Encoding.UTF8.GetBytes(content); + var formatter = CreateFormatter(allowInputFormatterExceptionMessages: true); - var modelState = new ModelStateDictionary(); + var content = "{name: 'Person Name', Age: 'not-an-age'}"; + var contentBytes = Encoding.UTF8.GetBytes(content); var httpContext = GetHttpContext(contentBytes); - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(typeof(User)); - var context = new InputFormatterContext( - httpContext, - modelName: string.Empty, - modelState: modelState, - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); + + var formatterContext = CreateInputFormatterContext(typeof(User), httpContext); // Act - var result = await formatter.ReadAsync(context); + var result = await formatter.ReadAsync(formatterContext); // Assert Assert.True(result.HasError); Assert.Equal( "Could not convert string to decimal: not-an-age. Path 'Age', line 1, position 39.", - modelState["Age"].Errors[0].ErrorMessage); + formatterContext.ModelState["Age"].Errors[0].ErrorMessage); } [Fact] public async Task ReadAsync_InvalidArray_AddsOverflowErrorsToModelState() { // Arrange - var content = "[0, 23, 300]"; - var logger = GetLogger(); - var formatter = - new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); - var contentBytes = Encoding.UTF8.GetBytes(content); + var formatter = CreateFormatter(allowInputFormatterExceptionMessages: true); - var modelState = new ModelStateDictionary(); + var content = "[0, 23, 300]"; + var contentBytes = Encoding.UTF8.GetBytes(content); var httpContext = GetHttpContext(contentBytes); - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(typeof(byte[])); - var context = new InputFormatterContext( - httpContext, - modelName: string.Empty, - modelState: modelState, - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); + + var formatterContext = CreateInputFormatterContext(typeof(byte[]), httpContext); // Act - var result = await formatter.ReadAsync(context); + var result = await formatter.ReadAsync(formatterContext); // Assert Assert.True(result.HasError); - Assert.Equal("The supplied value is invalid.", modelState["[2]"].Errors[0].ErrorMessage); - Assert.Null(modelState["[2]"].Errors[0].Exception); + Assert.Equal("The supplied value is invalid.", formatterContext.ModelState["[2]"].Errors[0].ErrorMessage); + Assert.Null(formatterContext.ModelState["[2]"].Errors[0].Exception); } [Fact] public async Task ReadAsync_InvalidComplexArray_AddsOverflowErrorsToModelState() { // Arrange - var content = "[{name: 'Name One', Age: 30}, {name: 'Name Two', Small: 300}]"; - var logger = GetLogger(); - var formatter = - new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); - var contentBytes = Encoding.UTF8.GetBytes(content); + var formatter = CreateFormatter(allowInputFormatterExceptionMessages: true); - var modelState = new ModelStateDictionary(); + var content = "[{name: 'Name One', Age: 30}, {name: 'Name Two', Small: 300}]"; + var contentBytes = Encoding.UTF8.GetBytes(content); var httpContext = GetHttpContext(contentBytes); - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(typeof(User[])); - var context = new InputFormatterContext( - httpContext, - modelName: "names", - modelState: modelState, - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); + + var formatterContext = CreateInputFormatterContext(typeof(User[]), httpContext, modelName: "names"); // Act - var result = await formatter.ReadAsync(context); + var result = await formatter.ReadAsync(formatterContext); // Assert Assert.True(result.HasError); Assert.Equal( "Error converting value 300 to type 'System.Byte'. Path '[1].Small', line 1, position 59.", - modelState["names[1].Small"].Errors[0].ErrorMessage); + formatterContext.ModelState["names[1].Small"].Errors[0].ErrorMessage); } [Fact] public async Task ReadAsync_UsesTryAddModelValidationErrorsToModelState() { // Arrange + var formatter = CreateFormatter(); + var content = "{name: 'Person Name', Age: 'not-an-age'}"; - var logger = GetLogger(); - var formatter = - new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); var contentBytes = Encoding.UTF8.GetBytes(content); - - var modelState = new ModelStateDictionary(); var httpContext = GetHttpContext(contentBytes); - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(typeof(User)); - var context = new InputFormatterContext( - httpContext, - modelName: string.Empty, - modelState: modelState, - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); - modelState.MaxAllowedErrors = 3; - modelState.AddModelError("key1", "error1"); - modelState.AddModelError("key2", "error2"); + var formatterContext = CreateInputFormatterContext(typeof(User), httpContext); + formatterContext.ModelState.MaxAllowedErrors = 3; + formatterContext.ModelState.AddModelError("key1", "error1"); + formatterContext.ModelState.AddModelError("key2", "error2"); // Act - var result = await formatter.ReadAsync(context); + var result = await formatter.ReadAsync(formatterContext); // Assert Assert.True(result.HasError); - Assert.False(modelState.ContainsKey("age")); - var error = Assert.Single(modelState[""].Errors); + + Assert.False(formatterContext.ModelState.ContainsKey("age")); + var error = Assert.Single(formatterContext.ModelState[""].Errors); Assert.IsType(error.Exception); } @@ -580,28 +483,24 @@ namespace Microsoft.AspNetCore.Mvc.Formatters [InlineData("null", false, false)] [InlineData(" ", true, true)] [InlineData(" ", false, false)] - public async Task ReadAsync_WithInputThatDeserializesToNull_SetsModelOnlyIfAllowingEmptyInput(string content, bool allowEmptyInput, bool expectedIsModelSet) + public async Task ReadAsync_WithInputThatDeserializesToNull_SetsModelOnlyIfAllowingEmptyInput( + string content, + bool treatEmptyInputAsDefaultValue, + bool expectedIsModelSet) { // Arrange - var logger = GetLogger(); - var formatter = - new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); - var contentBytes = Encoding.UTF8.GetBytes(content); + var formatter = CreateFormatter(); - var modelState = new ModelStateDictionary(); + var contentBytes = Encoding.UTF8.GetBytes(content); var httpContext = GetHttpContext(contentBytes); - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(typeof(object)); - var context = new InputFormatterContext( + + var formatterContext = CreateInputFormatterContext( + typeof(object), httpContext, - modelName: string.Empty, - modelState: modelState, - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader, - treatEmptyInputAsDefaultValue: allowEmptyInput); + treatEmptyInputAsDefaultValue: treatEmptyInputAsDefaultValue); // Act - var result = await formatter.ReadAsync(context); + var result = await formatter.ReadAsync(formatterContext); // Assert Assert.False(result.HasError); @@ -616,45 +515,35 @@ namespace Microsoft.AspNetCore.Mvc.Formatters var serializerSettings = new JsonSerializerSettings(); // Act - var jsonFormatter = new TestableJsonInputFormatter(serializerSettings); + var formatter = new TestableJsonInputFormatter(serializerSettings); // Assert - Assert.Same(serializerSettings, jsonFormatter.SerializerSettings); + Assert.Same(serializerSettings, formatter.SerializerSettings); } [Fact] public async Task CustomSerializerSettingsObject_TakesEffect() { // Arrange - // missing password property here - var contentBytes = Encoding.UTF8.GetBytes("{ \"UserName\" : \"John\"}"); - var logger = GetLogger(); - // by default we ignore missing members, so here explicitly changing it var serializerSettings = new JsonSerializerSettings { MissingMemberHandling = MissingMemberHandling.Error }; - var jsonFormatter = - new JsonInputFormatter(logger, serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); + var formatter = CreateFormatter(serializerSettings, allowInputFormatterExceptionMessages: true); - var modelState = new ModelStateDictionary(); + // missing password property here + var contentBytes = Encoding.UTF8.GetBytes("{ \"UserName\" : \"John\"}"); var httpContext = GetHttpContext(contentBytes, "application/json;charset=utf-8"); - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(typeof(UserLogin)); - var inputFormatterContext = new InputFormatterContext( - httpContext, - modelName: string.Empty, - modelState: modelState, - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); + + var formatterContext = CreateInputFormatterContext(typeof(UserLogin), httpContext); // Act - var result = await jsonFormatter.ReadAsync(inputFormatterContext); + var result = await formatter.ReadAsync(formatterContext); // Assert Assert.True(result.HasError); - Assert.False(modelState.IsValid); + Assert.False(formatterContext.ModelState.IsValid); - var modelErrorMessage = modelState.Values.First().Errors[0].ErrorMessage; - Assert.Contains("Required property 'Password' not found in JSON", modelErrorMessage); + var message = formatterContext.ModelState.Values.First().Errors[0].ErrorMessage; + Assert.Contains("Required property 'Password' not found in JSON", message); } [Fact] @@ -684,86 +573,98 @@ namespace Microsoft.AspNetCore.Mvc.Formatters [InlineData("{\"age\":\"x\"}", "age", "Could not convert string to decimal: x. Path 'age', line 1, position 10.")] [InlineData("{\"login\":1}", "login", "Error converting value 1 to type 'Microsoft.AspNetCore.Mvc.Formatters.JsonInputFormatterTest+UserLogin'. Path 'login', line 1, position 10.")] [InlineData("{\"login\":{\"username\":\"somevalue\"}}", "login", "Required property 'Password' not found in JSON. Path 'login', line 1, position 33.")] - public async Task ReadAsync_RegistersJsonInputExceptionsAsInputFormatterException( + public async Task ReadAsync_WithAllowInputFormatterExceptionMessages_RegistersJsonInputExceptionsAsInputFormatterException( string content, string modelStateKey, string expectedMessage) { // Arrange - var logger = GetLogger(); - var formatter = - new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); - var contentBytes = Encoding.UTF8.GetBytes(content); + var formatter = CreateFormatter(allowInputFormatterExceptionMessages: true); - var modelState = new ModelStateDictionary(); + var contentBytes = Encoding.UTF8.GetBytes(content); var httpContext = GetHttpContext(contentBytes); - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(typeof(User)); - var context = new InputFormatterContext( - httpContext, - modelName: string.Empty, - modelState: modelState, - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); + + var formatterContext = CreateInputFormatterContext(typeof(User), httpContext); // Act - var result = await formatter.ReadAsync(context); + var result = await formatter.ReadAsync(formatterContext); // Assert Assert.True(result.HasError); - Assert.True(!modelState.IsValid); - Assert.True(modelState.ContainsKey(modelStateKey)); + Assert.True(!formatterContext.ModelState.IsValid); + Assert.True(formatterContext.ModelState.ContainsKey(modelStateKey)); - var modelError = modelState[modelStateKey].Errors.Single(); + var modelError = formatterContext.ModelState[modelStateKey].Errors.Single(); Assert.Equal(expectedMessage, modelError.ErrorMessage); } [Fact] - public async Task ReadAsync_WhenSuppressJsonDeserializationExceptionMessagesIsTrue_DoesNotWrapJsonInputExceptions() + public async Task ReadAsync_DefaultOptions_DoesNotWrapJsonInputExceptions() { // Arrange - var logger = GetLogger(); var formatter = new JsonInputFormatter( - logger, + GetLogger(), _serializerSettings, ArrayPool.Shared, _objectPoolProvider, - new MvcOptions() - { - SuppressInputFormatterBuffering = false, - SuppressJsonDeserializationExceptionMessagesInModelState = true - }); - var contentBytes = Encoding.UTF8.GetBytes("{"); - var modelStateKey = string.Empty; + new MvcOptions(), + new MvcJsonOptions()); - var modelState = new ModelStateDictionary(); + var contentBytes = Encoding.UTF8.GetBytes("{"); var httpContext = GetHttpContext(contentBytes); - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(typeof(User)); - var context = new InputFormatterContext( - httpContext, - modelName: string.Empty, - modelState: modelState, - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); + + var formatterContext = CreateInputFormatterContext(typeof(User), httpContext); // Act - var result = await formatter.ReadAsync(context); + var result = await formatter.ReadAsync(formatterContext); // Assert Assert.True(result.HasError); - Assert.True(!modelState.IsValid); - Assert.True(modelState.ContainsKey(modelStateKey)); + Assert.True(!formatterContext.ModelState.IsValid); + Assert.True(formatterContext.ModelState.ContainsKey(string.Empty)); - var modelError = modelState[modelStateKey].Errors.Single(); + var modelError = formatterContext.ModelState[string.Empty].Errors.Single(); Assert.IsNotType(modelError.Exception); Assert.Empty(modelError.ErrorMessage); } + [Fact] + public async Task ReadAsync_AllowInputFormatterExceptionMessages_DoesNotWrapJsonInputExceptions() + { + // Arrange + var formatter = new JsonInputFormatter( + GetLogger(), + _serializerSettings, + ArrayPool.Shared, + _objectPoolProvider, + new MvcOptions(), + new MvcJsonOptions() + { + AllowInputFormatterExceptionMessages = true, + }); + + var contentBytes = Encoding.UTF8.GetBytes("{"); + var httpContext = GetHttpContext(contentBytes); + + var formatterContext = CreateInputFormatterContext(typeof(User), httpContext); + + // Act + var result = await formatter.ReadAsync(formatterContext); + + // Assert + Assert.True(result.HasError); + Assert.True(!formatterContext.ModelState.IsValid); + Assert.True(formatterContext.ModelState.ContainsKey(string.Empty)); + + var modelError = formatterContext.ModelState[string.Empty].Errors.Single(); + Assert.Null(modelError.Exception); + Assert.NotEmpty(modelError.ErrorMessage); + } + private class TestableJsonInputFormatter : JsonInputFormatter { public TestableJsonInputFormatter(JsonSerializerSettings settings) - : base(GetLogger(), settings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()) + : base(GetLogger(), settings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions(), new MvcJsonOptions()) { } @@ -777,6 +678,20 @@ namespace Microsoft.AspNetCore.Mvc.Formatters return NullLogger.Instance; } + private JsonInputFormatter CreateFormatter(JsonSerializerSettings serializerSettings = null, bool allowInputFormatterExceptionMessages = false) + { + return new JsonInputFormatter( + GetLogger(), + serializerSettings ?? _serializerSettings, + ArrayPool.Shared, + _objectPoolProvider, + new MvcOptions(), + new MvcJsonOptions() + { + AllowInputFormatterExceptionMessages = allowInputFormatterExceptionMessages, + }); + } + private static HttpContext GetHttpContext( byte[] contentBytes, string contentType = "application/json") @@ -800,6 +715,24 @@ namespace Microsoft.AspNetCore.Mvc.Formatters return httpContext.Object; } + private InputFormatterContext CreateInputFormatterContext( + Type modelType, + HttpContext httpContext, + string modelName = null, + bool treatEmptyInputAsDefaultValue = false) + { + var provider = new EmptyModelMetadataProvider(); + var metadata = provider.GetMetadataForType(modelType); + + return new InputFormatterContext( + httpContext, + modelName: modelName ?? string.Empty, + modelState: new ModelStateDictionary(), + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader, + treatEmptyInputAsDefaultValue: treatEmptyInputAsDefaultValue); + } + private IEnumerable GetModelStateErrorMessages(ModelStateDictionary modelStateDictionary) { var allErrorMessages = new List(); diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonPatchInputFormatterTest.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonPatchInputFormatterTest.cs index a998486402..dc097cd5c4 100644 --- a/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonPatchInputFormatterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonPatchInputFormatterTest.cs @@ -26,136 +26,128 @@ namespace Microsoft.AspNetCore.Mvc.Formatters private static readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings(); [Fact] - public async Task BuffersRequestBody_ByDefault() + public async Task Version_2_0_Constructor_BuffersRequestBody_ByDefault() { // Arrange - var logger = GetLogger(); #pragma warning disable CS0618 - var formatter = - new JsonPatchInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider); + var formatter = new JsonPatchInputFormatter( + GetLogger(), + _serializerSettings, + ArrayPool.Shared, + _objectPoolProvider); #pragma warning restore CS0618 + var content = "[{\"op\":\"add\",\"path\":\"Customer/Name\",\"value\":\"John\"}]"; var contentBytes = Encoding.UTF8.GetBytes(content); - var modelState = new ModelStateDictionary(); var httpContext = new DefaultHttpContext(); httpContext.Features.Set(new TestResponseFeature()); httpContext.Request.Body = new NonSeekableReadStream(contentBytes); httpContext.Request.ContentType = "application/json"; - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(typeof(JsonPatchDocument)); - var context = new InputFormatterContext( - httpContext, - modelName: string.Empty, - modelState: modelState, - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); + + var formatterContext = CreateInputFormatterContext(typeof(JsonPatchDocument), httpContext); // Act - var result = await formatter.ReadAsync(context); + var result = await formatter.ReadAsync(formatterContext); // Assert Assert.False(result.HasError); - var patchDoc = Assert.IsType>(result.Model); - Assert.Equal("add", patchDoc.Operations[0].op); - Assert.Equal("Customer/Name", patchDoc.Operations[0].path); - Assert.Equal("John", patchDoc.Operations[0].value); + var patchDocument = Assert.IsType>(result.Model); + Assert.Equal("add", patchDocument.Operations[0].op); + Assert.Equal("Customer/Name", patchDocument.Operations[0].path); + Assert.Equal("John", patchDocument.Operations[0].value); Assert.True(httpContext.Request.Body.CanSeek); httpContext.Request.Body.Seek(0L, SeekOrigin.Begin); - result = await formatter.ReadAsync(context); + result = await formatter.ReadAsync(formatterContext); // Assert Assert.False(result.HasError); - patchDoc = Assert.IsType>(result.Model); - Assert.Equal("add", patchDoc.Operations[0].op); - Assert.Equal("Customer/Name", patchDoc.Operations[0].path); - Assert.Equal("John", patchDoc.Operations[0].value); + patchDocument = Assert.IsType>(result.Model); + Assert.Equal("add", patchDocument.Operations[0].op); + Assert.Equal("Customer/Name", patchDocument.Operations[0].path); + Assert.Equal("John", patchDocument.Operations[0].value); } [Fact] - public async Task BuffersRequestBody_UsingDefaultOptions() + public async Task Version_2_1_Constructor_BuffersRequestBody_ByDefault() { // Arrange - var logger = GetLogger(); - var formatter = - new JsonPatchInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); + var formatter = new JsonPatchInputFormatter( + GetLogger(), + _serializerSettings, + ArrayPool.Shared, + _objectPoolProvider, + new MvcOptions(), + new MvcJsonOptions()); + var content = "[{\"op\":\"add\",\"path\":\"Customer/Name\",\"value\":\"John\"}]"; var contentBytes = Encoding.UTF8.GetBytes(content); - var modelState = new ModelStateDictionary(); var httpContext = new DefaultHttpContext(); httpContext.Features.Set(new TestResponseFeature()); httpContext.Request.Body = new NonSeekableReadStream(contentBytes); httpContext.Request.ContentType = "application/json"; - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(typeof(JsonPatchDocument)); - var context = new InputFormatterContext( - httpContext, - modelName: string.Empty, - modelState: modelState, - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); + + var formatterContext = CreateInputFormatterContext(typeof(JsonPatchDocument), httpContext); // Act - var result = await formatter.ReadAsync(context); + var result = await formatter.ReadAsync(formatterContext); // Assert Assert.False(result.HasError); - var patchDoc = Assert.IsType>(result.Model); - Assert.Equal("add", patchDoc.Operations[0].op); - Assert.Equal("Customer/Name", patchDoc.Operations[0].path); - Assert.Equal("John", patchDoc.Operations[0].value); + var patchDocument = Assert.IsType>(result.Model); + Assert.Equal("add", patchDocument.Operations[0].op); + Assert.Equal("Customer/Name", patchDocument.Operations[0].path); + Assert.Equal("John", patchDocument.Operations[0].value); Assert.True(httpContext.Request.Body.CanSeek); httpContext.Request.Body.Seek(0L, SeekOrigin.Begin); - result = await formatter.ReadAsync(context); + result = await formatter.ReadAsync(formatterContext); // Assert Assert.False(result.HasError); - patchDoc = Assert.IsType>(result.Model); - Assert.Equal("add", patchDoc.Operations[0].op); - Assert.Equal("Customer/Name", patchDoc.Operations[0].path); - Assert.Equal("John", patchDoc.Operations[0].value); + patchDocument = Assert.IsType>(result.Model); + Assert.Equal("add", patchDocument.Operations[0].op); + Assert.Equal("Customer/Name", patchDocument.Operations[0].path); + Assert.Equal("John", patchDocument.Operations[0].value); } [Fact] - public async Task SuppressInputFormatterBufferingSetToTrue_DoesNotBufferRequestBody() + public async Task Version_2_0_Constructor_SuppressInputFormatterBuffering_DoesNotBufferRequestBody() { // Arrange - var logger = GetLogger(); #pragma warning disable CS0618 - var formatter = - new JsonPatchInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, suppressInputFormatterBuffering: true); + var formatter = new JsonPatchInputFormatter( + GetLogger(), + _serializerSettings, + ArrayPool.Shared, + _objectPoolProvider, + suppressInputFormatterBuffering: true); #pragma warning restore CS0618 + var content = "[{\"op\":\"add\",\"path\":\"Customer/Name\",\"value\":\"John\"}]"; var contentBytes = Encoding.UTF8.GetBytes(content); - var modelState = new ModelStateDictionary(); var httpContext = new DefaultHttpContext(); httpContext.Features.Set(new TestResponseFeature()); httpContext.Request.Body = new NonSeekableReadStream(contentBytes); httpContext.Request.ContentType = "application/json"; - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(typeof(JsonPatchDocument)); - var context = new InputFormatterContext( - httpContext, - modelName: string.Empty, - modelState: modelState, - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); + + var context = CreateInputFormatterContext(typeof(JsonPatchDocument), httpContext); // Act var result = await formatter.ReadAsync(context); // Assert Assert.False(result.HasError); - var patchDoc = Assert.IsType>(result.Model); - Assert.Equal("add", patchDoc.Operations[0].op); - Assert.Equal("Customer/Name", patchDoc.Operations[0].path); - Assert.Equal("John", patchDoc.Operations[0].value); + + var patchDocument = Assert.IsType>(result.Model); + Assert.Equal("add", patchDocument.Operations[0].op); + Assert.Equal("Customer/Name", patchDocument.Operations[0].path); + Assert.Equal("John", patchDocument.Operations[0].value); Assert.False(httpContext.Request.Body.CanSeek); result = await formatter.ReadAsync(context); @@ -166,49 +158,46 @@ namespace Microsoft.AspNetCore.Mvc.Formatters } [Fact] - public async Task SuppressInputFormatterBufferingSetToTrue_UsingMutatedOptions_DoesNotBufferRequestBody() + public async Task Version_2_1_Constructor_SuppressInputFormatterBuffering_DoesNotBufferRequestBody() { // Arrange - var logger = GetLogger(); - var mvcOptions = new MvcOptions(); - mvcOptions.SuppressInputFormatterBuffering = false; + var mvcOptions = new MvcOptions() + { + SuppressInputFormatterBuffering = false, + }; var formatter = new JsonPatchInputFormatter( - logger, + GetLogger(), _serializerSettings, ArrayPool.Shared, _objectPoolProvider, - mvcOptions); + mvcOptions, + new MvcJsonOptions()); + var content = "[{\"op\":\"add\",\"path\":\"Customer/Name\",\"value\":\"John\"}]"; var contentBytes = Encoding.UTF8.GetBytes(content); - - var modelState = new ModelStateDictionary(); + var httpContext = new DefaultHttpContext(); httpContext.Features.Set(new TestResponseFeature()); httpContext.Request.Body = new NonSeekableReadStream(contentBytes); httpContext.Request.ContentType = "application/json"; - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(typeof(JsonPatchDocument)); - var context = new InputFormatterContext( - httpContext, - modelName: string.Empty, - modelState: modelState, - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); + + var formatterContext = CreateInputFormatterContext(typeof(JsonPatchDocument), httpContext); // Act // Mutate options after passing into the constructor to make sure that the value type is not store in the constructor mvcOptions.SuppressInputFormatterBuffering = true; - var result = await formatter.ReadAsync(context); + var result = await formatter.ReadAsync(formatterContext); // Assert Assert.False(result.HasError); - var patchDoc = Assert.IsType>(result.Model); - Assert.Equal("add", patchDoc.Operations[0].op); - Assert.Equal("Customer/Name", patchDoc.Operations[0].path); - Assert.Equal("John", patchDoc.Operations[0].value); + + var patchDocument = Assert.IsType>(result.Model); + Assert.Equal("add", patchDocument.Operations[0].op); + Assert.Equal("Customer/Name", patchDocument.Operations[0].path); + Assert.Equal("John", patchDocument.Operations[0].value); Assert.False(httpContext.Request.Body.CanSeek); - result = await formatter.ReadAsync(context); + result = await formatter.ReadAsync(formatterContext); // Assert Assert.False(result.HasError); @@ -219,67 +208,49 @@ namespace Microsoft.AspNetCore.Mvc.Formatters public async Task JsonPatchInputFormatter_ReadsOneOperation_Successfully() { // Arrange - var logger = GetLogger(); - var formatter = - new JsonPatchInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); + var formatter = CreateFormatter(); + var content = "[{\"op\":\"add\",\"path\":\"Customer/Name\",\"value\":\"John\"}]"; var contentBytes = Encoding.UTF8.GetBytes(content); + var httpContext = CreateHttpContext(contentBytes); - var modelState = new ModelStateDictionary(); - var httpContext = GetHttpContext(contentBytes); - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(typeof(JsonPatchDocument)); - var context = new InputFormatterContext( - httpContext, - modelName: string.Empty, - modelState: modelState, - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); + var formatterContext = CreateInputFormatterContext(typeof(JsonPatchDocument), httpContext); // Act - var result = await formatter.ReadAsync(context); + var result = await formatter.ReadAsync(formatterContext); // Assert Assert.False(result.HasError); - var patchDoc = Assert.IsType>(result.Model); - Assert.Equal("add", patchDoc.Operations[0].op); - Assert.Equal("Customer/Name", patchDoc.Operations[0].path); - Assert.Equal("John", patchDoc.Operations[0].value); + var patchDocument = Assert.IsType>(result.Model); + Assert.Equal("add", patchDocument.Operations[0].op); + Assert.Equal("Customer/Name", patchDocument.Operations[0].path); + Assert.Equal("John", patchDocument.Operations[0].value); } [Fact] public async Task JsonPatchInputFormatter_ReadsMultipleOperations_Successfully() { // Arrange - var logger = GetLogger(); - var formatter = - new JsonPatchInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); + var formatter = CreateFormatter(); + var content = "[{\"op\": \"add\", \"path\" : \"Customer/Name\", \"value\":\"John\"}," + "{\"op\": \"remove\", \"path\" : \"Customer/Name\"}]"; var contentBytes = Encoding.UTF8.GetBytes(content); + var httpContext = CreateHttpContext(contentBytes); - var modelState = new ModelStateDictionary(); - var httpContext = GetHttpContext(contentBytes); - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(typeof(JsonPatchDocument)); - var context = new InputFormatterContext( - httpContext, - modelName: string.Empty, - modelState: modelState, - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); + var formatterContext = CreateInputFormatterContext(typeof(JsonPatchDocument), httpContext); // Act - var result = await formatter.ReadAsync(context); + var result = await formatter.ReadAsync(formatterContext); // Assert Assert.False(result.HasError); - var patchDoc = Assert.IsType>(result.Model); - Assert.Equal("add", patchDoc.Operations[0].op); - Assert.Equal("Customer/Name", patchDoc.Operations[0].path); - Assert.Equal("John", patchDoc.Operations[0].value); - Assert.Equal("remove", patchDoc.Operations[1].op); - Assert.Equal("Customer/Name", patchDoc.Operations[1].path); + var patchDocument = Assert.IsType>(result.Model); + Assert.Equal("add", patchDocument.Operations[0].op); + Assert.Equal("Customer/Name", patchDocument.Operations[0].path); + Assert.Equal("John", patchDocument.Operations[0].value); + Assert.Equal("remove", patchDocument.Operations[1].op); + Assert.Equal("Customer/Name", patchDocument.Operations[1].path); } [Theory] @@ -290,22 +261,13 @@ namespace Microsoft.AspNetCore.Mvc.Formatters public void CanRead_ReturnsTrueOnlyForJsonPatchContentType(string requestContentType, bool expectedCanRead) { // Arrange - var logger = GetLogger(); - var formatter = - new JsonPatchInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); + var formatter = CreateFormatter(); + var content = "[{\"op\": \"add\", \"path\" : \"Customer/Name\", \"value\":\"John\"}]"; var contentBytes = Encoding.UTF8.GetBytes(content); + var httpContext = CreateHttpContext(contentBytes, contentType: requestContentType); - var modelState = new ModelStateDictionary(); - var httpContext = GetHttpContext(contentBytes, contentType: requestContentType); - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(typeof(JsonPatchDocument)); - var formatterContext = new InputFormatterContext( - httpContext, - modelName: string.Empty, - modelState: modelState, - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); + var formatterContext = CreateInputFormatterContext(typeof(JsonPatchDocument), httpContext); // Act var result = formatter.CanRead(formatterContext); @@ -320,22 +282,15 @@ namespace Microsoft.AspNetCore.Mvc.Formatters public void CanRead_ReturnsFalse_NonJsonPatchContentType(Type modelType) { // Arrange - var logger = GetLogger(); - var formatter = - new JsonPatchInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); + var formatter = CreateFormatter(); + var content = "[{\"op\": \"add\", \"path\" : \"Customer/Name\", \"value\":\"John\"}]"; var contentBytes = Encoding.UTF8.GetBytes(content); + var httpContext = CreateHttpContext(contentBytes, contentType: "application/json-patch+json"); - var modelState = new ModelStateDictionary(); - var httpContext = GetHttpContext(contentBytes, contentType: "application/json-patch+json"); var provider = new EmptyModelMetadataProvider(); var metadata = provider.GetMetadataForType(modelType); - var formatterContext = new InputFormatterContext( - httpContext, - modelName: string.Empty, - modelState: modelState, - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); + var formatterContext = CreateInputFormatterContext(modelType, httpContext); // Act var result = formatter.CanRead(formatterContext); @@ -351,29 +306,21 @@ namespace Microsoft.AspNetCore.Mvc.Formatters var exceptionMessage = "Cannot deserialize the current JSON array (e.g. [1,2,3]) into type " + $"'{typeof(Customer).FullName}' because the type requires a JSON object "; - var logger = GetLogger(); - var formatter = - new JsonPatchInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); + // This test relies on 2.1 error message behavior + var formatter = CreateFormatter(allowInputFormatterExceptionMessages: true); + var content = "[{\"op\": \"add\", \"path\" : \"Customer/Name\", \"value\":\"John\"}]"; var contentBytes = Encoding.UTF8.GetBytes(content); + var httpContext = CreateHttpContext(contentBytes, contentType: "application/json-patch+json"); - var modelState = new ModelStateDictionary(); - var httpContext = GetHttpContext(contentBytes, contentType: "application/json-patch+json"); - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(typeof(Customer)); - var context = new InputFormatterContext( - httpContext, - modelName: string.Empty, - modelState: modelState, - metadata: metadata, - readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); + var formatterContext = CreateInputFormatterContext(typeof(Customer), httpContext); // Act - var result = await formatter.ReadAsync(context); + var result = await formatter.ReadAsync(formatterContext); // Assert Assert.True(result.HasError); - Assert.Contains(exceptionMessage, modelState[""].Errors[0].ErrorMessage); + Assert.Contains(exceptionMessage, formatterContext.ModelState[""].Errors[0].ErrorMessage); } private static ILogger GetLogger() @@ -381,7 +328,34 @@ namespace Microsoft.AspNetCore.Mvc.Formatters return NullLogger.Instance; } - private static HttpContext GetHttpContext( + private JsonPatchInputFormatter CreateFormatter(bool allowInputFormatterExceptionMessages = false) + { + return new JsonPatchInputFormatter( + NullLogger.Instance, + _serializerSettings, + ArrayPool.Shared, + _objectPoolProvider, + new MvcOptions(), + new MvcJsonOptions() + { + AllowInputFormatterExceptionMessages = allowInputFormatterExceptionMessages, + }); + } + + private InputFormatterContext CreateInputFormatterContext(Type modelType, HttpContext httpContext) + { + var provider = new EmptyModelMetadataProvider(); + var metadata = provider.GetMetadataForType(modelType); + + return new InputFormatterContext( + httpContext, + modelName: string.Empty, + modelState: new ModelStateDictionary(), + metadata: metadata, + readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader); + } + + private static HttpContext CreateHttpContext( byte[] contentBytes, string contentType = "application/json-patch+json") { diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/InputFormatterTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/InputFormatterTests.cs index 0b6c52aab2..21a4b615d7 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/InputFormatterTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/InputFormatterTests.cs @@ -97,7 +97,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } - [Fact] + [Fact] // This test covers the 2.0 behavior. JSON.Net error messages are not preserved. public async Task JsonInputFormatter_SuppliedJsonDeserializationErrorMessage() { // Arrange @@ -109,7 +109,9 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - Assert.Equal("{\"\":[\"Unexpected end when reading JSON. Path '', line 1, position 1.\"]}", responseBody); + + // Update me in 3.0 xD + Assert.Equal("{\"\":[\"The input was not valid.\"]}", responseBody); } [Theory] diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs index 788b0d8da8..82970be594 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Options; +using Newtonsoft.Json; using Xunit; namespace Microsoft.AspNetCore.Mvc.IntegrationTests @@ -448,7 +449,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests public int Address { get; set; } } - [Fact] + [Fact] // This tests the 2.0 behavior. Error messages from JSON.NET are not preserved. public async Task FromBodyAndRequiredOnValueTypeProperty_EmptyBody_JsonFormatterAddsModelStateError() { // Arrange @@ -485,11 +486,10 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests Assert.Null(entry.Value.AttemptedValue); Assert.Null(entry.Value.RawValue); var error = Assert.Single(entry.Value.Errors); - Assert.Null(error.Exception); - - // Json.NET currently throws an exception starting with "No JSON content found and type 'System.Int32' is - // not nullable." but do not tie test to a particular Json.NET build. - Assert.NotEmpty(error.ErrorMessage); + + // Update me in 3.0 when MvcJsonOptions.AllowInputFormatterExceptionMessages is removed + Assert.IsType(error.Exception); + Assert.Empty(error.ErrorMessage); } private class Person5 @@ -545,7 +545,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests Assert.Empty(modelState); } - [Fact] + [Fact] // This test covers the 2.0 behavior. Error messages from JSON.Net are preserved. public async Task FromBodyWithInvalidPropertyData_JsonFormatterAddsModelError() { // Arrange @@ -586,18 +586,18 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests Assert.Null(state.AttemptedValue); Assert.Null(state.RawValue); var error = Assert.Single(state.Errors); - Assert.Null(error.Exception); - // Json.NET currently throws an Exception with a Message starting with "Could not convert string to - // integer: not a number." but do not tie test to a particular Json.NET build. - Assert.NotEmpty(error.ErrorMessage); + // Update me in 3.0 when MvcJsonOptions.AllowInputFormatterExceptionMessages is removed + Assert.IsType(error.Exception); + Assert.Empty(error.ErrorMessage); } [Theory] [InlineData(false, false)] [InlineData(true, true)] public async Task FromBodyWithEmptyBody_JsonFormatterAddsModelErrorWhenExpected( - bool allowEmptyInputInBodyModelBindingSetting, bool expectedModelStateIsValid) + bool allowEmptyInputInBodyModelBindingSetting, + bool expectedModelStateIsValid) { // Arrange var parameter = new ParameterDescriptor diff --git a/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs index c7ca86a35f..3c73fb507f 100644 --- a/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs @@ -28,11 +28,12 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest // Act var mvcOptions = services.GetRequiredService>().Value; + var jsonOptions = services.GetRequiredService>().Value; // Assert Assert.False(mvcOptions.SuppressBindingUndefinedValueToEnumType); Assert.Equal(InputFormatterExceptionPolicy.AllExceptions, mvcOptions.InputFormatterExceptionPolicy); - Assert.False(mvcOptions.SuppressJsonDeserializationExceptionMessagesInModelState); // This name needs to be inverted in #7157 + Assert.False(jsonOptions.AllowInputFormatterExceptionMessages); } [Fact(Skip = "#7157 - some settings have the wrong values, this test should pass once #7157 is fixed")] @@ -47,11 +48,12 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest // Act var mvcOptions = services.GetRequiredService>().Value; + var jsonOptions = services.GetRequiredService>().Value; // Assert Assert.True(mvcOptions.SuppressBindingUndefinedValueToEnumType); Assert.Equal(InputFormatterExceptionPolicy.MalformedInputExceptions, mvcOptions.InputFormatterExceptionPolicy); - Assert.True(mvcOptions.SuppressJsonDeserializationExceptionMessagesInModelState); // This name needs to be inverted in #7157 + Assert.True(jsonOptions.AllowInputFormatterExceptionMessages); } [Fact(Skip = "#7157 - some settings have the wrong values, this test should pass once #7157 is fixed")] @@ -66,11 +68,12 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest // Act var mvcOptions = services.GetRequiredService>().Value; + var jsonOptions = services.GetRequiredService>().Value; // Assert Assert.True(mvcOptions.SuppressBindingUndefinedValueToEnumType); Assert.Equal(InputFormatterExceptionPolicy.MalformedInputExceptions, mvcOptions.InputFormatterExceptionPolicy); - Assert.True(mvcOptions.SuppressJsonDeserializationExceptionMessagesInModelState); // This name needs to be inverted in #7157 + Assert.True(jsonOptions.AllowInputFormatterExceptionMessages); } // This just does the minimum needed to be able to resolve these options. diff --git a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs index b4fdbfc7b7..5fa8242a70 100644 --- a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs @@ -386,6 +386,13 @@ namespace Microsoft.AspNetCore.Mvc typeof(RazorPagesOptions).Assembly.GetType("Microsoft.AspNetCore.Mvc.RazorPages.RazorPagesOptionsConfigureCompatibilityOptions", throwOnError: true), } }, + { + typeof(IPostConfigureOptions), + new[] + { + typeof(MvcJsonOptions).Assembly.GetType("Microsoft.AspNetCore.Mvc.MvcJsonOptionsConfigureCompatibilityOptions", throwOnError: true), + } + }, { typeof(IActionConstraintProvider), new Type[]