From 1b868323e86e2f4cc11431a46ab28add27cef2f1 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 2 Apr 2019 10:34:50 -0700 Subject: [PATCH] Add a System.Text.Json based TempDataSerializer (#8874) * Add a System.Text.Json based TempDataSerializer * Update DefaultTempDataSerializer * Add common tests for DefaultTempDataSerializer & BsonTempDataSerializer * Remove uses of NewtonsoftJson in tests solely required for temp-data support Fixes https://github.com/aspnet/AspNetCore/issues/7255 --- .../src/BsonTempDataSerializer.cs | 6 +- .../test/BsonTempDataSerializerTest.cs | 145 ++++++++++---- ....AspNetCore.Mvc.NewtonsoftJson.Test.csproj | 1 + ...aFilterPageApplicationModelProviderTest.cs | 42 ++-- .../Filters/SaveTempDataPropertyFilterBase.cs | 26 ++- .../src/Infrastructure/ArrayBufferWriter.cs | 180 ++++++++++++++++++ .../DefaultTempDataSerializer.cs | 155 +++++++++++++-- .../src/Properties/Resources.Designer.cs | 28 +++ src/Mvc/Mvc.ViewFeatures/src/Resources.resx | 6 + .../TempDataApplicationModelProviderTest.cs | 37 ++-- .../DefaultTempDataSerializerTest.cs | 86 +++++++++ .../TempDataSerializerTestBase.cs | 179 +++++++++++++++++ .../Mvc.FunctionalTests/HtmlGenerationTest.cs | 2 +- .../Mvc.FunctionalTests/RazorPagesTest.cs | 2 +- .../WebSites/CorsWebSite/CorsWebSite.csproj | 3 +- src/Mvc/test/WebSites/CorsWebSite/Startup.cs | 1 - .../GenericHostWebSite.csproj | 2 - .../WebSites/GenericHostWebSite/Startup.cs | 7 +- .../HtmlGenerationWebSite.csproj | 3 +- .../WebSites/HtmlGenerationWebSite/Startup.cs | 1 - .../StartupWithoutEndpointRouting.cs | 1 - 21 files changed, 813 insertions(+), 100 deletions(-) create mode 100644 src/Mvc/Mvc.ViewFeatures/src/Infrastructure/ArrayBufferWriter.cs create mode 100644 src/Mvc/Mvc.ViewFeatures/test/Infrastructure/DefaultTempDataSerializerTest.cs create mode 100644 src/Mvc/Mvc.ViewFeatures/test/Infrastructure/TempDataSerializerTestBase.cs diff --git a/src/Mvc/Mvc.NewtonsoftJson/src/BsonTempDataSerializer.cs b/src/Mvc/Mvc.NewtonsoftJson/src/BsonTempDataSerializer.cs index 302c1b813b..a9d892f222 100644 --- a/src/Mvc/Mvc.NewtonsoftJson/src/BsonTempDataSerializer.cs +++ b/src/Mvc/Mvc.NewtonsoftJson/src/BsonTempDataSerializer.cs @@ -149,7 +149,7 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson } else { - return new byte[0]; + return Array.Empty(); } } @@ -165,6 +165,8 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson } } + public override bool CanSerializeType(Type type) => CanSerializeType(type, out _); + private static bool CanSerializeType(Type typeToSerialize, out string errorMessage) { typeToSerialize = typeToSerialize ?? throw new ArgumentNullException(nameof(typeToSerialize)); @@ -208,6 +210,8 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson } actualType = actualType ?? typeToSerialize; + actualType = Nullable.GetUnderlyingType(actualType) ?? actualType; + if (!IsSimpleType(actualType)) { errorMessage = Resources.FormatTempData_CannotSerializeType( diff --git a/src/Mvc/Mvc.NewtonsoftJson/test/BsonTempDataSerializerTest.cs b/src/Mvc/Mvc.NewtonsoftJson/test/BsonTempDataSerializerTest.cs index c2bc61d969..2ef1519774 100644 --- a/src/Mvc/Mvc.NewtonsoftJson/test/BsonTempDataSerializerTest.cs +++ b/src/Mvc/Mvc.NewtonsoftJson/test/BsonTempDataSerializerTest.cs @@ -3,12 +3,15 @@ using System; using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure; using Xunit; namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson { - public class BsonTempDataSerializerTest + public class BsonTempDataSerializerTest : TempDataSerializerTestBase { + protected override TempDataSerializer GetTempDataSerializer() => new BsonTempDataSerializer(); + public static TheoryData InvalidTypes { get @@ -93,6 +96,112 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson } } + [Fact] + public void RoundTripTest_ArrayOfIntegers() + { + // Arrange + var key = "test-key"; + var value = new[] { 1, -2, 3 }; + var testProvider = GetTempDataSerializer(); + var input = new Dictionary + { + { key, value }, + }; + + // Act + 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_DateTime() + { + // Arrange + var key = "test-key"; + var value = new DateTime(2007, 1, 1); + var testProvider = GetTempDataSerializer(); + var input = new Dictionary + { + { key, value }, + }; + + // Act + 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_Guid() + { + // Arrange + var key = "test-key"; + var value = Guid.NewGuid(); + var testProvider = GetTempDataSerializer(); + var input = new Dictionary + { + { key, value }, + }; + + // Act + var bytes = testProvider.Serialize(input); + var values = testProvider.Deserialize(bytes); + + // Assert + var roundTripValue = Assert.IsType(values[key]); + Assert.Equal(value, roundTripValue); + } + + [Theory] + [InlineData(2147483648)] + [InlineData(-2147483649)] + public void RoundTripTest_LongValue(long value) + { + // Arrange + var key = "test-key"; + var testProvider = GetTempDataSerializer(); + var input = new Dictionary + { + { key, value }, + }; + + // Act + 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_Double() + { + // Arrange + var key = "test-key"; + var value = 10d; + var testProvider = GetTempDataSerializer(); + var input = new Dictionary + { + { key, value }, + }; + + // Act + var bytes = testProvider.Serialize(input); + var values = testProvider.Deserialize(bytes); + + // Assert + var roundTripValue = (double)values[key]; + Assert.Equal(value, roundTripValue); + } + [Theory] [MemberData(nameof(ValidTypes))] public void EnsureObjectCanBeSerialized_DoesNotThrow_OnValidType(object value) @@ -104,40 +213,6 @@ namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson testProvider.EnsureObjectCanBeSerialized(value); } - [Fact] - public void DeserializeTempData_ReturnsEmptyDictionary_DataIsEmpty() - { - // Arrange - var serializer = new BsonTempDataSerializer(); - - // Act - var tempDataDictionary = serializer.Deserialize(new byte[0]); - - // Assert - Assert.NotNull(tempDataDictionary); - Assert.Empty(tempDataDictionary); - } - - [Fact] - public void SerializeAndDeserialize_NullValue_RoundTripsSuccessfully() - { - // Arrange - var key = "NullKey"; - var testProvider = new BsonTempDataSerializer(); - var input = new Dictionary - { - { key, null } - }; - - // Act - var bytes = testProvider.Serialize(input); - var values = testProvider.Deserialize(bytes); - - // Assert - Assert.True(values.ContainsKey(key)); - Assert.Null(values[key]); - } - private class TestItem { public int DummyInt { get; set; } diff --git a/src/Mvc/Mvc.NewtonsoftJson/test/Microsoft.AspNetCore.Mvc.NewtonsoftJson.Test.csproj b/src/Mvc/Mvc.NewtonsoftJson/test/Microsoft.AspNetCore.Mvc.NewtonsoftJson.Test.csproj index 412691fd2d..c843466c52 100644 --- a/src/Mvc/Mvc.NewtonsoftJson/test/Microsoft.AspNetCore.Mvc.NewtonsoftJson.Test.csproj +++ b/src/Mvc/Mvc.NewtonsoftJson/test/Microsoft.AspNetCore.Mvc.NewtonsoftJson.Test.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Mvc/Mvc.RazorPages/test/ApplicationModels/TempDataFilterPageApplicationModelProviderTest.cs b/src/Mvc/Mvc.RazorPages/test/ApplicationModels/TempDataFilterPageApplicationModelProviderTest.cs index 42db7a3191..ef4ac84077 100644 --- a/src/Mvc/Mvc.RazorPages/test/ApplicationModels/TempDataFilterPageApplicationModelProviderTest.cs +++ b/src/Mvc/Mvc.RazorPages/test/ApplicationModels/TempDataFilterPageApplicationModelProviderTest.cs @@ -5,9 +5,9 @@ using System; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure; -using Moq; using Xunit; namespace Microsoft.AspNetCore.Mvc.ApplicationModels @@ -44,6 +44,23 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels Assert.Equal(expected, ex.Message); } + [Fact] + public void OnProvidersExecuting_ThrowsIfThePropertyTypeIsUnsupported() + { + // Arrange + var type = typeof(TestPageModel_InvalidProperties); + var expected = $"TempData serializer '{typeof(DefaultTempDataSerializer)}' cannot serialize property '{type}.ModelState' of type '{typeof(ModelStateDictionary)}'." + + Environment.NewLine + + $"TempData serializer '{typeof(DefaultTempDataSerializer)}' cannot serialize property '{type}.TimeZone' of type '{typeof(TimeZoneInfo)}'."; + + var provider = CreateProvider(); + var context = CreateProviderContext(type); + + // Act & Assert + var ex = Assert.Throws(() => provider.OnProvidersExecuting(context)); + Assert.Equal(expected, ex.Message); + } + [Fact] public void AddsTempDataPropertyFilter_ForTempDataAttributeProperties() { @@ -114,18 +131,9 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels return context; } - private static CompiledPageActionDescriptor CreateDescriptor(Type type) - { - return new CompiledPageActionDescriptor(new PageActionDescriptor()) - { - PageTypeInfo = typeof(TestPage).GetTypeInfo(), - HandlerTypeInfo = type.GetTypeInfo(), - }; - } - private static TempDataFilterPageApplicationModelProvider CreateProvider() { - var tempDataSerializer = Mock.Of(s => s.CanSerializeType(It.IsAny()) == true); + var tempDataSerializer = new DefaultTempDataSerializer(); return new TempDataFilterPageApplicationModelProvider(tempDataSerializer); } @@ -160,5 +168,17 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels [TempData] public string Test { get; private set; } } + + public class TestPageModel_InvalidProperties + { + [TempData] + public ModelStateDictionary ModelState { get; set; } + + [TempData] + public int SomeProperty { get; set; } + + [TempData] + public TimeZoneInfo TimeZone { get; set; } + } } } diff --git a/src/Mvc/Mvc.ViewFeatures/src/Filters/SaveTempDataPropertyFilterBase.cs b/src/Mvc/Mvc.ViewFeatures/src/Filters/SaveTempDataPropertyFilterBase.cs index 53c5ca3751..e89f72319b 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/Filters/SaveTempDataPropertyFilterBase.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/Filters/SaveTempDataPropertyFilterBase.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.Reflection; using Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure; @@ -86,6 +87,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Filters Type type) { List results = null; + var errorMessages = new List(); var propertyHelpers = PropertyHelper.GetVisibleProperties(type: type); for (var i = 0; i < propertyHelpers.Length; i++) @@ -93,9 +95,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Filters var propertyHelper = propertyHelpers[i]; var property = propertyHelper.Property; var tempDataAttribute = property.GetCustomAttribute(); - if (tempDataAttribute != null) + if (tempDataAttribute != null && ValidateProperty(tempDataSerializer, errorMessages, propertyHelper.Property)) { - ValidateProperty(tempDataSerializer, propertyHelper.Property); if (results == null) { results = new List(); @@ -111,28 +112,41 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Filters } } + if (errorMessages.Count > 0) + { + throw new InvalidOperationException(string.Join(Environment.NewLine, errorMessages)); + } + return results; } - private static void ValidateProperty(TempDataSerializer tempDataSerializer, PropertyInfo property) + private static bool ValidateProperty(TempDataSerializer tempDataSerializer, List errorMessages, PropertyInfo property) { if (!(property.SetMethod != null && property.SetMethod.IsPublic && property.GetMethod != null && property.GetMethod.IsPublic)) { - throw new InvalidOperationException( + errorMessages.Add( Resources.FormatTempDataProperties_PublicGetterSetter(property.DeclaringType.FullName, property.Name, nameof(TempDataAttribute))); + + return false; } if (!tempDataSerializer.CanSerializeType(property.PropertyType)) { - throw new InvalidOperationException(Resources.FormatTempDataProperties_InvalidType( + var errorMessage = Resources.FormatTempDataProperties_InvalidType( tempDataSerializer.GetType().FullName, TypeNameHelper.GetTypeDisplayName(property.DeclaringType), property.Name, - TypeNameHelper.GetTypeDisplayName(property.PropertyType))); + TypeNameHelper.GetTypeDisplayName(property.PropertyType)); + + errorMessages.Add(errorMessage); + + return false; } + + return true; } } } diff --git a/src/Mvc/Mvc.ViewFeatures/src/Infrastructure/ArrayBufferWriter.cs b/src/Mvc/Mvc.ViewFeatures/src/Infrastructure/ArrayBufferWriter.cs new file mode 100644 index 0000000000..da8f38aa2f --- /dev/null +++ b/src/Mvc/Mvc.ViewFeatures/src/Infrastructure/ArrayBufferWriter.cs @@ -0,0 +1,180 @@ +// 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.Buffers; +using System.Diagnostics; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure +{ + // Note: this is currently an internal class that will be replaced with a shared version. + internal sealed class ArrayBufferWriter : IBufferWriter, IDisposable + { + private T[] _rentedBuffer; + private int _index; + + private const int MinimumBufferSize = 256; + + public ArrayBufferWriter() + { + _rentedBuffer = ArrayPool.Shared.Rent(MinimumBufferSize); + _index = 0; + } + + public ArrayBufferWriter(int initialCapacity) + { + if (initialCapacity <= 0) + throw new ArgumentException(nameof(initialCapacity)); + + _rentedBuffer = ArrayPool.Shared.Rent(initialCapacity); + _index = 0; + } + + public ReadOnlyMemory WrittenMemory + { + get + { + CheckIfDisposed(); + + return _rentedBuffer.AsMemory(0, _index); + } + } + + public int WrittenCount + { + get + { + CheckIfDisposed(); + + return _index; + } + } + + public int Capacity + { + get + { + CheckIfDisposed(); + + return _rentedBuffer.Length; + } + } + + public int FreeCapacity + { + get + { + CheckIfDisposed(); + + return _rentedBuffer.Length - _index; + } + } + + public void Clear() + { + CheckIfDisposed(); + + ClearHelper(); + } + + private void ClearHelper() + { + Debug.Assert(_rentedBuffer != null); + + _rentedBuffer.AsSpan(0, _index).Clear(); + _index = 0; + } + + // Returns the rented buffer back to the pool + public void Dispose() + { + if (_rentedBuffer == null) + { + return; + } + + ClearHelper(); + ArrayPool.Shared.Return(_rentedBuffer); + _rentedBuffer = null; + } + + private void CheckIfDisposed() + { + if (_rentedBuffer == null) + { + throw new ObjectDisposedException(nameof(ArrayBufferWriter)); + } + } + + public void Advance(int count) + { + CheckIfDisposed(); + + if (count < 0) + throw new ArgumentException(nameof(count)); + + if (_index > _rentedBuffer.Length - count) + ThrowInvalidOperationException(_rentedBuffer.Length); + + _index += count; + } + + public Memory GetMemory(int sizeHint = 0) + { + CheckIfDisposed(); + + CheckAndResizeBuffer(sizeHint); + return _rentedBuffer.AsMemory(_index); + } + + public Span GetSpan(int sizeHint = 0) + { + CheckIfDisposed(); + + CheckAndResizeBuffer(sizeHint); + return _rentedBuffer.AsSpan(_index); + } + + private void CheckAndResizeBuffer(int sizeHint) + { + Debug.Assert(_rentedBuffer != null); + + if (sizeHint < 0) + throw new ArgumentException(nameof(sizeHint)); + + if (sizeHint == 0) + { + sizeHint = MinimumBufferSize; + } + + int availableSpace = _rentedBuffer.Length - _index; + + if (sizeHint > availableSpace) + { + int growBy = Math.Max(sizeHint, _rentedBuffer.Length); + + int newSize = checked(_rentedBuffer.Length + growBy); + + T[] oldBuffer = _rentedBuffer; + + _rentedBuffer = ArrayPool.Shared.Rent(newSize); + + Debug.Assert(oldBuffer.Length >= _index); + Debug.Assert(_rentedBuffer.Length >= _index); + + Span previousBuffer = oldBuffer.AsSpan(0, _index); + previousBuffer.CopyTo(_rentedBuffer); + previousBuffer.Clear(); + ArrayPool.Shared.Return(oldBuffer); + } + + Debug.Assert(_rentedBuffer.Length - _index > 0); + Debug.Assert(_rentedBuffer.Length - _index >= sizeHint); + } + + private static void ThrowInvalidOperationException(int capacity) + { + throw new InvalidOperationException($"Cannot advance past the end of the buffer, which has a size of {capacity}."); + } + } +} diff --git a/src/Mvc/Mvc.ViewFeatures/src/Infrastructure/DefaultTempDataSerializer.cs b/src/Mvc/Mvc.ViewFeatures/src/Infrastructure/DefaultTempDataSerializer.cs index 7c2ebfb86a..b5ff663db4 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/Infrastructure/DefaultTempDataSerializer.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/Infrastructure/DefaultTempDataSerializer.cs @@ -3,30 +3,157 @@ using System; using System.Collections.Generic; -using Microsoft.Extensions.DependencyInjection; +using System.Globalization; +using System.Text.Json; namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure { internal class DefaultTempDataSerializer : TempDataSerializer { - public override IDictionary Deserialize(byte[] unprotectedData) + public override IDictionary Deserialize(byte[] value) { - throw new InvalidOperationException(Core.Resources.FormatReferenceToNewtonsoftJsonRequired( - Resources.DeserializingTempData, - "Microsoft.AspNetCore.Mvc.NewtonsoftJson", - nameof(IMvcBuilder), - "AddNewtonsoftJson", - "ConfigureServices(...)")); + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (value.Length == 0) + { + return new Dictionary(); + } + + using var jsonDocument = JsonDocument.Parse(value); + var rootElement = jsonDocument.RootElement; + return DeserializeDictionary(rootElement); + } + + private IDictionary DeserializeDictionary(JsonElement rootElement) + { + var deserialized = new Dictionary(StringComparer.Ordinal); + + foreach (var item in rootElement.EnumerateObject()) + { + object deserializedValue; + switch (item.Value.Type) + { + case JsonValueType.False: + case JsonValueType.True: + deserializedValue = item.Value.GetBoolean(); + break; + + case JsonValueType.Number: + deserializedValue = item.Value.GetInt32(); + 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)) + { + deserializedValue = guid; + } + else + { + deserializedValue = stringValue; + } + break; + + case JsonValueType.Null: + deserializedValue = null; + break; + + default: + throw new InvalidOperationException(Resources.FormatTempData_CannotDeserializeType(item.Value.Type)); + } + + deserialized[item.Name] = deserializedValue; + } + + return deserialized; } public override byte[] Serialize(IDictionary values) { - throw new InvalidOperationException(Core.Resources.FormatReferenceToNewtonsoftJsonRequired( - Resources.SerializingTempData, - "Microsoft.AspNetCore.Mvc.NewtonsoftJson", - nameof(IMvcBuilder), - "AddNewtonsoftJson", - "ConfigureServices(...)")); + if (values == null || values.Count == 0) + { + return Array.Empty(); + } + + using (var bufferWriter = new ArrayBufferWriter()) + { + var writer = new Utf8JsonWriter(bufferWriter); + writer.WriteStartObject(); + foreach (var (key, value) in values) + { + if (value == null) + { + writer.WriteNull(key); + continue; + } + + // We want to allow only simple types to be serialized. + if (!CanSerializeType(value.GetType())) + { + throw new InvalidOperationException( + Resources.FormatTempData_CannotSerializeType( + typeof(DefaultTempDataSerializer).FullName, + value.GetType())); + } + + switch (value) + { + case Enum _: + writer.WriteNumber(key, (int)value); + break; + + case string stringValue: + writer.WriteString(key, stringValue); + break; + + case int intValue: + writer.WriteNumber(key, intValue); + break; + + case bool boolValue: + writer.WriteBoolean(key, boolValue); + break; + + case DateTime dateTime: + writer.WriteString(key, dateTime.ToString("r", CultureInfo.InvariantCulture)); + break; + + case Guid guid: + writer.WriteString(key, guid.ToString("B", CultureInfo.InvariantCulture)); + break; + } + } + writer.WriteEndObject(); + writer.Flush(); + + return bufferWriter.WrittenMemory.ToArray(); + } + } + + public override bool CanSerializeType(Type type) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + type = Nullable.GetUnderlyingType(type) ?? type; + + return + type.IsEnum || + type == typeof(int) || + type == typeof(string) || + type == typeof(bool) || + type == typeof(DateTime) || + type == typeof(Guid); } } } diff --git a/src/Mvc/Mvc.ViewFeatures/src/Properties/Resources.Designer.cs b/src/Mvc/Mvc.ViewFeatures/src/Properties/Resources.Designer.cs index c83ef0745a..0de43a7bd8 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/Properties/Resources.Designer.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/Properties/Resources.Designer.cs @@ -808,6 +808,34 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures internal static string FormatSerializingTempData() => GetString("SerializingTempData"); + /// + /// The '{0}' cannot serialize an object of type '{1}'. + /// + internal static string TempData_CannotSerializeType + { + get => GetString("TempData_CannotSerializeType"); + } + + /// + /// The '{0}' cannot serialize an object of type '{1}'. + /// + internal static string FormatTempData_CannotSerializeType(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("TempData_CannotSerializeType"), p0, p1); + + /// + /// Unsupported data type '{0}'. + /// + internal static string TempData_CannotDeserializeType + { + get => GetString("TempData_CannotDeserializeType"); + } + + /// + /// Unsupported data type '{0}'. + /// + internal static string FormatTempData_CannotDeserializeType(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("TempData_CannotDeserializeType"), p0); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Mvc/Mvc.ViewFeatures/src/Resources.resx b/src/Mvc/Mvc.ViewFeatures/src/Resources.resx index c1d17c6e7b..e7362a9bf5 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/Resources.resx +++ b/src/Mvc/Mvc.ViewFeatures/src/Resources.resx @@ -289,4 +289,10 @@ Serializing TempDataDictionary + + The '{0}' cannot serialize an object of type '{1}'. + + + Unsupported data type '{0}'. + \ No newline at end of file diff --git a/src/Mvc/Mvc.ViewFeatures/test/Filters/TempDataApplicationModelProviderTest.cs b/src/Mvc/Mvc.ViewFeatures/test/Filters/TempDataApplicationModelProviderTest.cs index 404bf2a4ab..8120489280 100644 --- a/src/Mvc/Mvc.ViewFeatures/test/Filters/TempDataApplicationModelProviderTest.cs +++ b/src/Mvc/Mvc.ViewFeatures/test/Filters/TempDataApplicationModelProviderTest.cs @@ -8,7 +8,6 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure; using Microsoft.Extensions.Options; -using Moq; using Xunit; namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Filters @@ -48,20 +47,20 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Filters } [Fact] - public void AddsTempDataPropertyFilter_ForTempDataAttributeProperties() + public void OnProvidersExecuting_ThrowsIfThePropertyTypeIsUnsupported() { // Arrange - var type = typeof(TestController_NullableNonPrimitiveTempDataProperty); + var type = typeof(TestController_InvalidProperties); + var expected = $"TempData serializer '{typeof(DefaultTempDataSerializer)}' cannot serialize property '{type}.ModelState' of type '{typeof(ModelStateDictionary)}'." + + Environment.NewLine + + $"TempData serializer '{typeof(DefaultTempDataSerializer)}' cannot serialize property '{type}.TimeZone' of type '{typeof(TimeZoneInfo)}'."; var provider = CreateProvider(); var context = GetContext(type); - // Act - provider.OnProvidersExecuting(context); - - // Assert - var controller = Assert.Single(context.Result.Controllers); - Assert.IsType(Assert.Single(controller.Filters)); + // Act & Assert + var ex = Assert.Throws(() => provider.OnProvidersExecuting(context)); + Assert.Equal(expected, ex.Message); } [Fact] @@ -109,7 +108,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Filters private static TempDataApplicationModelProvider CreateProvider() { - var tempDataSerializer = Mock.Of(s => s.CanSerializeType(It.IsAny()) == true); + var tempDataSerializer = new DefaultTempDataSerializer(); return new TempDataApplicationModelProvider(tempDataSerializer); } @@ -129,12 +128,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Filters public DateTime? DateTime { get; set; } } - public class TestController_NullableNonPrimitiveTempDataProperty - { - [TempData] - public DateTime? DateTime { get; set; } - } - public class TestController_OneTempDataProperty { public string Test { get; set; } @@ -148,5 +141,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Filters [TempData] public string Test { get; private set; } } + + public class TestController_InvalidProperties + { + [TempData] + public ModelStateDictionary ModelState { get; set; } + + [TempData] + public int SomeProperty { get; set; } + + [TempData] + public TimeZoneInfo TimeZone { get; set; } + } } } diff --git a/src/Mvc/Mvc.ViewFeatures/test/Infrastructure/DefaultTempDataSerializerTest.cs b/src/Mvc/Mvc.ViewFeatures/test/Infrastructure/DefaultTempDataSerializerTest.cs new file mode 100644 index 0000000000..18f0f2f9cb --- /dev/null +++ b/src/Mvc/Mvc.ViewFeatures/test/Infrastructure/DefaultTempDataSerializerTest.cs @@ -0,0 +1,86 @@ +// 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.Collections.Generic; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure +{ + public class DefaultTempDataSerializerTest : TempDataSerializerTestBase + { + protected override TempDataSerializer GetTempDataSerializer() => new DefaultTempDataSerializer(); + + [Fact] + public void RoundTripTest_StringThatLooksLikeCompliantDateTime() + { + // 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("r") } + }; + + // Act + 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); + } + } +} diff --git a/src/Mvc/Mvc.ViewFeatures/test/Infrastructure/TempDataSerializerTestBase.cs b/src/Mvc/Mvc.ViewFeatures/test/Infrastructure/TempDataSerializerTestBase.cs new file mode 100644 index 0000000000..b4030d9bea --- /dev/null +++ b/src/Mvc/Mvc.ViewFeatures/test/Infrastructure/TempDataSerializerTestBase.cs @@ -0,0 +1,179 @@ +// 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.Collections.Generic; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure +{ + public abstract class TempDataSerializerTestBase + { + [Fact] + public void DeserializeTempData_ReturnsEmptyDictionary_DataIsEmpty() + { + // Arrange + var serializer = GetTempDataSerializer(); + + // Act + var tempDataDictionary = serializer.Deserialize(new byte[0]); + + // Assert + Assert.NotNull(tempDataDictionary); + Assert.Empty(tempDataDictionary); + } + + [Fact] + public void RoundTripTest_NullValue() + { + // Arrange + var key = "NullKey"; + var testProvider = GetTempDataSerializer(); + var input = new Dictionary + { + { key, null } + }; + + // Act + var bytes = testProvider.Serialize(input); + var values = testProvider.Deserialize(bytes); + + // Assert + Assert.True(values.ContainsKey(key)); + Assert.Null(values[key]); + } + + [Theory] + [InlineData(-10)] + [InlineData(3340)] + [InlineData(int.MaxValue)] + [InlineData(int.MinValue)] + public void RoundTripTest_IntValue(int value) + { + // Arrange + var key = "test-key"; + var testProvider = GetTempDataSerializer(); + var input = new Dictionary + { + { key, value }, + }; + + // Act + var bytes = testProvider.Serialize(input); + var values = testProvider.Deserialize(bytes); + + // Assert + var roundTripValue = Assert.IsType(values[key]); + Assert.Equal(value, roundTripValue); + } + + [Theory] + [InlineData(null)] + [InlineData(10)] + public void RoundTripTest_NullableInt(int? value) + { + // Arrange + var key = "test-key"; + var testProvider = GetTempDataSerializer(); + var input = new Dictionary + { + { key, value }, + }; + + // Act + var bytes = testProvider.Serialize(input); + var values = testProvider.Deserialize(bytes); + + // Assert + var roundTripValue = (int?)values[key]; + Assert.Equal(value, roundTripValue); + } + + [Fact] + public void RoundTripTest_StringValue() + { + // Arrange + var key = "test-key"; + var value = "test-value"; + var testProvider = GetTempDataSerializer(); + var input = new Dictionary + { + { key, value }, + }; + + // Act + 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_Enum() + { + // Arrange + var key = "test-key"; + var value = DayOfWeek.Friday; + var testProvider = GetTempDataSerializer(); + var input = new Dictionary + { + { key, value }, + }; + + // Act + var bytes = testProvider.Serialize(input); + var values = testProvider.Deserialize(bytes); + + // Assert + var roundTripValue = (DayOfWeek)values[key]; + Assert.Equal(value, roundTripValue); + } + + [Fact] + public void RoundTripTest_DateTimeValue() + { + // Arrange + var key = "test-key"; + var value = new DateTime(2009, 1, 1, 12, 37, 43); + var testProvider = GetTempDataSerializer(); + var input = new Dictionary + { + { key, value }, + }; + + // Act + 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_GuidValue() + { + // Arrange + var key = "test-key"; + var testProvider = GetTempDataSerializer(); + var value = Guid.NewGuid(); + var input = new Dictionary + { + { key, value } + }; + + // 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 4288fbff8c..de4a5dee17 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/RazorPagesTest.cs b/src/Mvc/test/Mvc.FunctionalTests/RazorPagesTest.cs index 8f4f5be2c0..8ecd44c0bd 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/RazorPagesTest.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/RazorPagesTest.cs @@ -784,7 +784,7 @@ Hello from /Pages/WithViewStart/Index.cshtml!"; Assert.Equal(expected, content); } - [Fact] + [Fact(Skip = "https://github.com/dotnet/corefx/issues/36024")] public async Task PolymorphicPropertiesOnPageModelsAreValidated() { // Arrange diff --git a/src/Mvc/test/WebSites/CorsWebSite/CorsWebSite.csproj b/src/Mvc/test/WebSites/CorsWebSite/CorsWebSite.csproj index 40bbdd9c39..87070b5059 100644 --- a/src/Mvc/test/WebSites/CorsWebSite/CorsWebSite.csproj +++ b/src/Mvc/test/WebSites/CorsWebSite/CorsWebSite.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 @@ -8,7 +8,6 @@ - diff --git a/src/Mvc/test/WebSites/CorsWebSite/Startup.cs b/src/Mvc/test/WebSites/CorsWebSite/Startup.cs index b8d4d7c727..dbfc16d1cd 100644 --- a/src/Mvc/test/WebSites/CorsWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/CorsWebSite/Startup.cs @@ -13,7 +13,6 @@ namespace CorsWebSite public void ConfigureServices(IServiceCollection services) { services.AddControllers(ConfigureMvcOptions) - .AddNewtonsoftJson() .SetCompatibilityVersion(CompatibilityVersion.Latest); services.Configure(options => { diff --git a/src/Mvc/test/WebSites/GenericHostWebSite/GenericHostWebSite.csproj b/src/Mvc/test/WebSites/GenericHostWebSite/GenericHostWebSite.csproj index 6f600027f6..dfb9a568e0 100644 --- a/src/Mvc/test/WebSites/GenericHostWebSite/GenericHostWebSite.csproj +++ b/src/Mvc/test/WebSites/GenericHostWebSite/GenericHostWebSite.csproj @@ -11,8 +11,6 @@ - - diff --git a/src/Mvc/test/WebSites/GenericHostWebSite/Startup.cs b/src/Mvc/test/WebSites/GenericHostWebSite/Startup.cs index 055adbe13b..75362be7ec 100644 --- a/src/Mvc/test/WebSites/GenericHostWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/GenericHostWebSite/Startup.cs @@ -1,11 +1,8 @@ // 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 Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.DependencyInjection; @@ -24,9 +21,7 @@ namespace GenericHostWebSite // Remove when all URL generation tests are passing - https://github.com/aspnet/Routing/issues/590 options.EnableEndpointRouting = false; }) - .SetCompatibilityVersion(CompatibilityVersion.Latest) - .AddNewtonsoftJson() - .AddXmlDataContractSerializerFormatters(); + .SetCompatibilityVersion(CompatibilityVersion.Latest); services.AddLogging(); services.AddHttpContextAccessor(); diff --git a/src/Mvc/test/WebSites/HtmlGenerationWebSite/HtmlGenerationWebSite.csproj b/src/Mvc/test/WebSites/HtmlGenerationWebSite/HtmlGenerationWebSite.csproj index a114481270..4ab1cb7bef 100644 --- a/src/Mvc/test/WebSites/HtmlGenerationWebSite/HtmlGenerationWebSite.csproj +++ b/src/Mvc/test/WebSites/HtmlGenerationWebSite/HtmlGenerationWebSite.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 @@ -8,7 +8,6 @@ - diff --git a/src/Mvc/test/WebSites/HtmlGenerationWebSite/Startup.cs b/src/Mvc/test/WebSites/HtmlGenerationWebSite/Startup.cs index b1efdeb4ed..bf34e47faa 100644 --- a/src/Mvc/test/WebSites/HtmlGenerationWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/HtmlGenerationWebSite/Startup.cs @@ -19,7 +19,6 @@ namespace HtmlGenerationWebSite // null which is interpreted as true unless element includes an action attribute. services.AddMvc(ConfigureMvcOptions) .InitializeTagHelper((helper, _) => helper.Antiforgery = false) - .AddNewtonsoftJson() .SetCompatibilityVersion(CompatibilityVersion.Latest); services.AddSingleton(typeof(ISignalTokenProviderService<>), typeof(SignalTokenProviderService<>)); diff --git a/src/Mvc/test/WebSites/RazorPagesWebSite/StartupWithoutEndpointRouting.cs b/src/Mvc/test/WebSites/RazorPagesWebSite/StartupWithoutEndpointRouting.cs index 4b08d0c458..a091d0a86d 100644 --- a/src/Mvc/test/WebSites/RazorPagesWebSite/StartupWithoutEndpointRouting.cs +++ b/src/Mvc/test/WebSites/RazorPagesWebSite/StartupWithoutEndpointRouting.cs @@ -17,7 +17,6 @@ namespace RazorPagesWebSite services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options => options.LoginPath = "/Login"); services.AddMvc(options => options.EnableEndpointRouting = false) .AddMvcLocalization() - .AddNewtonsoftJson() .AddRazorPagesOptions(options => { options.Conventions.AuthorizePage("/HelloWorldWithAuth");