diff --git a/src/Mvc/Mvc.Core/src/MvcOptions.cs b/src/Mvc/Mvc.Core/src/MvcOptions.cs index 52da78e8e2..3fa005f96c 100644 --- a/src/Mvc/Mvc.Core/src/MvcOptions.cs +++ b/src/Mvc/Mvc.Core/src/MvcOptions.cs @@ -329,7 +329,16 @@ namespace Microsoft.AspNetCore.Mvc /// Gets the used by and /// . /// - public JsonSerializerOptions SerializerOptions { get; } = new JsonSerializerOptions(); + public JsonSerializerOptions SerializerOptions { get; } = new JsonSerializerOptions + { + // Limit the object graph we'll consume to a fixed depth. This prevents stackoverflow exceptions + // from deserialization errors that might occur from deeply nested objects. + // This value is the same for model binding and Json.Net's serialization. + MaxDepth = DefaultMaxModelBindingRecursionDepth, + + // Use camel casing for properties + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; IEnumerator IEnumerable.GetEnumerator() => _switches.GetEnumerator(); diff --git a/src/Mvc/Mvc.Core/test/Formatters/JsonInputFormatterTestBase.cs b/src/Mvc/Mvc.Core/test/Formatters/JsonInputFormatterTestBase.cs index fe7024b089..e7190037e9 100644 --- a/src/Mvc/Mvc.Core/test/Formatters/JsonInputFormatterTestBase.cs +++ b/src/Mvc/Mvc.Core/test/Formatters/JsonInputFormatterTestBase.cs @@ -106,7 +106,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters public virtual async Task JsonFormatterReadsDateTimeValue() { // Arrange - var content = "\"2012-02-01 12:45 AM\""; + var expected = new DateTime(2012, 02, 01, 00, 45, 00); + var content = $"\"{expected.ToString("O")}\""; var formatter = GetInputFormatter(); var contentBytes = Encoding.UTF8.GetBytes(content); @@ -120,7 +121,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters // Assert Assert.False(result.HasError); var dateValue = Assert.IsType(result.Model); - Assert.Equal(new DateTime(2012, 02, 01, 00, 45, 00), dateValue); + Assert.Equal(expected, dateValue); } [Fact] @@ -129,7 +130,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters // Arrange var formatter = GetInputFormatter(); - var content = "{\"Name\": \"Person Name\", \"Age\": 30}"; + var content = "{\"name\": \"Person Name\", \"age\": 30}"; var contentBytes = Encoding.UTF8.GetBytes(content); var httpContext = GetHttpContext(contentBytes); @@ -194,8 +195,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters // Assert Assert.False(result.HasError); - var integers = Assert.IsType>(result.Model); - Assert.Equal(new int[] { 0, 23, 300 }, integers); + Assert.IsAssignableFrom(requestedType, result.Model); + Assert.Equal(new int[] { 0, 23, 300 }, (IEnumerable)result.Model); } [Fact] diff --git a/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonInputFormatterTest.cs b/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonInputFormatterTest.cs index 72ac638842..020c357961 100644 --- a/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonInputFormatterTest.cs +++ b/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonInputFormatterTest.cs @@ -8,12 +8,6 @@ namespace Microsoft.AspNetCore.Mvc.Formatters { public class SystemTextJsonInputFormatterTest : JsonInputFormatterTestBase { - [Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/8489")] - public override Task JsonFormatterReadsDateTimeValue() - { - return base.JsonFormatterReadsDateTimeValue(); - } - [Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/8474")] public override Task ReadAsync_AddsModelValidationErrorsToModelState() { @@ -38,24 +32,6 @@ namespace Microsoft.AspNetCore.Mvc.Formatters return base.ReadAsync_UsesTryAddModelValidationErrorsToModelState(); } - [Fact(Skip = "https://github.com/dotnet/corefx/issues/36026")] - public override Task ReadAsync_ReadsValidArray_AsCollectionOfT() - { - return base.ReadAsync_ReadsValidArray_AsCollectionOfT(); - } - - [Fact(Skip = "https://github.com/dotnet/corefx/issues/36026")] - public override Task ReadAsync_ReadsValidArray_AsEnumerableOfT() - { - return base.ReadAsync_ReadsValidArray_AsEnumerableOfT(); - } - - [Fact(Skip = "https://github.com/dotnet/corefx/issues/36026")] - public override Task ReadAsync_ReadsValidArray_AsIListOfT() - { - return base.ReadAsync_ReadsValidArray_AsIListOfT(); - } - protected override TextInputFormatter GetInputFormatter() { return new SystemTextJsonInputFormatter(new MvcOptions()); diff --git a/src/Mvc/Mvc.NewtonsoftJson/test/BsonTempDataSerializerTest.cs b/src/Mvc/Mvc.NewtonsoftJson/test/BsonTempDataSerializerTest.cs index 2ef1519774..a2438b1b8b 100644 --- a/src/Mvc/Mvc.NewtonsoftJson/test/BsonTempDataSerializerTest.cs +++ b/src/Mvc/Mvc.NewtonsoftJson/test/BsonTempDataSerializerTest.cs @@ -213,6 +213,28 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson testProvider.EnsureObjectCanBeSerialized(value); } + [Fact] + public override void RoundTripTest_GuidToString() + { + // Documents the behavior of round-tripping a Guid value as a string + // Arrange + var key = "test-key"; + var testProvider = GetTempDataSerializer(); + var value = Guid.NewGuid(); + var input = new Dictionary + { + { key, value.ToString() } + }; + + // Act + var bytes = testProvider.Serialize(input); + var values = testProvider.Deserialize(bytes); + + // Assert + var roundTripValue = Assert.IsType(values[key]); + Assert.Equal(value.ToString(), roundTripValue); + } + private class TestItem { public int DummyInt { get; set; } diff --git a/src/Mvc/Mvc.ViewFeatures/src/Infrastructure/DefaultTempDataSerializer.cs b/src/Mvc/Mvc.ViewFeatures/src/Infrastructure/DefaultTempDataSerializer.cs index 026f4d0528..27b30c9dc6 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/Infrastructure/DefaultTempDataSerializer.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/Infrastructure/DefaultTempDataSerializer.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Text.Json; namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure @@ -46,19 +45,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure break; case JsonValueType.String: - var stringValue = item.Value.GetString(); - // BsonTempDataSerializer will parse certain types of string values. We'll attempt to imitiate it. - if (DateTime.TryParseExact(stringValue, "r", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dateTime)) - { - deserializedValue = dateTime; - } - else if (Guid.TryParseExact(stringValue, "B", out var guid)) + if (item.Value.TryGetGuid(out var guid)) { deserializedValue = guid; } + else if (item.Value.TryGetDateTime(out var dateTime)) + { + deserializedValue = dateTime; + } else { - deserializedValue = stringValue; + deserializedValue = item.Value.GetString(); } break; @@ -123,11 +120,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure break; case DateTime dateTime: - writer.WriteString(key, dateTime.ToString("r", CultureInfo.InvariantCulture)); + writer.WriteString(key, dateTime); break; case Guid guid: - writer.WriteString(key, guid.ToString("B", CultureInfo.InvariantCulture)); + writer.WriteString(key, guid); break; } } diff --git a/src/Mvc/Mvc.ViewFeatures/test/Infrastructure/DefaultTempDataSerializerTest.cs b/src/Mvc/Mvc.ViewFeatures/test/Infrastructure/DefaultTempDataSerializerTest.cs index 18f0f2f9cb..982e9fc9f1 100644 --- a/src/Mvc/Mvc.ViewFeatures/test/Infrastructure/DefaultTempDataSerializerTest.cs +++ b/src/Mvc/Mvc.ViewFeatures/test/Infrastructure/DefaultTempDataSerializerTest.cs @@ -12,11 +12,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure protected override TempDataSerializer GetTempDataSerializer() => new DefaultTempDataSerializer(); [Fact] - public void RoundTripTest_StringThatLooksLikeCompliantDateTime() + public void RoundTripTest_NonStandardDateTimeStringFormat_RoundTripsAsString() { - // This is an unintentional side-effect of trying to support a compat with JSON.NET. - // Any string that looks like a compliant DateTime object will be parsed as a DateTime. - // This test documents this behavior. + // DateTime that do not match the format that System.Text.Json uses for round-tripping + // should round-trip as strings. + // Arrange var key = "test-key"; var testProvider = GetTempDataSerializer(); @@ -30,57 +30,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure var bytes = testProvider.Serialize(input); var values = testProvider.Deserialize(bytes); - // Assert - var roundTripValue = Assert.IsType(values[key]); - Assert.Equal(value, roundTripValue); - } - - [Fact] - public void RoundTripTest_StringThatIsNotCompliantDateTime() - { - // This is an unintentional side-effect of trying to support a compat with JSON.NET. - // Any string that looks like a compliant DateTime object will be parsed as a DateTime. - // This test documents this behavior. - // Arrange - var key = "test-key"; - var testProvider = GetTempDataSerializer(); - var value = new DateTime(2009, 1, 1, 12, 37, 43); - var input = new Dictionary - { - { key, value.ToString() } - }; - - // Act - var bytes = testProvider.Serialize(input); - var values = testProvider.Deserialize(bytes); - // Assert var roundTripValue = Assert.IsType(values[key]); - Assert.Equal(value.ToString(), roundTripValue); - } - - [Fact] - public void RoundTripTest_StringThatIsNotCompliantGuid() - { - // This is an unintentional side-effect of trying to support a compat with JSON.NET. - // Any string that looks like a compliant DateTime object will be parsed as a DateTime. - // This test documents this behavior. - // Arrange - var key = "test-key"; - var testProvider = GetTempDataSerializer(); - var value = Guid.NewGuid(); - var input = new Dictionary - { - { key, value.ToString() } - }; - - // Act - var bytes = testProvider.Serialize(input); - var values = testProvider.Deserialize(bytes); - - // Assert - var roundTripValue = Assert.IsType(values[key]); - Assert.Equal(value.ToString(), roundTripValue); + Assert.Equal(value.ToString("r"), roundTripValue); } } } diff --git a/src/Mvc/Mvc.ViewFeatures/test/Infrastructure/TempDataSerializerTestBase.cs b/src/Mvc/Mvc.ViewFeatures/test/Infrastructure/TempDataSerializerTestBase.cs index b4030d9bea..38a540f8e7 100644 --- a/src/Mvc/Mvc.ViewFeatures/test/Infrastructure/TempDataSerializerTestBase.cs +++ b/src/Mvc/Mvc.ViewFeatures/test/Infrastructure/TempDataSerializerTestBase.cs @@ -173,6 +173,72 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure Assert.Equal(value, roundTripValue); } + [Fact] + public virtual void RoundTripTest_DateTimeToString() + { + // Documents the behavior of round-tripping a DateTime value as a string + // Arrange + var key = "test-key"; + var testProvider = GetTempDataSerializer(); + var value = new DateTime(2009, 1, 1, 12, 37, 43); + var input = new Dictionary + { + { key, value.ToString() } + }; + + // Act + var bytes = testProvider.Serialize(input); + var values = testProvider.Deserialize(bytes); + + // Assert + var roundTripValue = Assert.IsType(values[key]); + Assert.Equal(value.ToString(), roundTripValue); + } + + [Fact] + public virtual void RoundTripTest_StringThatIsNotCompliantGuid() + { + // Documents the behavior of round-tripping a Guid with a non-default format specifier + // Arrange + var key = "test-key"; + var testProvider = GetTempDataSerializer(); + var value = Guid.NewGuid(); + var input = new Dictionary + { + { key, value.ToString("N") } + }; + + // Act + var bytes = testProvider.Serialize(input); + var values = testProvider.Deserialize(bytes); + + // Assert + var roundTripValue = Assert.IsType(values[key]); + Assert.Equal(value.ToString("N"), roundTripValue); + } + + [Fact] + public virtual void RoundTripTest_GuidToString() + { + // Documents the behavior of round-tripping a Guid value as a string + // Arrange + var key = "test-key"; + var testProvider = GetTempDataSerializer(); + var value = Guid.NewGuid(); + var input = new Dictionary + { + { key, value.ToString() } + }; + + // Act + var bytes = testProvider.Serialize(input); + var values = testProvider.Deserialize(bytes); + + // Assert + var roundTripValue = Assert.IsType(values[key]); + Assert.Equal(value, roundTripValue); + } + protected abstract TempDataSerializer GetTempDataSerializer(); } } diff --git a/src/Mvc/test/Mvc.FunctionalTests/HtmlGenerationTest.cs b/src/Mvc/test/Mvc.FunctionalTests/HtmlGenerationTest.cs index de4a5dee17..75ac9df6d9 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/HtmlGenerationTest.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/HtmlGenerationTest.cs @@ -507,7 +507,7 @@ Products: Book1, Book2 (1)"; // Act - 3 // Trigger an expiration of the nested content. - var content = @"[{ ""ProductName"": ""Music Systems"" },{ ""ProductName"": ""Televisions"" }]"; + var content = @"[{ ""productName"": ""Music Systems"" },{ ""productName"": ""Televisions"" }]"; var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/categories/Electronics"); requestMessage.Content = new StringContent(content, Encoding.UTF8, "application/json"); (await Client.SendAsync(requestMessage)).EnsureSuccessStatusCode(); diff --git a/src/Mvc/test/Mvc.FunctionalTests/JsonInputFormatterTestBase.cs b/src/Mvc/test/Mvc.FunctionalTests/JsonInputFormatterTestBase.cs index a8940bceb6..ceeb88fd48 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/JsonInputFormatterTestBase.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/JsonInputFormatterTestBase.cs @@ -1,10 +1,12 @@ // 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; using System.Linq; using System.Net; using System.Net.Http; using System.Text; +using System.Text.Json.Serialization; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Xunit; @@ -31,7 +33,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests { // Arrange var sampleInputInt = 10; - var input = "{\"SampleInt\":10}"; + var input = "{\"sampleInt\":10}"; var content = new StringContent(input, Encoding.UTF8, requestContentType); // Act @@ -43,7 +45,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests } [Theory] - [InlineData("application/json", "{\"SampleInt\":10}", 10)] + [InlineData("application/json", "{\"sampleInt\":10}", 10)] [InlineData("application/json", "{}", 0)] public async Task JsonInputFormatter_IsModelStateValid_ForValidContentType( string requestContentType, @@ -100,7 +102,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests public async Task JsonInputFormatter_Returns415UnsupportedMediaType_ForEmptyContentType() { // Arrange - var jsonInput = "{\"SampleInt\":10}"; + var jsonInput = "{\"sampleInt\":10}"; var content = new StringContent(jsonInput, Encoding.UTF8, "application/json"); content.Headers.Clear(); @@ -112,7 +114,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests } [Theory] - [InlineData("application/json", "{\"SampleInt\":10}", 10)] + [InlineData("application/json", "{\"sampleInt\":10}", 10)] [InlineData("application/json", "{}", 0)] public async Task JsonInputFormatter_IsModelStateValid_ForTransferEncodingChunk( string requestContentType, @@ -133,6 +135,5 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(expectedSampleIntValue.ToString(), responseBody); } - } } \ No newline at end of file diff --git a/src/Mvc/test/Mvc.FunctionalTests/SystemTextJsonOutputFormatterTest.cs b/src/Mvc/test/Mvc.FunctionalTests/SystemTextJsonOutputFormatterTest.cs index 321c4b1de1..15af231bea 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/SystemTextJsonOutputFormatterTest.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/SystemTextJsonOutputFormatterTest.cs @@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests { } - [Fact(Skip = "Insert issue here")] + [Fact(Skip = "Dictionary serialization does not correctly work.")] public override Task SerializableErrorIsReturnedInExpectedFormat() => base.SerializableErrorIsReturnedInExpectedFormat(); [Fact] @@ -29,67 +29,13 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal("\"Hello Mr. \\ud83e\\udd8a\"", await response.Content.ReadAsStringAsync()); } - [Fact] - public override async Task Formatting_SimpleModel() - { - // Arrange - var expected = "{\"Id\":10,\"Name\":\"Test\",\"StreetName\":\"Some street\"}"; - - // Act - var response = await Client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.SimpleModelResult)}"); - - // Assert - await response.AssertStatusCodeAsync(HttpStatusCode.OK); - Assert.Equal(expected, await response.Content.ReadAsStringAsync()); - } - - [Fact] - public override async Task Formatting_CollectionType() - { - // Arrange - var expected = "[{\"Id\":10,\"Name\":\"TestName\",\"StreetName\":null},{\"Id\":11,\"Name\":\"TestName1\",\"StreetName\":\"Some street\"}]"; - - // Act - var response = await Client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.CollectionModelResult)}"); - - // Assert - await response.AssertStatusCodeAsync(HttpStatusCode.OK); - Assert.Equal(expected, await response.Content.ReadAsStringAsync()); - } - [Fact(Skip = "Dictionary serialization does not correctly work.")] public override Task Formatting_DictionaryType() => base.Formatting_DictionaryType(); [Fact(Skip = "Dictionary serialization does not correctly work.")] public override Task Formatting_ProblemDetails() => base.Formatting_ProblemDetails(); - [Fact] - public override async Task Formatting_PolymorphicModel() - { - // Arrange - var expected = "{\"Id\":10,\"Name\":\"test\",\"StreetName\":null}"; - - // Act - var response = await Client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.PolymorphicResult)}"); - - // Assert - await response.AssertStatusCodeAsync(HttpStatusCode.OK); - Assert.Equal(expected, await response.Content.ReadAsStringAsync()); - } - - [Fact] - public override async Task Formatting_LargeObject() - { - // Arrange - var expectedName = "This is long so we can test large objects " + new string('a', 1024 * 65); - var expected = $"{{\"Id\":10,\"Name\":\"{expectedName}\",\"StreetName\":null}}"; - - // Act - var response = await Client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.LargeObjectResult)}"); - - // Assert - await response.AssertStatusCodeAsync(HttpStatusCode.OK); - Assert.Equal(expected, await response.Content.ReadAsStringAsync()); - } + [Fact(Skip = "https://github.com/dotnet/corefx/issues/36166")] + public override Task Formatting_PolymorphicModel() => base.Formatting_PolymorphicModel(); } } \ No newline at end of file