Move option for JSON errors to MvcJsonOptions

This was in the wrong place - JSON formatters have their own options
type already.

Moved the option to MvcJsonOptions and updated the naming + defaults to
reflect our plan.

Also did a bunch of general cleanup on these tests, which were a bit
sloppy.
This commit is contained in:
ASP.NET CI 2018-01-06 15:19:56 -08:00 committed by Ryan Nowak
parent bfb5f23647
commit 2e73bab2a4
15 changed files with 601 additions and 566 deletions

View File

@ -51,7 +51,9 @@ namespace Microsoft.AspNetCore.Mvc
/// <remarks>
/// ASP.NET Core MVC 2.1 introduces compatibility switches for the following:
/// <list type="bullet">
/// <item><description><see cref="MvcOptions.InputFormatterExceptionPolicy"/></description></item>
/// <item><description><see cref="MvcOptions.SuppressBindingUndefinedValueToEnumType"/></description></item>
/// <item><description><c>MvcJsonOptions.AllowInputFormatterExceptionMessages</c></description></item>
/// <item><description><c>RazorPagesOptions.AllowAreas</c></description></item>
/// </list>
/// </remarks>

View File

@ -24,9 +24,11 @@ namespace Microsoft.AspNetCore.Mvc
// See CompatibilitySwitch.cs for guide on how to implement these.
private readonly CompatibilitySwitch<InputFormatterExceptionPolicy> _inputFormatterExceptionPolicy;
private readonly CompatibilitySwitch<bool> _suppressBindingUndefinedValueToEnumType;
private readonly CompatibilitySwitch<bool> _suppressJsonDeserializationExceptionMessagesInModelState;
private readonly ICompatibilitySwitch[] _switches;
/// <summary>
/// Creates a new instance of <see cref="MvcOptions"/>.
/// </summary>
public MvcOptions()
{
CacheProfiles = new Dictionary<string, CacheProfile>(StringComparer.OrdinalIgnoreCase);
@ -43,12 +45,11 @@ namespace Microsoft.AspNetCore.Mvc
_inputFormatterExceptionPolicy = new CompatibilitySwitch<InputFormatterExceptionPolicy>(nameof(InputFormatterExceptionPolicy), InputFormatterExceptionPolicy.AllExceptions);
_suppressBindingUndefinedValueToEnumType = new CompatibilitySwitch<bool>(nameof(SuppressBindingUndefinedValueToEnumType));
_suppressJsonDeserializationExceptionMessagesInModelState = new CompatibilitySwitch<bool>(nameof(SuppressJsonDeserializationExceptionMessagesInModelState));
_switches = new ICompatibilitySwitch[]
{
_inputFormatterExceptionPolicy,
_suppressBindingUndefinedValueToEnumType,
_suppressJsonDeserializationExceptionMessagesInModelState,
};
}
@ -241,20 +242,6 @@ namespace Microsoft.AspNetCore.Mvc
/// </summary>
public bool RequireHttpsPermanent { get; set; }
/// <summary>
/// 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.
/// <see langword="false"/> by default, meaning that clients may receive details about
/// why the JSON they posted is considered invalid.
/// </summary>
public bool SuppressJsonDeserializationExceptionMessagesInModelState
{
get => _suppressJsonDeserializationExceptionMessagesInModelState.Value;
set => _suppressJsonDeserializationExceptionMessagesInModelState.Value = value;
}
IEnumerator<ICompatibilitySwitch> IEnumerable<ICompatibilitySwitch>.GetEnumerator()
{
return ((IEnumerable<ICompatibilitySwitch>)_switches).GetEnumerator();

View File

@ -75,6 +75,8 @@ namespace Microsoft.Extensions.DependencyInjection
{
services.TryAddEnumerable(
ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, MvcJsonMvcOptionsSetup>());
services.TryAddEnumerable(
ServiceDescriptor.Transient<IPostConfigureOptions<MvcJsonOptions>, MvcJsonOptionsConfigureCompatibilityOptions>());
services.TryAddEnumerable(
ServiceDescriptor.Transient<IApiDescriptionProvider, JsonPatchOperationsArrayProvider>());
services.TryAddSingleton<JsonResultExecutor>();

View File

@ -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<MvcOptions>
{
private readonly ILoggerFactory _loggerFactory;
private readonly JsonSerializerSettings _jsonSerializerSettings;
private readonly MvcJsonOptions _jsonOptions;
private readonly ArrayPool<char> _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<JsonPatchInputFormatter>();
options.InputFormatters.Add(new JsonPatchInputFormatter(
jsonInputPatchLogger,
_jsonSerializerSettings,
_jsonOptions.SerializerSettings,
_charPool,
_objectPoolProvider,
options));
options,
_jsonOptions));
var jsonInputLogger = _loggerFactory.CreateLogger<JsonInputFormatter>();
options.InputFormatters.Add(new JsonInputFormatter(
jsonInputLogger,
_jsonSerializerSettings,
_jsonOptions.SerializerSettings,
_charPool,
_objectPoolProvider,
options));
options,
_jsonOptions));
options.FormatterMappings.SetMediaTypeMappingForFormat("json", MediaTypeHeaderValue.Parse("application/json"));

View File

@ -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<JsonSerializer> _jsonSerializerPool;
@ -74,10 +78,10 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
ArrayPool<char> 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
}
/// <summary>
@ -92,7 +96,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
/// <param name="charPool">The <see cref="ArrayPool{Char}"/>.</param>
/// <param name="objectPoolProvider">The <see cref="ObjectPoolProvider"/>.</param>
/// <param name="suppressInputFormatterBuffering">Flag to buffer entire request body before deserializing it.</param>
/// <param name="suppressJsonDeserializationExceptionMessages">If <see langword="true"/>, JSON deserialization exception messages will replaced by a generic message in model state.</param>
/// <param name="allowInputFormatterExceptionMessages">If <see langword="true"/>, JSON deserialization exception messages will replaced by a generic message in model state.</param>
[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<char> charPool,
ObjectPoolProvider objectPoolProvider,
bool suppressInputFormatterBuffering,
bool suppressJsonDeserializationExceptionMessages)
bool allowInputFormatterExceptionMessages)
{
if (logger == null)
{
@ -127,7 +131,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
_charPool = new JsonArrayPool<char>(charPool);
_objectPoolProvider = objectPoolProvider;
_suppressInputFormatterBuffering = suppressInputFormatterBuffering;
_suppressJsonDeserializationExceptionMessages = suppressJsonDeserializationExceptionMessages;
_allowInputFormatterExceptionMessages = allowInputFormatterExceptionMessages;
SupportedEncodings.Add(UTF8EncodingWithoutBOM);
SupportedEncodings.Add(UTF16EncodingLittleEndian);
@ -149,12 +153,14 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
/// <param name="charPool">The <see cref="ArrayPool{Char}"/>.</param>
/// <param name="objectPoolProvider">The <see cref="ObjectPoolProvider"/>.</param>
/// <param name="options">The <see cref="MvcOptions"/>.</param>
/// <param name="jsonOptions">The <see cref="MvcJsonOptions"/>.</param>
public JsonInputFormatter(
ILogger logger,
JsonSerializerSettings serializerSettings,
ArrayPool<char> charPool,
ObjectPoolProvider objectPoolProvider,
MvcOptions options)
MvcOptions options,
MvcJsonOptions jsonOptions)
{
if (logger == null)
{
@ -181,6 +187,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
_charPool = new JsonArrayPool<char>(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;
}
}
}

View File

@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
ArrayPool<char> 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
/// <param name="charPool">The <see cref="ArrayPool{Char}"/>.</param>
/// <param name="objectPoolProvider">The <see cref="ObjectPoolProvider"/>.</param>
/// <param name="suppressInputFormatterBuffering">Flag to buffer entire request body before deserializing it.</param>
/// <param name="suppressJsonDeserializationExceptionMessages">If <see langword="true"/>, JSON deserialization exception messages will replaced by a generic message in model state.</param>
/// <param name="allowInputFormatterExceptionMessages">
/// If <see langword="false"/>, JSON deserialization exception messages will replaced by a generic message in model state.
/// </param>
[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<char> 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
/// <param name="charPool">The <see cref="ArrayPool{Char}"/>.</param>
/// <param name="objectPoolProvider">The <see cref="ObjectPoolProvider"/>.</param>
/// <param name="options">The <see cref="MvcOptions"/>.</param>
/// <param name="jsonOptions">The <see cref="MvcJsonOptions"/>.</param>
public JsonPatchInputFormatter(
ILogger logger,
JsonSerializerSettings serializerSettings,
ArrayPool<char> 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();

View File

@ -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
/// <summary>
/// Provides programmatic configuration for JSON in the MVC framework.
/// </summary>
public class MvcJsonOptions
public class MvcJsonOptions : IEnumerable<ICompatibilitySwitch>
{
private readonly CompatibilitySwitch<bool> _allowInputFormatterExceptionMessages;
private readonly ICompatibilitySwitch[] _switches;
/// <summary>
/// Creates a new instance of <see cref="MvcJsonOptions"/>.
/// </summary>
public MvcJsonOptions()
{
_allowInputFormatterExceptionMessages = new CompatibilitySwitch<bool>(nameof(AllowInputFormatterExceptionMessages));
_switches = new ICompatibilitySwitch[]
{
_allowInputFormatterExceptionMessages,
};
}
/// <summary>
/// Gets or sets a flag to determine whether error messsages from JSON deserialization by the
/// <see cref="JsonInputFormatter"/> will be added to the <see cref="ModelStateDictionary"/>. The default
/// value is <c>false</c>, meaning that a generic error message will be used instead.
/// </summary>
/// <remarks>
/// <para>
/// Error messages in the <see cref="ModelStateDictionary"/> are often communicated to clients, either in HTML
/// or using <see cref="BadRequestObjectResult"/>. In effect, this setting controls whether clients can recieve
/// detailed error messages about submitted JSON data.
/// </para>
/// <para>
/// This property is associated with a compatibility switch and can provide a different behavior depending on
/// the configured compatibility version for the application. See <see cref="CompatibilityVersion"/> for
/// guidance and examples of setting the application's compatibility version.
/// </para>
/// <para>
/// 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 <see cref="CompatibilityVersion"/>.
/// </para>
/// <para>
/// If the application's compatibility version is set to <see cref="CompatibilityVersion.Version_2_0"/> then
/// this setting will have value <c>false</c> if not explicitly configured.
/// </para>
/// <para>
/// If the application's compatibility version is set to <see cref="CompatibilityVersion.Version_2_1"/> or
/// higher then this setting will have value <c>true</c> if not explicitly configured.
/// </para>
/// </remarks>
public bool AllowInputFormatterExceptionMessages
{
get => _allowInputFormatterExceptionMessages.Value;
set => _allowInputFormatterExceptionMessages.Value = value;
}
/// <summary>
/// Gets the <see cref="JsonSerializerSettings"/> that are used by this application.
/// </summary>
public JsonSerializerSettings SerializerSettings { get; } =
JsonSerializerSettingsProvider.CreateSerializerSettings();
public JsonSerializerSettings SerializerSettings { get; } = JsonSerializerSettingsProvider.CreateSerializerSettings();
IEnumerator<ICompatibilitySwitch> IEnumerable<ICompatibilitySwitch>.GetEnumerator()
{
return ((IEnumerable<ICompatibilitySwitch>)_switches).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() => _switches.GetEnumerator();
}
}

View File

@ -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<MvcJsonOptions>
{
public MvcJsonOptionsConfigureCompatibilityOptions(
ILoggerFactory loggerFactory,
IOptions<MvcCompatibilityOptions> compatibilityOptions)
: base(loggerFactory, compatibilityOptions)
{
}
protected override IReadOnlyDictionary<string, object> DefaultValues
{
get
{
var values = new Dictionary<string, object>();
if (Version >= CompatibilityVersion.Version_2_1)
{
values[nameof(MvcJsonOptions.AllowInputFormatterExceptionMessages)] = true;
}
return values;
}
}
}
}

View File

@ -799,7 +799,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
private readonly bool _throwNonInputFormatterException;
public TestableJsonInputFormatter(bool throwNonInputFormatterException)
: base(GetLogger(), new JsonSerializerSettings(), ArrayPool<char>.Shared, new DefaultObjectPoolProvider(), new MvcOptions())
: base(GetLogger(), new JsonSerializerSettings(), ArrayPool<char>.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<char>.Shared, new DefaultObjectPoolProvider(), new MvcOptions())
: base(GetLogger(), new JsonSerializerSettings(), ArrayPool<char>.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;
}

View File

@ -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<char>.Shared, _objectPoolProvider);
var formatter = new JsonInputFormatter(
GetLogger(),
_serializerSettings,
ArrayPool<char>.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<IHttpResponseFeature>(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<User>(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<User>(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<char>.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<IHttpResponseFeature>(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<User>(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<User>(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<char>.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<IHttpResponseFeature>(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<User>(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<char>.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<IHttpResponseFeature>(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<User>(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<char>.Shared, _objectPoolProvider, mvcOptions);
var contentBytes = Encoding.UTF8.GetBytes(content);
var mvcOptions = new MvcOptions()
{
SuppressInputFormatterBuffering = true,
};
var formatter = new JsonInputFormatter(
GetLogger(),
_serializerSettings,
ArrayPool<char>.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<IHttpResponseFeature>(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<User>(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<char>.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<IHttpResponseFeature>(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<User>(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<char>.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<char>.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<char>.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<char>.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<char>.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<char>.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<char>.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<char>.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<char>.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<char>.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<TooManyModelErrorsException>(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<char>.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<char>.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<char>.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<char>.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<InputFormatterException>(modelError.Exception);
Assert.Empty(modelError.ErrorMessage);
}
[Fact]
public async Task ReadAsync_AllowInputFormatterExceptionMessages_DoesNotWrapJsonInputExceptions()
{
// Arrange
var formatter = new JsonInputFormatter(
GetLogger(),
_serializerSettings,
ArrayPool<char>.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<char>.Shared, _objectPoolProvider, new MvcOptions())
: base(GetLogger(), settings, ArrayPool<char>.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<char>.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<string> GetModelStateErrorMessages(ModelStateDictionary modelStateDictionary)
{
var allErrorMessages = new List<string>();

View File

@ -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<char>.Shared, _objectPoolProvider);
var formatter = new JsonPatchInputFormatter(
GetLogger(),
_serializerSettings,
ArrayPool<char>.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<IHttpResponseFeature>(new TestResponseFeature());
httpContext.Request.Body = new NonSeekableReadStream(contentBytes);
httpContext.Request.ContentType = "application/json";
var provider = new EmptyModelMetadataProvider();
var metadata = provider.GetMetadataForType(typeof(JsonPatchDocument<Customer>));
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: modelState,
metadata: metadata,
readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader);
var formatterContext = CreateInputFormatterContext(typeof(JsonPatchDocument<Customer>), httpContext);
// Act
var result = await formatter.ReadAsync(context);
var result = await formatter.ReadAsync(formatterContext);
// Assert
Assert.False(result.HasError);
var patchDoc = Assert.IsType<JsonPatchDocument<Customer>>(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<JsonPatchDocument<Customer>>(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<JsonPatchDocument<Customer>>(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<JsonPatchDocument<Customer>>(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<char>.Shared, _objectPoolProvider, new MvcOptions());
var formatter = new JsonPatchInputFormatter(
GetLogger(),
_serializerSettings,
ArrayPool<char>.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<IHttpResponseFeature>(new TestResponseFeature());
httpContext.Request.Body = new NonSeekableReadStream(contentBytes);
httpContext.Request.ContentType = "application/json";
var provider = new EmptyModelMetadataProvider();
var metadata = provider.GetMetadataForType(typeof(JsonPatchDocument<Customer>));
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: modelState,
metadata: metadata,
readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader);
var formatterContext = CreateInputFormatterContext(typeof(JsonPatchDocument<Customer>), httpContext);
// Act
var result = await formatter.ReadAsync(context);
var result = await formatter.ReadAsync(formatterContext);
// Assert
Assert.False(result.HasError);
var patchDoc = Assert.IsType<JsonPatchDocument<Customer>>(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<JsonPatchDocument<Customer>>(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<JsonPatchDocument<Customer>>(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<JsonPatchDocument<Customer>>(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<char>.Shared, _objectPoolProvider, suppressInputFormatterBuffering: true);
var formatter = new JsonPatchInputFormatter(
GetLogger(),
_serializerSettings,
ArrayPool<char>.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<IHttpResponseFeature>(new TestResponseFeature());
httpContext.Request.Body = new NonSeekableReadStream(contentBytes);
httpContext.Request.ContentType = "application/json";
var provider = new EmptyModelMetadataProvider();
var metadata = provider.GetMetadataForType(typeof(JsonPatchDocument<Customer>));
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: modelState,
metadata: metadata,
readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader);
var context = CreateInputFormatterContext(typeof(JsonPatchDocument<Customer>), httpContext);
// Act
var result = await formatter.ReadAsync(context);
// Assert
Assert.False(result.HasError);
var patchDoc = Assert.IsType<JsonPatchDocument<Customer>>(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<JsonPatchDocument<Customer>>(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<char>.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<IHttpResponseFeature>(new TestResponseFeature());
httpContext.Request.Body = new NonSeekableReadStream(contentBytes);
httpContext.Request.ContentType = "application/json";
var provider = new EmptyModelMetadataProvider();
var metadata = provider.GetMetadataForType(typeof(JsonPatchDocument<Customer>));
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: modelState,
metadata: metadata,
readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader);
var formatterContext = CreateInputFormatterContext(typeof(JsonPatchDocument<Customer>), 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<JsonPatchDocument<Customer>>(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<JsonPatchDocument<Customer>>(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<char>.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<Customer>));
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: modelState,
metadata: metadata,
readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader);
var formatterContext = CreateInputFormatterContext(typeof(JsonPatchDocument<Customer>), httpContext);
// Act
var result = await formatter.ReadAsync(context);
var result = await formatter.ReadAsync(formatterContext);
// Assert
Assert.False(result.HasError);
var patchDoc = Assert.IsType<JsonPatchDocument<Customer>>(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<JsonPatchDocument<Customer>>(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<char>.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<Customer>));
var context = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: modelState,
metadata: metadata,
readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader);
var formatterContext = CreateInputFormatterContext(typeof(JsonPatchDocument<Customer>), httpContext);
// Act
var result = await formatter.ReadAsync(context);
var result = await formatter.ReadAsync(formatterContext);
// Assert
Assert.False(result.HasError);
var patchDoc = Assert.IsType<JsonPatchDocument<Customer>>(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<JsonPatchDocument<Customer>>(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<char>.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<Customer>));
var formatterContext = new InputFormatterContext(
httpContext,
modelName: string.Empty,
modelState: modelState,
metadata: metadata,
readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader);
var formatterContext = CreateInputFormatterContext(typeof(JsonPatchDocument<Customer>), 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<char>.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<char>.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<char>.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")
{

View File

@ -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]

View File

@ -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<JsonSerializationException>(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<JsonReaderException>(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

View File

@ -28,11 +28,12 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
// Act
var mvcOptions = services.GetRequiredService<IOptions<MvcOptions>>().Value;
var jsonOptions = services.GetRequiredService<IOptions<MvcJsonOptions>>().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<IOptions<MvcOptions>>().Value;
var jsonOptions = services.GetRequiredService<IOptions<MvcJsonOptions>>().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<IOptions<MvcOptions>>().Value;
var jsonOptions = services.GetRequiredService<IOptions<MvcJsonOptions>>().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.

View File

@ -386,6 +386,13 @@ namespace Microsoft.AspNetCore.Mvc
typeof(RazorPagesOptions).Assembly.GetType("Microsoft.AspNetCore.Mvc.RazorPages.RazorPagesOptionsConfigureCompatibilityOptions", throwOnError: true),
}
},
{
typeof(IPostConfigureOptions<MvcJsonOptions>),
new[]
{
typeof(MvcJsonOptions).Assembly.GetType("Microsoft.AspNetCore.Mvc.MvcJsonOptionsConfigureCompatibilityOptions", throwOnError: true),
}
},
{
typeof(IActionConstraintProvider),
new Type[]