diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs index e487da16af..88aa088830 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs @@ -46,7 +46,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal options.ModelBinderProviders.Add(new BodyModelBinderProvider(options.InputFormatters, _readerFactory, _loggerFactory, options)); options.ModelBinderProviders.Add(new HeaderModelBinderProvider()); options.ModelBinderProviders.Add(new FloatingPointTypeModelBinderProvider()); - options.ModelBinderProviders.Add(new EnumTypeModelBinderProvider(options.AllowBindingUndefinedValueToEnumType)); + options.ModelBinderProviders.Add(new EnumTypeModelBinderProvider(options)); options.ModelBinderProviders.Add(new SimpleTypeModelBinderProvider()); options.ModelBinderProviders.Add(new CancellationTokenModelBinderProvider()); options.ModelBinderProviders.Add(new ByteArrayModelBinderProvider()); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/EnumTypeModelBinderProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/EnumTypeModelBinderProvider.cs index c7ef8be646..5bf700e536 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/EnumTypeModelBinderProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/EnumTypeModelBinderProvider.cs @@ -10,11 +10,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders /// public class EnumTypeModelBinderProvider : IModelBinderProvider { - private readonly bool _allowBindingUndefinedValueToEnumType; + private readonly MvcOptions _options; - public EnumTypeModelBinderProvider(bool allowBindingUndefinedValueToEnumType) + public EnumTypeModelBinderProvider(MvcOptions options) { - _allowBindingUndefinedValueToEnumType = allowBindingUndefinedValueToEnumType; + _options = options; } /// @@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders if (context.Metadata.IsEnum) { return new EnumTypeModelBinder( - _allowBindingUndefinedValueToEnumType, + _options.AllowBindingUndefinedValueToEnumType, context.Metadata.UnderlyingOrModelType); } diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/MvcJsonMvcOptionsSetup.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/MvcJsonMvcOptionsSetup.cs index ad32ab6505..ad2af947ed 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/MvcJsonMvcOptionsSetup.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/MvcJsonMvcOptionsSetup.cs @@ -69,8 +69,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json.Internal _jsonSerializerSettings, _charPool, _objectPoolProvider, - options.SuppressInputFormatterBuffering, - options.SuppressJsonDeserializationExceptionMessagesInModelState)); + options)); var jsonInputLogger = _loggerFactory.CreateLogger(); options.InputFormatters.Add(new JsonInputFormatter( @@ -78,8 +77,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json.Internal _jsonSerializerSettings, _charPool, _objectPoolProvider, - options.SuppressInputFormatterBuffering, - options.SuppressJsonDeserializationExceptionMessagesInModelState)); + options)); 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 e0c5ed650a..2500296a6a 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonInputFormatter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonInputFormatter.cs @@ -27,6 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters private readonly IArrayPool _charPool; private readonly ILogger _logger; private readonly ObjectPoolProvider _objectPoolProvider; + private readonly MvcOptions _options; private readonly bool _suppressInputFormatterBuffering; private readonly bool _suppressJsonDeserializationExceptionMessages; @@ -43,6 +44,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// /// The . /// The . + [Obsolete("This constructor is obsolete and will be removed in a future version.")] public JsonInputFormatter( ILogger logger, JsonSerializerSettings serializerSettings, @@ -65,6 +67,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// The . /// The . /// Flag to buffer entire request body before deserializing it. + [Obsolete("This constructor is obsolete and will be removed in a future version.")] public JsonInputFormatter( ILogger logger, JsonSerializerSettings serializerSettings, @@ -90,6 +93,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// The . /// Flag to buffer entire request body before deserializing it. /// 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, JsonSerializerSettings serializerSettings, @@ -133,6 +137,59 @@ namespace Microsoft.AspNetCore.Mvc.Formatters SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyJsonSyntax); } + /// + /// Initializes a new instance of . + /// + /// The . + /// + /// The . Should be either the application-wide settings + /// () or an instance + /// initially returned. + /// + /// The . + /// The . + /// The . + public JsonInputFormatter( + ILogger logger, + JsonSerializerSettings serializerSettings, + ArrayPool charPool, + ObjectPoolProvider objectPoolProvider, + MvcOptions options) + { + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + if (serializerSettings == null) + { + throw new ArgumentNullException(nameof(serializerSettings)); + } + + if (charPool == null) + { + throw new ArgumentNullException(nameof(charPool)); + } + + if (objectPoolProvider == null) + { + throw new ArgumentNullException(nameof(objectPoolProvider)); + } + + _logger = logger; + SerializerSettings = serializerSettings; + _charPool = new JsonArrayPool(charPool); + _objectPoolProvider = objectPoolProvider; + _options = options; + + SupportedEncodings.Add(UTF8EncodingWithoutBOM); + SupportedEncodings.Add(UTF16EncodingLittleEndian); + + SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationJson); + SupportedMediaTypes.Add(MediaTypeHeaderValues.TextJson); + SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyJsonSyntax); + } + /// public virtual InputFormatterExceptionModelStatePolicy ExceptionPolicy { @@ -172,7 +229,9 @@ namespace Microsoft.AspNetCore.Mvc.Formatters var request = context.HttpContext.Request; - if (!request.Body.CanSeek && !_suppressInputFormatterBuffering) + var suppressInputFormatterBuffering = _options?.SuppressInputFormatterBuffering ?? _suppressInputFormatterBuffering; + + if (!request.Body.CanSeek && !suppressInputFormatterBuffering) { // JSON.Net does synchronous reads. In order to avoid blocking on the stream, we asynchronously // read everything into a buffer, and then seek back to the beginning. @@ -352,8 +411,9 @@ namespace Microsoft.AspNetCore.Mvc.Formatters // 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; + suppressJsonDeserializationExceptionMessages || !isJsonExceptionType; return suppressOriginalMessage ? exception : new InputFormatterException(exception.Message, exception); diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonPatchInputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonPatchInputFormatter.cs index 9f1c94945b..6e704e8348 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonPatchInputFormatter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonPatchInputFormatter.cs @@ -30,6 +30,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// /// The . /// The . + [Obsolete("This constructor is obsolete and will be removed in a future version.")] public JsonPatchInputFormatter( ILogger logger, JsonSerializerSettings serializerSettings, @@ -51,6 +52,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// The . /// The . /// Flag to buffer entire request body before deserializing it. + [Obsolete("This constructor is obsolete and will be removed in a future version.")] public JsonPatchInputFormatter( ILogger logger, JsonSerializerSettings serializerSettings, @@ -74,6 +76,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// The . /// Flag to buffer entire request body before deserializing it. /// 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, JsonSerializerSettings serializerSettings, @@ -89,6 +92,32 @@ namespace Microsoft.AspNetCore.Mvc.Formatters SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationJsonPatch); } + /// + /// Initializes a new instance. + /// + /// The . + /// + /// The . Should be either the application-wide settings + /// () or an instance + /// initially returned. + /// + /// The . + /// The . + /// The . + public JsonPatchInputFormatter( + ILogger logger, + JsonSerializerSettings serializerSettings, + ArrayPool charPool, + ObjectPoolProvider objectPoolProvider, + MvcOptions options) + : base(logger, serializerSettings, charPool, objectPoolProvider, options) + { + // Clear all values and only include json-patch+json value. + SupportedMediaTypes.Clear(); + + SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationJsonPatch); + } + /// public override InputFormatterExceptionModelStatePolicy ExceptionPolicy { 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 84e0c5cb69..b50afa25cf 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,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders private readonly bool _throwNonInputFormatterException; public TestableJsonInputFormatter(bool throwNonInputFormatterException) - : base(GetLogger(), new JsonSerializerSettings(), ArrayPool.Shared, new DefaultObjectPoolProvider()) + : base(GetLogger(), new JsonSerializerSettings(), ArrayPool.Shared, new DefaultObjectPoolProvider(), new MvcOptions()) { _throwNonInputFormatterException = throwNonInputFormatterException; } @@ -863,7 +863,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders private readonly bool _throwNonInputFormatterException; public DerivedJsonInputFormatter(bool throwNonInputFormatterException) - : base(GetLogger(), new JsonSerializerSettings(), ArrayPool.Shared, new DefaultObjectPoolProvider()) + : base(GetLogger(), new JsonSerializerSettings(), ArrayPool.Shared, new DefaultObjectPoolProvider(), new MvcOptions()) { _throwNonInputFormatterException = throwNonInputFormatterException; } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/EnumTypeModelBinderProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/EnumTypeModelBinderProviderTest.cs index 6773822709..70444d731c 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/EnumTypeModelBinderProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/EnumTypeModelBinderProviderTest.cs @@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public void ReturnsBinder_ForEnumType(Type modelType) { // Arrange - var provider = new EnumTypeModelBinderProvider(allowBindingUndefinedValueToEnumType: true); + var provider = new EnumTypeModelBinderProvider(new MvcOptions { AllowBindingUndefinedValueToEnumType = true }); var context = new TestModelBinderProviderContext(modelType); // Act @@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public void ReturnsBinder_ForFlagsEnumType(Type modelType) { // Arrange - var provider = new EnumTypeModelBinderProvider(allowBindingUndefinedValueToEnumType: true); + var provider = new EnumTypeModelBinderProvider(new MvcOptions { AllowBindingUndefinedValueToEnumType = true }); var context = new TestModelBinderProviderContext(modelType); // Act @@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public void DoesNotReturnBinder_ForNonEnumTypes(Type modelType) { // Arrange - var provider = new EnumTypeModelBinderProvider(allowBindingUndefinedValueToEnumType: false); + var provider = new EnumTypeModelBinderProvider(new MvcOptions { AllowBindingUndefinedValueToEnumType = false }); var context = new TestModelBinderProviderContext(modelType); // Act diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/EnumTypeModelBinderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/EnumTypeModelBinderTest.cs index 88d461d78b..2379c0c933 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/EnumTypeModelBinderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/EnumTypeModelBinderTest.cs @@ -5,6 +5,7 @@ using System; using System.ComponentModel; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; +using Microsoft.Extensions.Options; using Xunit; namespace Microsoft.AspNetCore.Mvc.ModelBinding @@ -281,7 +282,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { modelName, valueProviderValue } } }; - var binderProvider = new EnumTypeModelBinderProvider(allowBindingUndefinedValueToEnumType); + var binderProvider = new EnumTypeModelBinderProvider(new MvcOptions + { + AllowBindingUndefinedValueToEnumType = allowBindingUndefinedValueToEnumType + }); var binder = binderProvider.GetBinder(binderProviderContext); return (bindingContext, binder); } diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs index e879f4f424..88e712ae90 100644 --- a/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs @@ -33,8 +33,59 @@ namespace Microsoft.AspNetCore.Mvc.Formatters // Arrange var content = "{name: 'Person Name', Age: '30'}"; var logger = GetLogger(); +#pragma warning disable CS0618 var formatter = new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider); +#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.True(httpContext.Request.Body.CanSeek); + httpContext.Request.Body.Seek(0L, SeekOrigin.Begin); + + result = await formatter.ReadAsync(context); + + // 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() + { + // 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 modelState = new ModelStateDictionary(); @@ -78,8 +129,10 @@ namespace Microsoft.AspNetCore.Mvc.Formatters // 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(); @@ -105,7 +158,99 @@ namespace Microsoft.AspNetCore.Mvc.Formatters Assert.Equal("Person Name", userModel.Name); Assert.Equal(30, userModel.Age); - // Reading again should not fail as the request body should have been buffered by the formatter + 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, + _serializerSettings, + ArrayPool.Shared, + _objectPoolProvider, + mvcOptions); + 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_UsingMutatedOptions() + { + // 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 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 + // 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); + + // 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 @@ -135,7 +280,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters var loggerMock = GetLogger(); var formatter = - new JsonInputFormatter(loggerMock, _serializerSettings, ArrayPool.Shared, _objectPoolProvider); + new JsonInputFormatter(loggerMock, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); var contentBytes = Encoding.UTF8.GetBytes("content"); var httpContext = GetHttpContext(contentBytes, contentType: requestContentType); @@ -161,7 +306,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters // Arrange var loggerMock = GetLogger(); var formatter = - new JsonInputFormatter(loggerMock, _serializerSettings, ArrayPool.Shared, _objectPoolProvider); + new JsonInputFormatter(loggerMock, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); // Act var mediaType = formatter.SupportedMediaTypes[0]; @@ -188,7 +333,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters // Arrange var logger = GetLogger(); var formatter = - new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider); + new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); var contentBytes = Encoding.UTF8.GetBytes(content); var httpContext = GetHttpContext(contentBytes); @@ -216,7 +361,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters var content = "{name: 'Person Name', Age: '30'}"; var logger = GetLogger(); var formatter = - new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider); + new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); var contentBytes = Encoding.UTF8.GetBytes(content); var httpContext = GetHttpContext(contentBytes); @@ -246,7 +391,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters var content = "[0, 23, 300]"; var logger = GetLogger(); var formatter = - new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider); + new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); var contentBytes = Encoding.UTF8.GetBytes(content); var modelState = new ModelStateDictionary(); @@ -280,7 +425,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters var content = "[0, 23, 300]"; var logger = GetLogger(); var formatter = - new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider); + new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); var contentBytes = Encoding.UTF8.GetBytes(content); var modelState = new ModelStateDictionary(); @@ -310,7 +455,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters var content = "{name: 'Person Name', Age: 'not-an-age'}"; var logger = GetLogger(); var formatter = - new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider); + new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); var contentBytes = Encoding.UTF8.GetBytes(content); var modelState = new ModelStateDictionary(); @@ -341,7 +486,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters var content = "[0, 23, 300]"; var logger = GetLogger(); var formatter = - new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider); + new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); var contentBytes = Encoding.UTF8.GetBytes(content); var modelState = new ModelStateDictionary(); @@ -371,7 +516,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters 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 JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); var contentBytes = Encoding.UTF8.GetBytes(content); var modelState = new ModelStateDictionary(); @@ -402,7 +547,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters var content = "{name: 'Person Name', Age: 'not-an-age'}"; var logger = GetLogger(); var formatter = - new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider); + new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); var contentBytes = Encoding.UTF8.GetBytes(content); var modelState = new ModelStateDictionary(); @@ -440,7 +585,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters // Arrange var logger = GetLogger(); var formatter = - new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider); + new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); var contentBytes = Encoding.UTF8.GetBytes(content); var modelState = new ModelStateDictionary(); @@ -488,7 +633,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters // 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 JsonInputFormatter(logger, serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); var modelState = new ModelStateDictionary(); var httpContext = GetHttpContext(contentBytes, "application/json;charset=utf-8"); @@ -547,7 +692,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters // Arrange var logger = GetLogger(); var formatter = - new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider); + new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); var contentBytes = Encoding.UTF8.GetBytes(content); var modelState = new ModelStateDictionary(); @@ -579,8 +724,15 @@ namespace Microsoft.AspNetCore.Mvc.Formatters // Arrange var logger = GetLogger(); var formatter = new JsonInputFormatter( - logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, - suppressInputFormatterBuffering: false, suppressJsonDeserializationExceptionMessages: true); + logger, + _serializerSettings, + ArrayPool.Shared, + _objectPoolProvider, + new MvcOptions() + { + SuppressInputFormatterBuffering = false, + SuppressJsonDeserializationExceptionMessagesInModelState = true + }); var contentBytes = Encoding.UTF8.GetBytes("{"); var modelStateKey = string.Empty; @@ -611,7 +763,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters private class TestableJsonInputFormatter : JsonInputFormatter { public TestableJsonInputFormatter(JsonSerializerSettings settings) - : base(GetLogger(), settings, ArrayPool.Shared, _objectPoolProvider) + : base(GetLogger(), settings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()) { } diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonPatchInputFormatterTest.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonPatchInputFormatterTest.cs index 202d008019..a998486402 100644 --- a/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonPatchInputFormatterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonPatchInputFormatterTest.cs @@ -30,8 +30,57 @@ namespace Microsoft.AspNetCore.Mvc.Formatters { // Arrange var logger = GetLogger(); +#pragma warning disable CS0618 var formatter = new JsonPatchInputFormatter(logger, _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); + + // 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); + + Assert.True(httpContext.Request.Body.CanSeek); + httpContext.Request.Body.Seek(0L, SeekOrigin.Begin); + + result = await formatter.ReadAsync(context); + + // 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); + } + + [Fact] + public async Task BuffersRequestBody_UsingDefaultOptions() + { + // Arrange + var logger = GetLogger(); + var formatter = + new JsonPatchInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); var content = "[{\"op\":\"add\",\"path\":\"Customer/Name\",\"value\":\"John\"}]"; var contentBytes = Encoding.UTF8.GetBytes(content); @@ -77,8 +126,10 @@ namespace Microsoft.AspNetCore.Mvc.Formatters { // Arrange var logger = GetLogger(); +#pragma warning disable CS0618 var formatter = new JsonPatchInputFormatter(logger, _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); @@ -106,6 +157,57 @@ namespace Microsoft.AspNetCore.Mvc.Formatters Assert.Equal("Customer/Name", patchDoc.Operations[0].path); Assert.Equal("John", patchDoc.Operations[0].value); + 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_UsingMutatedOptions_DoesNotBufferRequestBody() + { + // Arrange + var logger = GetLogger(); + var mvcOptions = new MvcOptions(); + mvcOptions.SuppressInputFormatterBuffering = false; + var formatter = new JsonPatchInputFormatter( + logger, + _serializerSettings, + ArrayPool.Shared, + _objectPoolProvider, + mvcOptions); + 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); + + // 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); + + // 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.False(httpContext.Request.Body.CanSeek); result = await formatter.ReadAsync(context); // Assert @@ -119,7 +221,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters // Arrange var logger = GetLogger(); var formatter = - new JsonPatchInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider); + new JsonPatchInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); var content = "[{\"op\":\"add\",\"path\":\"Customer/Name\",\"value\":\"John\"}]"; var contentBytes = Encoding.UTF8.GetBytes(content); @@ -151,7 +253,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters // Arrange var logger = GetLogger(); var formatter = - new JsonPatchInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider); + new JsonPatchInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); var content = "[{\"op\": \"add\", \"path\" : \"Customer/Name\", \"value\":\"John\"}," + "{\"op\": \"remove\", \"path\" : \"Customer/Name\"}]"; var contentBytes = Encoding.UTF8.GetBytes(content); @@ -190,7 +292,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters // Arrange var logger = GetLogger(); var formatter = - new JsonPatchInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider); + new JsonPatchInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); var content = "[{\"op\": \"add\", \"path\" : \"Customer/Name\", \"value\":\"John\"}]"; var contentBytes = Encoding.UTF8.GetBytes(content); @@ -220,7 +322,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters // Arrange var logger = GetLogger(); var formatter = - new JsonPatchInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider); + new JsonPatchInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); var content = "[{\"op\": \"add\", \"path\" : \"Customer/Name\", \"value\":\"John\"}]"; var contentBytes = Encoding.UTF8.GetBytes(content); @@ -251,7 +353,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters var logger = GetLogger(); var formatter = - new JsonPatchInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider); + new JsonPatchInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider, new MvcOptions()); var content = "[{\"op\": \"add\", \"path\" : \"Customer/Name\", \"value\":\"John\"}]"; var contentBytes = Encoding.UTF8.GetBytes(content);