diff --git a/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonInputFormatter.cs b/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonInputFormatter.cs index 4b13a1b709..028c24894f 100644 --- a/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonInputFormatter.cs +++ b/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonInputFormatter.cs @@ -8,6 +8,8 @@ using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Formatters.Json; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Mvc.Formatters { @@ -16,13 +18,19 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// public class SystemTextJsonInputFormatter : TextInputFormatter, IInputFormatterExceptionPolicy { + private readonly ILogger _logger; + /// /// Initializes a new instance of . /// /// The . - public SystemTextJsonInputFormatter(JsonOptions options) + /// The . + public SystemTextJsonInputFormatter( + JsonOptions options, + ILogger logger) { SerializerOptions = options.JsonSerializerOptions; + _logger = logger; SupportedEncodings.Add(UTF8EncodingWithoutBOM); SupportedEncodings.Add(UTF16EncodingLittleEndian); @@ -67,6 +75,26 @@ namespace Microsoft.AspNetCore.Mvc.Formatters { model = await JsonSerializer.ReadAsync(inputStream, context.ModelType, SerializerOptions); } + catch (JsonException jsonException) + { + var path = jsonException.Path; + if (path.StartsWith("$.", StringComparison.Ordinal)) + { + path = path.Substring(2); + } + + // Handle path combinations such as ""+"Property", "Parent"+"Property", or "Parent"+"[12]". + var key = ModelNames.CreatePropertyModelName(context.ModelName, path); + + var formatterException = new InputFormatterException(jsonException.Message, jsonException); + + var metadata = GetPathMetadata(context.Metadata, path); + context.ModelState.TryAddModelError(key, formatterException, metadata); + + Log.JsonInputException(_logger, jsonException); + + return InputFormatterResult.Failure(); + } finally { if (inputStream is TranscodingReadStream transcoding) @@ -98,5 +126,68 @@ namespace Microsoft.AspNetCore.Mvc.Formatters return new TranscodingReadStream(httpContext.Request.Body, encoding); } + + // Keep in sync with NewtonsoftJsonInputFormatter.GetPatMetadata + private ModelMetadata GetPathMetadata(ModelMetadata metadata, string path) + { + var index = 0; + while (index >= 0 && index < path.Length) + { + if (path[index] == '[') + { + // At start of "[0]". + if (metadata.ElementMetadata == null) + { + // Odd case but don't throw just because ErrorContext had an odd-looking path. + break; + } + + metadata = metadata.ElementMetadata; + index = path.IndexOf(']', index); + } + else if (path[index] == '.' || path[index] == ']') + { + // Skip '.' in "prefix.property" or "[0].property" or ']' in "[0]". + index++; + } + else + { + // At start of "property", "property." or "property[0]". + var endIndex = path.IndexOfAny(new[] { '.', '[' }, index); + if (endIndex == -1) + { + endIndex = path.Length; + } + + var propertyName = path.Substring(index, endIndex - index); + if (metadata.Properties[propertyName] == null) + { + // Odd case but don't throw just because ErrorContext had an odd-looking path. + break; + } + + metadata = metadata.Properties[propertyName]; + index = endIndex; + } + } + + return metadata; + } + + private static class Log + { + private static readonly Action _jsonInputFormatterException; + + static Log() + { + _jsonInputFormatterException = LoggerMessage.Define( + LogLevel.Debug, + new EventId(1, "SystemTextJsonInputException"), + "JSON input formatter threw an exception."); + } + + public static void JsonInputException(ILogger logger, Exception exception) + => _jsonInputFormatterException(logger, exception); + } } } diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs b/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs index b94ff39557..79005a8389 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs @@ -78,7 +78,7 @@ namespace Microsoft.AspNetCore.Mvc options.Filters.Add(new UnsupportedContentTypeFilter()); // Set up default input formatters. - options.InputFormatters.Add(new SystemTextJsonInputFormatter(_jsonOptions.Value)); + options.InputFormatters.Add(new SystemTextJsonInputFormatter(_jsonOptions.Value, _loggerFactory.CreateLogger())); // Set up default output formatters. options.OutputFormatters.Add(new HttpNoContentOutputFormatter()); diff --git a/src/Mvc/Mvc.NewtonsoftJson/src/NewtonsoftJsonLoggerExtensions.cs b/src/Mvc/Mvc.NewtonsoftJson/src/NewtonsoftJsonLoggerExtensions.cs index 14dcd92174..dfad01025e 100644 --- a/src/Mvc/Mvc.NewtonsoftJson/src/NewtonsoftJsonLoggerExtensions.cs +++ b/src/Mvc/Mvc.NewtonsoftJson/src/NewtonsoftJsonLoggerExtensions.cs @@ -8,13 +8,13 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson { internal static class NewtonsoftJsonLoggerExtensions { - private static readonly Action _jsonInputFormatterCrashed; + private static readonly Action _jsonInputFormatterException; private static readonly Action _jsonResultExecuting; static NewtonsoftJsonLoggerExtensions() { - _jsonInputFormatterCrashed = LoggerMessage.Define( + _jsonInputFormatterException = LoggerMessage.Define( LogLevel.Debug, new EventId(1, "JsonInputException"), "JSON input formatter threw an exception."); @@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson public static void JsonInputException(this ILogger logger, Exception exception) { - _jsonInputFormatterCrashed(logger, exception); + _jsonInputFormatterException(logger, exception); } public static void JsonResultExecuting(this ILogger logger, object value) diff --git a/src/Mvc/samples/MvcSandbox/Controllers/HomeController.cs b/src/Mvc/samples/MvcSandbox/Controllers/HomeController.cs index 2aa4ff6829..751004d291 100644 --- a/src/Mvc/samples/MvcSandbox/Controllers/HomeController.cs +++ b/src/Mvc/samples/MvcSandbox/Controllers/HomeController.cs @@ -5,14 +5,19 @@ using Microsoft.AspNetCore.Mvc; namespace MvcSandbox.Controllers { + [ApiController] public class HomeController : Controller { - [ModelBinder] - public string Id { get; set; } - - public IActionResult Index() + [HttpPost("/")] + public IActionResult Index(Person person) { - return View(); + return Ok(person); } } + + public class Person + { + public int Id { get; set; } + } + }