JSON error handling (#11190)
* Exception handling with SystemTextJsonInputFormatter * Additional tests * Update ref package * PR feedback * Test fixes and feedback * Update refs * Restructure tests * Cleanup
This commit is contained in:
commit
151ae52661
|
|
@ -1877,7 +1877,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
}
|
}
|
||||||
public partial class SystemTextJsonInputFormatter : Microsoft.AspNetCore.Mvc.Formatters.TextInputFormatter, Microsoft.AspNetCore.Mvc.Formatters.IInputFormatterExceptionPolicy
|
public partial class SystemTextJsonInputFormatter : Microsoft.AspNetCore.Mvc.Formatters.TextInputFormatter, Microsoft.AspNetCore.Mvc.Formatters.IInputFormatterExceptionPolicy
|
||||||
{
|
{
|
||||||
public SystemTextJsonInputFormatter(Microsoft.AspNetCore.Mvc.JsonOptions options) { }
|
public SystemTextJsonInputFormatter(Microsoft.AspNetCore.Mvc.JsonOptions options, Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonInputFormatter> logger) { }
|
||||||
Microsoft.AspNetCore.Mvc.Formatters.InputFormatterExceptionPolicy Microsoft.AspNetCore.Mvc.Formatters.IInputFormatterExceptionPolicy.ExceptionPolicy { get { throw null; } }
|
Microsoft.AspNetCore.Mvc.Formatters.InputFormatterExceptionPolicy Microsoft.AspNetCore.Mvc.Formatters.IInputFormatterExceptionPolicy.ExceptionPolicy { get { throw null; } }
|
||||||
public System.Text.Json.JsonSerializerOptions SerializerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
public System.Text.Json.JsonSerializerOptions SerializerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||||
[System.Diagnostics.DebuggerStepThroughAttribute]
|
[System.Diagnostics.DebuggerStepThroughAttribute]
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters.Json;
|
using Microsoft.AspNetCore.Mvc.Formatters.Json;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
{
|
{
|
||||||
|
|
@ -16,13 +18,19 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SystemTextJsonInputFormatter : TextInputFormatter, IInputFormatterExceptionPolicy
|
public class SystemTextJsonInputFormatter : TextInputFormatter, IInputFormatterExceptionPolicy
|
||||||
{
|
{
|
||||||
|
private readonly ILogger<SystemTextJsonInputFormatter> _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of <see cref="SystemTextJsonInputFormatter"/>.
|
/// Initializes a new instance of <see cref="SystemTextJsonInputFormatter"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="options">The <see cref="JsonOptions"/>.</param>
|
/// <param name="options">The <see cref="JsonOptions"/>.</param>
|
||||||
public SystemTextJsonInputFormatter(JsonOptions options)
|
/// <param name="logger">The <see cref="ILogger"/>.</param>
|
||||||
|
public SystemTextJsonInputFormatter(
|
||||||
|
JsonOptions options,
|
||||||
|
ILogger<SystemTextJsonInputFormatter> logger)
|
||||||
{
|
{
|
||||||
SerializerOptions = options.JsonSerializerOptions;
|
SerializerOptions = options.JsonSerializerOptions;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
SupportedEncodings.Add(UTF8EncodingWithoutBOM);
|
SupportedEncodings.Add(UTF8EncodingWithoutBOM);
|
||||||
SupportedEncodings.Add(UTF16EncodingLittleEndian);
|
SupportedEncodings.Add(UTF16EncodingLittleEndian);
|
||||||
|
|
@ -67,6 +75,18 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
{
|
{
|
||||||
model = await JsonSerializer.ReadAsync(inputStream, context.ModelType, SerializerOptions);
|
model = await JsonSerializer.ReadAsync(inputStream, context.ModelType, SerializerOptions);
|
||||||
}
|
}
|
||||||
|
catch (JsonException jsonException)
|
||||||
|
{
|
||||||
|
var path = jsonException.Path;
|
||||||
|
|
||||||
|
var formatterException = new InputFormatterException(jsonException.Message, jsonException);
|
||||||
|
|
||||||
|
context.ModelState.TryAddModelError(path, formatterException, context.Metadata);
|
||||||
|
|
||||||
|
Log.JsonInputException(_logger, jsonException);
|
||||||
|
|
||||||
|
return InputFormatterResult.Failure();
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
if (inputStream is TranscodingReadStream transcoding)
|
if (inputStream is TranscodingReadStream transcoding)
|
||||||
|
|
@ -85,6 +105,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
Log.JsonInputSuccess(_logger, context.ModelType);
|
||||||
return InputFormatterResult.Success(model);
|
return InputFormatterResult.Success(model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -98,5 +119,29 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
|
|
||||||
return new TranscodingReadStream(httpContext.Request.Body, encoding);
|
return new TranscodingReadStream(httpContext.Request.Body, encoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class Log
|
||||||
|
{
|
||||||
|
private static readonly Action<ILogger, string, Exception> _jsonInputFormatterException;
|
||||||
|
private static readonly Action<ILogger, string, Exception> _jsonInputSuccess;
|
||||||
|
|
||||||
|
static Log()
|
||||||
|
{
|
||||||
|
_jsonInputFormatterException = LoggerMessage.Define<string>(
|
||||||
|
LogLevel.Debug,
|
||||||
|
new EventId(1, "SystemTextJsonInputException"),
|
||||||
|
"JSON input formatter threw an exception: {Message}");
|
||||||
|
_jsonInputSuccess = LoggerMessage.Define<string>(
|
||||||
|
LogLevel.Debug,
|
||||||
|
new EventId(2, "SystemTextJsonInputSuccess"),
|
||||||
|
"JSON input formatter succeeded, deserializing to type '{TypeName}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void JsonInputException(ILogger logger, Exception exception)
|
||||||
|
=> _jsonInputFormatterException(logger, exception.Message, exception);
|
||||||
|
|
||||||
|
public static void JsonInputSuccess(ILogger logger, Type modelType)
|
||||||
|
=> _jsonInputSuccess(logger, modelType.FullName, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
options.Filters.Add(new UnsupportedContentTypeFilter());
|
options.Filters.Add(new UnsupportedContentTypeFilter());
|
||||||
|
|
||||||
// Set up default input formatters.
|
// Set up default input formatters.
|
||||||
options.InputFormatters.Add(new SystemTextJsonInputFormatter(_jsonOptions.Value));
|
options.InputFormatters.Add(new SystemTextJsonInputFormatter(_jsonOptions.Value, _loggerFactory.CreateLogger<SystemTextJsonInputFormatter>()));
|
||||||
|
|
||||||
// Set up default output formatters.
|
// Set up default output formatters.
|
||||||
options.OutputFormatters.Add(new HttpNoContentOutputFormatter());
|
options.OutputFormatters.Add(new HttpNoContentOutputFormatter());
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,21 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
using Microsoft.Extensions.Logging.Testing;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
{
|
{
|
||||||
public abstract class JsonInputFormatterTestBase
|
public abstract class JsonInputFormatterTestBase : LoggedTest
|
||||||
{
|
{
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("application/json", true)]
|
[InlineData("application/json", true)]
|
||||||
|
|
@ -102,6 +107,61 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
Assert.Equal("abcd", stringValue);
|
Assert.Equal("abcd", stringValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public virtual async Task JsonFormatter_EscapedKeys_Bracket()
|
||||||
|
{
|
||||||
|
var expectedKey = JsonFormatter_EscapedKeys_Bracket_Expected;
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
var content = "[{\"It[s a key\":1234556}]";
|
||||||
|
var formatter = GetInputFormatter();
|
||||||
|
|
||||||
|
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||||
|
var httpContext = GetHttpContext(contentBytes);
|
||||||
|
|
||||||
|
var formatterContext = CreateInputFormatterContext(typeof(IEnumerable<IDictionary<string, short>>), httpContext);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await formatter.ReadAsync(formatterContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.HasError);
|
||||||
|
Assert.Collection(
|
||||||
|
formatterContext.ModelState.OrderBy(k => k.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal(expectedKey, kvp.Key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public virtual async Task JsonFormatter_EscapedKeys()
|
||||||
|
{
|
||||||
|
var expectedKey = JsonFormatter_EscapedKeys_Expected;
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
var content = "[{\"It\\\"s a key\": 1234556}]";
|
||||||
|
var formatter = GetInputFormatter();
|
||||||
|
|
||||||
|
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||||
|
var httpContext = GetHttpContext(contentBytes);
|
||||||
|
|
||||||
|
var formatterContext = CreateInputFormatterContext(
|
||||||
|
typeof(IEnumerable<IDictionary<string, short>>), httpContext);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await formatter.ReadAsync(formatterContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.HasError);
|
||||||
|
Assert.Collection(
|
||||||
|
formatterContext.ModelState.OrderBy(k => k.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal(expectedKey, kvp.Key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public virtual async Task JsonFormatterReadsDateTimeValue()
|
public virtual async Task JsonFormatterReadsDateTimeValue()
|
||||||
{
|
{
|
||||||
|
|
@ -199,6 +259,33 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
Assert.Equal(new int[] { 0, 23, 300 }, (IEnumerable<int>)result.Model);
|
Assert.Equal(new int[] { 0, 23, 300 }, (IEnumerable<int>)result.Model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public virtual async Task ReadAsync_ArrayOfObjects_HasCorrectKey()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var formatter = GetInputFormatter();
|
||||||
|
|
||||||
|
var content = "[{\"Age\": 5}, {\"Age\": 3}, {\"Age\": \"Cheese\"} ]";
|
||||||
|
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||||
|
var httpContext = GetHttpContext(contentBytes);
|
||||||
|
|
||||||
|
var formatterContext = CreateInputFormatterContext(typeof(List<ComplexModel>), httpContext);
|
||||||
|
|
||||||
|
var expectedKey = ReadAsync_ArrayOfObjects_HasCorrectKey_Expected;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await formatter.ReadAsync(formatterContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.HasError, "Model should have had an error!");
|
||||||
|
Assert.Collection(formatterContext.ModelState.OrderBy(k => k.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal(expectedKey, kvp.Key);
|
||||||
|
Assert.Single(kvp.Value.Errors);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public virtual async Task ReadAsync_AddsModelValidationErrorsToModelState()
|
public virtual async Task ReadAsync_AddsModelValidationErrorsToModelState()
|
||||||
{
|
{
|
||||||
|
|
@ -210,15 +297,19 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
var httpContext = GetHttpContext(contentBytes);
|
var httpContext = GetHttpContext(contentBytes);
|
||||||
|
|
||||||
var formatterContext = CreateInputFormatterContext(typeof(ComplexModel), httpContext);
|
var formatterContext = CreateInputFormatterContext(typeof(ComplexModel), httpContext);
|
||||||
|
var expectedKey = ReadAsync_AddsModelValidationErrorsToModelState_Expected;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await formatter.ReadAsync(formatterContext);
|
var result = await formatter.ReadAsync(formatterContext);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(result.HasError);
|
Assert.True(result.HasError, "Model should have had an error!");
|
||||||
Assert.Equal(
|
Assert.Collection(formatterContext.ModelState.OrderBy(k => k.Key),
|
||||||
"Could not convert string to decimal: not-an-age. Path 'Age', line 1, position 44.",
|
kvp =>
|
||||||
formatterContext.ModelState["Age"].Errors[0].ErrorMessage);
|
{
|
||||||
|
Assert.Equal(expectedKey, kvp.Key);
|
||||||
|
Assert.Single(kvp.Value.Errors);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -227,19 +318,23 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
// Arrange
|
// Arrange
|
||||||
var formatter = GetInputFormatter();
|
var formatter = GetInputFormatter();
|
||||||
|
|
||||||
var content = "[0, 23, 300]";
|
var content = "[0, 23, 33767]";
|
||||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||||
var httpContext = GetHttpContext(contentBytes);
|
var httpContext = GetHttpContext(contentBytes);
|
||||||
|
|
||||||
var formatterContext = CreateInputFormatterContext(typeof(byte[]), httpContext);
|
var formatterContext = CreateInputFormatterContext(typeof(short[]), httpContext);
|
||||||
|
|
||||||
|
var expectedValue = ReadAsync_InvalidArray_AddsOverflowErrorsToModelState_Expected;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await formatter.ReadAsync(formatterContext);
|
var result = await formatter.ReadAsync(formatterContext);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(result.HasError);
|
Assert.True(result.HasError, "Model should have produced an error!");
|
||||||
Assert.Equal("The supplied value is invalid.", formatterContext.ModelState["[2]"].Errors[0].ErrorMessage);
|
Assert.Collection(formatterContext.ModelState.OrderBy(k => k.Key),
|
||||||
Assert.Null(formatterContext.ModelState["[2]"].Errors[0].Exception);
|
kvp => {
|
||||||
|
Assert.Equal(expectedValue, kvp.Key);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -253,15 +348,19 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
var httpContext = GetHttpContext(contentBytes);
|
var httpContext = GetHttpContext(contentBytes);
|
||||||
|
|
||||||
var formatterContext = CreateInputFormatterContext(typeof(ComplexModel[]), httpContext, modelName: "names");
|
var formatterContext = CreateInputFormatterContext(typeof(ComplexModel[]), httpContext, modelName: "names");
|
||||||
|
var expectedKey = ReadAsync_InvalidComplexArray_AddsOverflowErrorsToModelState_Expected;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await formatter.ReadAsync(formatterContext);
|
var result = await formatter.ReadAsync(formatterContext);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(result.HasError);
|
Assert.True(result.HasError);
|
||||||
Assert.Equal(
|
Assert.Collection(
|
||||||
"Error converting value 300 to type 'System.Byte'. Path '[1].Small', line 1, position 69.",
|
formatterContext.ModelState.OrderBy(k => k.Key),
|
||||||
formatterContext.ModelState["names[1].Small"].Errors[0].ErrorMessage);
|
kvp => {
|
||||||
|
Assert.Equal(expectedKey, kvp.Key);
|
||||||
|
Assert.Single(kvp.Value.Errors);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -318,6 +417,65 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
Assert.Null(result.Model);
|
Assert.Null(result.Model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ReadAsync_ComplexPoco()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var formatter = GetInputFormatter();
|
||||||
|
|
||||||
|
var content = "{ \"Id\": 5, \"Person\": { \"Name\": \"name\", \"Numbers\": [3, 2, \"Hamburger\"]} }";
|
||||||
|
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||||
|
var httpContext = GetHttpContext(contentBytes);
|
||||||
|
|
||||||
|
var formatterContext = CreateInputFormatterContext(typeof(ComplexPoco), httpContext);
|
||||||
|
|
||||||
|
var expectedKey = ReadAsync_ComplexPoco_Expected;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await formatter.ReadAsync(formatterContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.HasError, "Model should have had an error!");
|
||||||
|
Assert.Collection(formatterContext.ModelState.OrderBy(k => k.Key),
|
||||||
|
kvp => {
|
||||||
|
Assert.Equal(expectedKey, kvp.Key);
|
||||||
|
Assert.Single(kvp.Value.Errors);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public virtual async Task ReadAsync_RequiredAttribute()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var formatter = GetInputFormatter();
|
||||||
|
var content = "{ \"Id\": 5, \"Person\": {\"Numbers\": [3]} }";
|
||||||
|
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||||
|
var httpContext = GetHttpContext(contentBytes);
|
||||||
|
|
||||||
|
var formatterContext = CreateInputFormatterContext(typeof(ComplexPoco), httpContext);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await formatter.ReadAsync(formatterContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.HasError, "Model should have had an error!");
|
||||||
|
Assert.Single(formatterContext.ModelState["Person.Name"].Errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal abstract string JsonFormatter_EscapedKeys_Bracket_Expected { get; }
|
||||||
|
|
||||||
|
internal abstract string JsonFormatter_EscapedKeys_Expected { get; }
|
||||||
|
|
||||||
|
internal abstract string ReadAsync_ArrayOfObjects_HasCorrectKey_Expected { get; }
|
||||||
|
|
||||||
|
internal abstract string ReadAsync_AddsModelValidationErrorsToModelState_Expected { get; }
|
||||||
|
|
||||||
|
internal abstract string ReadAsync_InvalidArray_AddsOverflowErrorsToModelState_Expected { get; }
|
||||||
|
|
||||||
|
internal abstract string ReadAsync_InvalidComplexArray_AddsOverflowErrorsToModelState_Expected { get; }
|
||||||
|
|
||||||
|
internal abstract string ReadAsync_ComplexPoco_Expected { get; }
|
||||||
|
|
||||||
protected abstract TextInputFormatter GetInputFormatter();
|
protected abstract TextInputFormatter GetInputFormatter();
|
||||||
|
|
||||||
protected static HttpContext GetHttpContext(
|
protected static HttpContext GetHttpContext(
|
||||||
|
|
@ -356,6 +514,20 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
treatEmptyInputAsDefaultValue: treatEmptyInputAsDefaultValue);
|
treatEmptyInputAsDefaultValue: treatEmptyInputAsDefaultValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected sealed class ComplexPoco
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public Person Person{ get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
protected sealed class Person
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[JsonProperty(Required = Required.Always)]
|
||||||
|
public string Name { get; set; }
|
||||||
|
public IEnumerable<int> Numbers { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
protected sealed class ComplexModel
|
protected sealed class ComplexModel
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,103 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
{
|
{
|
||||||
public class SystemTextJsonInputFormatterTest : JsonInputFormatterTestBase
|
public class SystemTextJsonInputFormatterTest : JsonInputFormatterTestBase
|
||||||
{
|
{
|
||||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/8474")]
|
[Fact]
|
||||||
public override Task ReadAsync_AddsModelValidationErrorsToModelState()
|
public override Task ReadAsync_AddsModelValidationErrorsToModelState()
|
||||||
{
|
{
|
||||||
return base.ReadAsync_AddsModelValidationErrorsToModelState();
|
return base.ReadAsync_AddsModelValidationErrorsToModelState();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/8474")]
|
[Fact]
|
||||||
public override Task ReadAsync_InvalidArray_AddsOverflowErrorsToModelState()
|
public override Task ReadAsync_InvalidArray_AddsOverflowErrorsToModelState()
|
||||||
{
|
{
|
||||||
return base.ReadAsync_InvalidArray_AddsOverflowErrorsToModelState();
|
return base.ReadAsync_InvalidArray_AddsOverflowErrorsToModelState();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/8474")]
|
[Fact]
|
||||||
public override Task ReadAsync_InvalidComplexArray_AddsOverflowErrorsToModelState()
|
public override Task ReadAsync_InvalidComplexArray_AddsOverflowErrorsToModelState()
|
||||||
{
|
{
|
||||||
return base.ReadAsync_InvalidComplexArray_AddsOverflowErrorsToModelState();
|
return base.ReadAsync_InvalidComplexArray_AddsOverflowErrorsToModelState();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/8474")]
|
[Fact]
|
||||||
public override Task ReadAsync_UsesTryAddModelValidationErrorsToModelState()
|
public override Task ReadAsync_UsesTryAddModelValidationErrorsToModelState()
|
||||||
{
|
{
|
||||||
return base.ReadAsync_UsesTryAddModelValidationErrorsToModelState();
|
return base.ReadAsync_UsesTryAddModelValidationErrorsToModelState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact(Skip = "https://github.com/dotnet/corefx/issues/38492")]
|
||||||
|
public override Task ReadAsync_RequiredAttribute()
|
||||||
|
{
|
||||||
|
// System.Text.Json does not yet support an equivalent of Required.
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public override Task JsonFormatter_EscapedKeys()
|
||||||
|
{
|
||||||
|
return base.JsonFormatter_EscapedKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public override Task JsonFormatter_EscapedKeys_Bracket()
|
||||||
|
{
|
||||||
|
return base.JsonFormatter_EscapedKeys_Bracket();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ReadAsync_SingleError()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var formatter = GetInputFormatter();
|
||||||
|
|
||||||
|
var content = "[5, 'seven', 3, notnum ]";
|
||||||
|
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||||
|
var httpContext = GetHttpContext(contentBytes);
|
||||||
|
|
||||||
|
var formatterContext = CreateInputFormatterContext(typeof(List<int>), httpContext);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await formatter.ReadAsync(formatterContext);
|
||||||
|
|
||||||
|
Assert.Collection(
|
||||||
|
formatterContext.ModelState.OrderBy(k => k),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("$[1]", kvp.Key);
|
||||||
|
var error = Assert.Single(kvp.Value.Errors);
|
||||||
|
Assert.StartsWith("''' is an invalid start of a value", error.ErrorMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected override TextInputFormatter GetInputFormatter()
|
protected override TextInputFormatter GetInputFormatter()
|
||||||
{
|
{
|
||||||
return new SystemTextJsonInputFormatter(new JsonOptions());
|
return new SystemTextJsonInputFormatter(new JsonOptions(), LoggerFactory.CreateLogger<SystemTextJsonInputFormatter>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal override string ReadAsync_AddsModelValidationErrorsToModelState_Expected => "$.Age";
|
||||||
|
|
||||||
|
internal override string JsonFormatter_EscapedKeys_Expected => "$[0]['It\\u0022s a key']";
|
||||||
|
|
||||||
|
internal override string JsonFormatter_EscapedKeys_Bracket_Expected => "$[0]['It[s a key']";
|
||||||
|
|
||||||
|
internal override string ReadAsync_ArrayOfObjects_HasCorrectKey_Expected => "$[2].Age";
|
||||||
|
|
||||||
|
internal override string ReadAsync_InvalidArray_AddsOverflowErrorsToModelState_Expected => "$[2]";
|
||||||
|
|
||||||
|
internal override string ReadAsync_InvalidComplexArray_AddsOverflowErrorsToModelState_Expected => "$[1].Small";
|
||||||
|
|
||||||
|
internal override string ReadAsync_ComplexPoco_Expected => "$.Person.Numbers[2]";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -226,7 +226,9 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
addMember = !path.EndsWith("." + member, StringComparison.Ordinal);
|
addMember = !path.EndsWith("." + member, StringComparison.Ordinal)
|
||||||
|
&& !path.EndsWith("['" + member + "']", StringComparison.Ordinal)
|
||||||
|
&& !path.EndsWith("[" + member + "]", StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,13 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
||||||
{
|
{
|
||||||
internal static class NewtonsoftJsonLoggerExtensions
|
internal static class NewtonsoftJsonLoggerExtensions
|
||||||
{
|
{
|
||||||
private static readonly Action<ILogger, Exception> _jsonInputFormatterCrashed;
|
private static readonly Action<ILogger, Exception> _jsonInputFormatterException;
|
||||||
|
|
||||||
private static readonly Action<ILogger, string, Exception> _jsonResultExecuting;
|
private static readonly Action<ILogger, string, Exception> _jsonResultExecuting;
|
||||||
|
|
||||||
static NewtonsoftJsonLoggerExtensions()
|
static NewtonsoftJsonLoggerExtensions()
|
||||||
{
|
{
|
||||||
_jsonInputFormatterCrashed = LoggerMessage.Define(
|
_jsonInputFormatterException = LoggerMessage.Define(
|
||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
new EventId(1, "JsonInputException"),
|
new EventId(1, "JsonInputException"),
|
||||||
"JSON input formatter threw an exception.");
|
"JSON input formatter threw an exception.");
|
||||||
|
|
@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
||||||
|
|
||||||
public static void JsonInputException(this ILogger logger, Exception exception)
|
public static void JsonInputException(this ILogger logger, Exception exception)
|
||||||
{
|
{
|
||||||
_jsonInputFormatterCrashed(logger, exception);
|
_jsonInputFormatterException(logger, exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void JsonResultExecuting(this ILogger logger, object value)
|
public static void JsonResultExecuting(this ILogger logger, object value)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.IO;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -196,6 +196,18 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
Assert.Equal(settings.DateTimeZoneHandling, actual.DateTimeZoneHandling);
|
Assert.Equal(settings.DateTimeZoneHandling, actual.DateTimeZoneHandling);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public override Task JsonFormatter_EscapedKeys()
|
||||||
|
{
|
||||||
|
return base.JsonFormatter_EscapedKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public override Task JsonFormatter_EscapedKeys_Bracket()
|
||||||
|
{
|
||||||
|
return base.JsonFormatter_EscapedKeys_Bracket();
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(" ", true, true)]
|
[InlineData(" ", true, true)]
|
||||||
[InlineData(" ", false, false)]
|
[InlineData(" ", false, false)]
|
||||||
|
|
@ -235,6 +247,39 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
Assert.Equal(expectedMessage, modelError.ErrorMessage);
|
Assert.Equal(expectedMessage, modelError.ErrorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ReadAsync_AllowMultipleErrors()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var content = "[5, 'seven', 3, 'notnum']";
|
||||||
|
|
||||||
|
var formatter = CreateFormatter(allowInputFormatterExceptionMessages: true);
|
||||||
|
|
||||||
|
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||||
|
var httpContext = GetHttpContext(contentBytes);
|
||||||
|
|
||||||
|
var formatterContext = CreateInputFormatterContext(typeof(List<int>), httpContext);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await formatter.ReadAsync(formatterContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
formatterContext.ModelState.OrderBy(k => k.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("[1]", kvp.Key);
|
||||||
|
var error = Assert.Single(kvp.Value.Errors);
|
||||||
|
Assert.StartsWith("Could not convert string to integer:", error.ErrorMessage);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("[3]", kvp.Key);
|
||||||
|
var error = Assert.Single(kvp.Value.Errors);
|
||||||
|
Assert.StartsWith("Could not convert string to integer:", error.ErrorMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ReadAsync_DoNotAllowInputFormatterExceptionMessages_DoesNotWrapJsonInputExceptions()
|
public async Task ReadAsync_DoNotAllowInputFormatterExceptionMessages_DoesNotWrapJsonInputExceptions()
|
||||||
{
|
{
|
||||||
|
|
@ -325,6 +370,20 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal override string JsonFormatter_EscapedKeys_Expected => "[0]['It\"s a key']";
|
||||||
|
|
||||||
|
internal override string JsonFormatter_EscapedKeys_Bracket_Expected => "[0][\'It[s a key\']";
|
||||||
|
|
||||||
|
internal override string ReadAsync_AddsModelValidationErrorsToModelState_Expected => "Age";
|
||||||
|
|
||||||
|
internal override string ReadAsync_ArrayOfObjects_HasCorrectKey_Expected => "[2].Age";
|
||||||
|
|
||||||
|
internal override string ReadAsync_ComplexPoco_Expected => "Person.Numbers[2]";
|
||||||
|
|
||||||
|
internal override string ReadAsync_InvalidComplexArray_AddsOverflowErrorsToModelState_Expected => "names[1].Small";
|
||||||
|
|
||||||
|
internal override string ReadAsync_InvalidArray_AddsOverflowErrorsToModelState_Expected => "[2]";
|
||||||
|
|
||||||
private class Location
|
private class Location
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -325,6 +325,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions.ApiDescription.S
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.ApiDescription.Server", "Extensions.ApiDescription.Server\src\Microsoft.Extensions.ApiDescription.Server.csproj", "{D7CF2A1E-A29E-45AB-9C2A-CD6C3BAE54F2}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.ApiDescription.Server", "Extensions.ApiDescription.Server\src\Microsoft.Extensions.ApiDescription.Server.csproj", "{D7CF2A1E-A29E-45AB-9C2A-CD6C3BAE54F2}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Metadata", "..\Http\Metadata\src\Microsoft.AspNetCore.Metadata.csproj", "{464195B3-022A-4D19-9104-8C66CC882D67}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
|
@ -1841,6 +1843,18 @@ Global
|
||||||
{D7CF2A1E-A29E-45AB-9C2A-CD6C3BAE54F2}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
{D7CF2A1E-A29E-45AB-9C2A-CD6C3BAE54F2}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||||
{D7CF2A1E-A29E-45AB-9C2A-CD6C3BAE54F2}.Release|x86.ActiveCfg = Release|Any CPU
|
{D7CF2A1E-A29E-45AB-9C2A-CD6C3BAE54F2}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{D7CF2A1E-A29E-45AB-9C2A-CD6C3BAE54F2}.Release|x86.Build.0 = Release|Any CPU
|
{D7CF2A1E-A29E-45AB-9C2A-CD6C3BAE54F2}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{464195B3-022A-4D19-9104-8C66CC882D67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{464195B3-022A-4D19-9104-8C66CC882D67}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{464195B3-022A-4D19-9104-8C66CC882D67}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||||
|
{464195B3-022A-4D19-9104-8C66CC882D67}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||||
|
{464195B3-022A-4D19-9104-8C66CC882D67}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{464195B3-022A-4D19-9104-8C66CC882D67}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{464195B3-022A-4D19-9104-8C66CC882D67}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{464195B3-022A-4D19-9104-8C66CC882D67}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{464195B3-022A-4D19-9104-8C66CC882D67}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||||
|
{464195B3-022A-4D19-9104-8C66CC882D67}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||||
|
{464195B3-022A-4D19-9104-8C66CC882D67}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{464195B3-022A-4D19-9104-8C66CC882D67}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
@ -1972,6 +1986,7 @@ Global
|
||||||
{C6F3BCE6-1EFD-4360-932B-B98573E78926} = {45CE788D-4B69-4F83-981C-F43D8F15B0F1}
|
{C6F3BCE6-1EFD-4360-932B-B98573E78926} = {45CE788D-4B69-4F83-981C-F43D8F15B0F1}
|
||||||
{637119E8-5BBB-4FC7-A372-DAF38FF5EBD9} = {5FE3048A-E96B-44F8-A7C4-FC590D7E04B4}
|
{637119E8-5BBB-4FC7-A372-DAF38FF5EBD9} = {5FE3048A-E96B-44F8-A7C4-FC590D7E04B4}
|
||||||
{D7CF2A1E-A29E-45AB-9C2A-CD6C3BAE54F2} = {C15AA245-9E54-4FD6-90FF-B46F47779C46}
|
{D7CF2A1E-A29E-45AB-9C2A-CD6C3BAE54F2} = {C15AA245-9E54-4FD6-90FF-B46F47779C46}
|
||||||
|
{464195B3-022A-4D19-9104-8C66CC882D67} = {5FE3048A-E96B-44F8-A7C4-FC590D7E04B4}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {63D344F6-F86D-40E6-85B9-0AABBE338C4A}
|
SolutionGuid = {63D344F6-F86D-40E6-85B9-0AABBE338C4A}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue