diff --git a/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonInputFormatter.cs b/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonInputFormatter.cs index bb21bc64df..d93260032f 100644 --- a/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonInputFormatter.cs +++ b/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonInputFormatter.cs @@ -118,6 +118,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters } else { + Log.JsonInputSuccess(_logger, context.ModelType); return InputFormatterResult.Success(model); } } @@ -134,18 +135,26 @@ namespace Microsoft.AspNetCore.Mvc.Formatters private static class Log { - private static readonly Action _jsonInputFormatterException; + private static readonly Action _jsonInputFormatterException; + private static readonly Action _jsonInputSuccess; static Log() { - _jsonInputFormatterException = LoggerMessage.Define( - LogLevel.Debug, - new EventId(1, "SystemTextJsonInputException"), - "JSON input formatter threw an exception."); + _jsonInputFormatterException = LoggerMessage.Define( + LogLevel.Debug, + new EventId(1, "SystemTextJsonInputException"), + "JSON input formatter threw an exception: {Message}"); + _jsonInputSuccess = LoggerMessage.Define( + 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); + => _jsonInputFormatterException(logger, exception.Message, exception); + + public static void JsonInputSuccess(ILogger logger, Type modelType) + => _jsonInputSuccess(logger, modelType.FullName, null); } } } diff --git a/src/Mvc/Mvc.Core/test/Formatters/JsonInputFormatterTestBase.cs b/src/Mvc/Mvc.Core/test/Formatters/JsonInputFormatterTestBase.cs index fe94632904..af680361ea 100644 --- a/src/Mvc/Mvc.Core/test/Formatters/JsonInputFormatterTestBase.cs +++ b/src/Mvc/Mvc.Core/test/Formatters/JsonInputFormatterTestBase.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; +using System.Linq; using System.Text; using System.Text.Json; using System.Threading.Tasks; @@ -18,6 +19,14 @@ namespace Microsoft.AspNetCore.Mvc.Formatters { public abstract class JsonInputFormatterTestBase : LoggedTest { + internal enum Formatter + { + Newtonsoft, + SystemText + } + + internal abstract Formatter CurrentFormatter { get; } + [Theory] [InlineData("application/json", true)] [InlineData("application/*", false)] @@ -106,6 +115,67 @@ namespace Microsoft.AspNetCore.Mvc.Formatters Assert.Equal("abcd", stringValue); } + [Fact] + public async Task JsonFormatter_EscapedKeys_Bracket() + { + // 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>), httpContext); + + // Act + var result = await formatter.ReadAsync(formatterContext); + + // Assert + Assert.True(result.HasError); + Assert.Collection( + formatterContext.ModelState.OrderBy(k => k.Key), + kvp => + { + Assert.Equal("[0][\'It[s a key\']", kvp.Key); + }); + } + + [Fact] + public async Task JsonFormatter_EscapedKeys() + { + // 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>), httpContext); + + // Act + var result = await formatter.ReadAsync(formatterContext); + + // Assert + Assert.True(result.HasError); + Assert.Collection( + formatterContext.ModelState.OrderBy(k => k.Key), + kvp => + { + switch(CurrentFormatter) + { + case Formatter.Newtonsoft: + Assert.Equal("[0]['It\"s a key']", kvp.Key); + break; + case Formatter.SystemText: + Assert.Equal("[0][\'It\\u0022s a key\']", kvp.Key); + break; + default: + throw new NotImplementedException(); + } + }); + } + [Fact] public virtual async Task JsonFormatterReadsDateTimeValue() { @@ -280,7 +350,12 @@ namespace Microsoft.AspNetCore.Mvc.Formatters // Assert Assert.True(result.HasError); - Assert.Single(formatterContext.ModelState["names[1].Small"].Errors); + Assert.Collection( + formatterContext.ModelState.OrderBy(k => k.Key), + kvp => { + Assert.Equal("names[1].Small", kvp.Key); + Assert.Single(kvp.Value.Errors); + }); } [Fact] diff --git a/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonInputFormatterTest.cs b/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonInputFormatterTest.cs index aa43db0221..9ef9630a45 100644 --- a/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonInputFormatterTest.cs +++ b/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonInputFormatterTest.cs @@ -13,6 +13,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters { public class SystemTextJsonInputFormatterTest : JsonInputFormatterTestBase { + internal override Formatter CurrentFormatter => Formatter.SystemText; + [Fact] public override Task ReadAsync_AddsModelValidationErrorsToModelState() { diff --git a/src/Mvc/Mvc.NewtonsoftJson/src/NewtonsoftJsonInputFormatter.cs b/src/Mvc/Mvc.NewtonsoftJson/src/NewtonsoftJsonInputFormatter.cs index 438f996032..49a5581b87 100644 --- a/src/Mvc/Mvc.NewtonsoftJson/src/NewtonsoftJsonInputFormatter.cs +++ b/src/Mvc/Mvc.NewtonsoftJson/src/NewtonsoftJsonInputFormatter.cs @@ -227,6 +227,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters else { addMember = !path.EndsWith("." + member, StringComparison.Ordinal) + && !path.EndsWith("['" + member + "']", StringComparison.Ordinal) && !path.EndsWith("[" + member + "]", StringComparison.Ordinal); } } diff --git a/src/Mvc/Mvc.NewtonsoftJson/test/NewtonsoftJsonInputFormatterTest.cs b/src/Mvc/Mvc.NewtonsoftJson/test/NewtonsoftJsonInputFormatterTest.cs index b1a2234a5d..fa29d0289f 100644 --- a/src/Mvc/Mvc.NewtonsoftJson/test/NewtonsoftJsonInputFormatterTest.cs +++ b/src/Mvc/Mvc.NewtonsoftJson/test/NewtonsoftJsonInputFormatterTest.cs @@ -24,6 +24,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters private static readonly ObjectPoolProvider _objectPoolProvider = new DefaultObjectPoolProvider(); private static readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings(); + internal override Formatter CurrentFormatter => Formatter.Newtonsoft; + [Fact] public async Task Constructor_BuffersRequestBody_UsingDefaultOptions() { @@ -253,7 +255,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters // Assert Assert.Collection( - formatterContext.ModelState.OrderBy(k => k), + formatterContext.ModelState.OrderBy(k => k.Key), kvp => { Assert.Equal("[1]", kvp.Key);