diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/CookieTempDataProviderOptions.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/CookieTempDataProviderOptions.cs new file mode 100644 index 0000000000..42c8275816 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/CookieTempDataProviderOptions.cs @@ -0,0 +1,24 @@ +// 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.Http; +using Microsoft.AspNetCore.Mvc.ViewFeatures; + +namespace Microsoft.AspNetCore.Mvc +{ + /// + /// Provides programmatic configuration for cookies set by + /// + public class CookieTempDataProviderOptions + { + /// + /// The path set for a cookie. If not specified, current request's value is used. + /// + public string Path { get; set; } + + /// + /// The domain set on a cookie. Defaults to null. + /// + public string Domain { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/SaveTempDataFilter.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/SaveTempDataFilter.cs index 282976777e..f430ba7b08 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/SaveTempDataFilter.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/SaveTempDataFilter.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Internal; namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal { @@ -10,6 +12,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal /// public class SaveTempDataFilter : IResourceFilter, IResultFilter { + // Internal for unit testing + internal static readonly object TempDataSavedKey = new object(); + private readonly ITempDataDictionaryFactory _factory; /// @@ -29,21 +34,68 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal /// public void OnResourceExecuted(ResourceExecutedContext context) { - _factory.GetTempData(context.HttpContext).Save(); } /// public void OnResultExecuting(ResultExecutingContext context) { + context.HttpContext.Response.OnStarting((state) => + { + var saveTempDataContext = (SaveTempDataContext)state; + + // If temp data was already saved, skip trying to save again as the calls here would potentially fail + // because the session feature might not be available at this point. + // Example: An action returns NoContentResult and since NoContentResult does not write anything to + // the body of the response, this delegate would get executed way late in the pipeline at which point + // the session feature would have been removed. + object obj; + if (saveTempDataContext.HttpContext.Items.TryGetValue(TempDataSavedKey, out obj)) + { + return TaskCache.CompletedTask; + } + + SaveTempData( + saveTempDataContext.ActionResult, + saveTempDataContext.TempDataDictionaryFactory, + saveTempDataContext.HttpContext); + + return TaskCache.CompletedTask; + }, + state: new SaveTempDataContext() + { + HttpContext = context.HttpContext, + ActionResult = context.Result, + TempDataDictionaryFactory = _factory + }); } /// public void OnResultExecuted(ResultExecutedContext context) { - if (context.Result is IKeepTempDataResult) + // We are doing this here again because the OnStarting delegate above might get fired too late in scenarios + // where the action result doesn't write anything to the body. This causes the delegate to be executed + // late in the pipeline at which point SessionFeature would not be available. + if (!context.HttpContext.Response.HasStarted) { - _factory.GetTempData(context.HttpContext).Keep(); + SaveTempData(context.Result, _factory, context.HttpContext); + context.HttpContext.Items.Add(TempDataSavedKey, true); } } + + private static void SaveTempData(IActionResult result, ITempDataDictionaryFactory factory, HttpContext httpContext) + { + if (result is IKeepTempDataResult) + { + factory.GetTempData(httpContext).Keep(); + } + factory.GetTempData(httpContext).Save(); + } + + private class SaveTempDataContext + { + public HttpContext HttpContext { get; set; } + public IActionResult ActionResult { get; set; } + public ITempDataDictionaryFactory TempDataDictionaryFactory { get; set; } + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/TempDataSerializer.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/TempDataSerializer.cs new file mode 100644 index 0000000000..78cb43a100 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/TempDataSerializer.cs @@ -0,0 +1,239 @@ +// 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.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Extensions.Internal; +using Newtonsoft.Json; +using Newtonsoft.Json.Bson; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal +{ + public class TempDataSerializer + { + private readonly JsonSerializer _jsonSerializer = + JsonSerializer.Create(JsonSerializerSettingsProvider.CreateSerializerSettings()); + + private static readonly MethodInfo _convertArrayMethodInfo = typeof(TempDataSerializer).GetMethod( + nameof(ConvertArray), BindingFlags.Static | BindingFlags.NonPublic); + private static readonly MethodInfo _convertDictionaryMethodInfo = typeof(TempDataSerializer).GetMethod( + nameof(ConvertDictionary), BindingFlags.Static | BindingFlags.NonPublic); + + private static readonly ConcurrentDictionary> _arrayConverters = + new ConcurrentDictionary>(); + private static readonly ConcurrentDictionary> _dictionaryConverters = + new ConcurrentDictionary>(); + + private static readonly Dictionary _tokenTypeLookup = new Dictionary + { + { JTokenType.String, typeof(string) }, + { JTokenType.Integer, typeof(int) }, + { JTokenType.Boolean, typeof(bool) }, + { JTokenType.Float, typeof(float) }, + { JTokenType.Guid, typeof(Guid) }, + { JTokenType.Date, typeof(DateTime) }, + { JTokenType.TimeSpan, typeof(TimeSpan) }, + { JTokenType.Uri, typeof(Uri) }, + }; + + public IDictionary Deserialize(byte[] value) + { + Dictionary tempDataDictionary = null; + + using (var memoryStream = new MemoryStream(value)) + using (var writer = new BsonReader(memoryStream)) + { + tempDataDictionary = _jsonSerializer.Deserialize>(writer); + if (tempDataDictionary == null) + { + return new Dictionary(StringComparer.OrdinalIgnoreCase); + } + } + + var convertedDictionary = new Dictionary( + tempDataDictionary, + StringComparer.OrdinalIgnoreCase); + foreach (var item in tempDataDictionary) + { + var jArrayValue = item.Value as JArray; + var jObjectValue = item.Value as JObject; + if (jArrayValue != null && jArrayValue.Count > 0) + { + var arrayType = jArrayValue[0].Type; + Type returnType; + if (_tokenTypeLookup.TryGetValue(arrayType, out returnType)) + { + var arrayConverter = _arrayConverters.GetOrAdd(returnType, type => + { + return (Func)_convertArrayMethodInfo + .MakeGenericMethod(type) + .CreateDelegate(typeof(Func)); + }); + var result = arrayConverter(jArrayValue); + + convertedDictionary[item.Key] = result; + } + else + { + var message = Resources.FormatTempData_CannotDeserializeToken(nameof(JToken), arrayType); + throw new InvalidOperationException(message); + } + } + else if (jObjectValue != null) + { + if (!jObjectValue.HasValues) + { + convertedDictionary[item.Key] = null; + continue; + } + + var jTokenType = jObjectValue.Properties().First().Value.Type; + Type valueType; + if (_tokenTypeLookup.TryGetValue(jTokenType, out valueType)) + { + var dictionaryConverter = _dictionaryConverters.GetOrAdd(valueType, type => + { + return (Func)_convertDictionaryMethodInfo + .MakeGenericMethod(type) + .CreateDelegate(typeof(Func)); + }); + var result = dictionaryConverter(jObjectValue); + + convertedDictionary[item.Key] = result; + } + else + { + var message = Resources.FormatTempData_CannotDeserializeToken(nameof(JToken), jTokenType); + throw new InvalidOperationException(message); + } + } + else if (item.Value is long) + { + var longValue = (long)item.Value; + if (longValue >= int.MinValue && longValue <= int.MaxValue) + { + // BsonReader casts all ints to longs. We'll attempt to work around this by force converting + // longs to ints when there's no loss of precision. + convertedDictionary[item.Key] = (int)longValue; + } + } + } + + return convertedDictionary ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + public byte[] Serialize(IDictionary values) + { + var hasValues = (values != null && values.Count > 0); + if (hasValues) + { + foreach (var item in values.Values) + { + if (item != null) + { + // We want to allow only simple types to be serialized. + EnsureObjectCanBeSerialized(item); + } + } + + using (var memoryStream = new MemoryStream()) + { + using (var writer = new BsonWriter(memoryStream)) + { + _jsonSerializer.Serialize(writer, values); + return memoryStream.ToArray(); + } + } + } + else + { + return new byte[0]; + } + } + + public void EnsureObjectCanBeSerialized(object item) + { + var itemType = item.GetType(); + Type actualType = null; + + if (itemType.IsArray) + { + itemType = itemType.GetElementType(); + } + else if (itemType.GetTypeInfo().IsGenericType) + { + if (ClosedGenericMatcher.ExtractGenericInterface(itemType, typeof(IList<>)) != null) + { + var genericTypeArguments = itemType.GenericTypeArguments; + Debug.Assert(genericTypeArguments.Length == 1, "IList has one generic argument"); + actualType = genericTypeArguments[0]; + } + else if (ClosedGenericMatcher.ExtractGenericInterface(itemType, typeof(IDictionary<,>)) != null) + { + var genericTypeArguments = itemType.GenericTypeArguments; + Debug.Assert( + genericTypeArguments.Length == 2, + "IDictionary has two generic arguments"); + + // Throw if the key type of the dictionary is not string. + if (genericTypeArguments[0] != typeof(string)) + { + var message = Resources.FormatTempData_CannotSerializeDictionary( + typeof(TempDataSerializer).FullName, genericTypeArguments[0]); + throw new InvalidOperationException(message); + } + else + { + actualType = genericTypeArguments[1]; + } + } + } + + actualType = actualType ?? itemType; + if (!IsSimpleType(actualType)) + { + var underlyingType = Nullable.GetUnderlyingType(actualType) ?? actualType; + var message = Resources.FormatTempData_CannotSerializeType( + typeof(TempDataSerializer).FullName, underlyingType); + throw new InvalidOperationException(message); + } + } + + private static IList ConvertArray(JArray array) + { + return array.Values().ToArray(); + } + + private static IDictionary ConvertDictionary(JObject jObject) + { + var convertedDictionary = new Dictionary(StringComparer.Ordinal); + foreach (var item in jObject) + { + convertedDictionary.Add(item.Key, jObject.Value(item.Key)); + } + return convertedDictionary; + } + + private static bool IsSimpleType(Type type) + { + var typeInfo = type.GetTypeInfo(); + + return typeInfo.IsPrimitive || + typeInfo.IsEnum || + type.Equals(typeof(decimal)) || + type.Equals(typeof(string)) || + type.Equals(typeof(DateTime)) || + type.Equals(typeof(Guid)) || + type.Equals(typeof(DateTimeOffset)) || + type.Equals(typeof(TimeSpan)) || + type.Equals(typeof(Uri)); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Properties/Resources.Designer.cs index a2db38f704..88113eae82 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Properties/Resources.Designer.cs @@ -779,7 +779,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures } /// - /// The '{0}' cannot serialize a dictionary with a key of type '{1}' to session state. + /// The '{0}' cannot serialize a dictionary with a key of type '{1}'. /// internal static string TempData_CannotSerializeDictionary { @@ -787,7 +787,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures } /// - /// The '{0}' cannot serialize a dictionary with a key of type '{1}' to session state. + /// The '{0}' cannot serialize a dictionary with a key of type '{1}'. /// internal static string FormatTempData_CannotSerializeDictionary(object p0, object p1) { @@ -795,19 +795,19 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures } /// - /// The '{0}' cannot serialize an object of type '{1}' to session state. + /// The '{0}' cannot serialize an object of type '{1}'. /// - internal static string TempData_CannotSerializeToSession + internal static string TempData_CannotSerializeType { - get { return GetString("TempData_CannotSerializeToSession"); } + get { return GetString("TempData_CannotSerializeType"); } } /// - /// The '{0}' cannot serialize an object of type '{1}' to session state. + /// The '{0}' cannot serialize an object of type '{1}'. /// - internal static string FormatTempData_CannotSerializeToSession(object p0, object p1) + internal static string FormatTempData_CannotSerializeType(object p0, object p1) { - return string.Format(CultureInfo.CurrentCulture, GetString("TempData_CannotSerializeToSession"), p0, p1); + return string.Format(CultureInfo.CurrentCulture, GetString("TempData_CannotSerializeType"), p0, p1); } /// diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Resources.resx b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Resources.resx index 55a2345b48..a1482ffde3 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Resources.resx @@ -1,17 +1,17 @@  - @@ -263,10 +263,10 @@ Cannot deserialize {0} of type '{1}'. - The '{0}' cannot serialize a dictionary with a key of type '{1}' to session state. + The '{0}' cannot serialize a dictionary with a key of type '{1}'. - - The '{0}' cannot serialize an object of type '{1}' to session state. + + The '{0}' cannot serialize an object of type '{1}'. The collection already contains an entry with key '{0}'. diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/CookieTempDataProvider.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/CookieTempDataProvider.cs new file mode 100644 index 0000000000..f232175d2d --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/CookieTempDataProvider.cs @@ -0,0 +1,85 @@ +// 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 Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Internal; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures +{ + /// + /// Provides data from cookie to the current object. + /// + public class CookieTempDataProvider : ITempDataProvider + { + public static readonly string CookieName = ".AspNetCore.Mvc.CookieTempDataProvider"; + private static readonly string Purpose = "Microsoft.AspNetCore.Mvc.CookieTempDataProviderToken.v1"; + private const byte TokenVersion = 0x01; + private readonly IDataProtector _dataProtector; + private readonly TempDataSerializer _tempDataSerializer; + private readonly ChunkingCookieManager _chunkingCookieManager; + private readonly IOptions _options; + + public CookieTempDataProvider(IDataProtectionProvider dataProtectionProvider, IOptions options) + { + _dataProtector = dataProtectionProvider.CreateProtector(Purpose); + _tempDataSerializer = new TempDataSerializer(); + _chunkingCookieManager = new ChunkingCookieManager(); + _options = options; + } + + public IDictionary LoadTempData(HttpContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Request.Cookies.ContainsKey(CookieName)) + { + var base64EncodedValue = _chunkingCookieManager.GetRequestCookie(context, CookieName); + if (!string.IsNullOrEmpty(base64EncodedValue)) + { + var protectedData = Convert.FromBase64String(base64EncodedValue); + var unprotectedData = _dataProtector.Unprotect(protectedData); + return _tempDataSerializer.Deserialize(unprotectedData); + } + } + + return new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + public void SaveTempData(HttpContext context, IDictionary values) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var cookieOptions = new CookieOptions() + { + Path = string.IsNullOrEmpty(_options.Value.Path) ? context.Request.PathBase.ToString() : _options.Value.Path, + Domain = string.IsNullOrEmpty(_options.Value.Domain) ? null : _options.Value.Domain, + HttpOnly = true, + Secure = true + }; + + var hasValues = (values != null && values.Count > 0); + if (hasValues) + { + var bytes = _tempDataSerializer.Serialize(values); + bytes = _dataProtector.Protect(bytes); + var base64EncodedValue = Convert.ToBase64String(bytes); + _chunkingCookieManager.AppendResponseCookie(context, CookieName, base64EncodedValue, cookieOptions); + } + else + { + _chunkingCookieManager.DeleteCookie(context, CookieName, cookieOptions); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/SaveTempDataAttribute.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/SaveTempDataAttribute.cs index a9c2b53ec7..3798bcfeee 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/SaveTempDataAttribute.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/SaveTempDataAttribute.cs @@ -14,7 +14,15 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class SaveTempDataAttribute : Attribute, IFilterFactory, IOrderedFilter { - /// + public SaveTempDataAttribute() + { + // Since SaveTempDataFilter registers for a response's OnStarting callback, we want this filter to run + // as early as possible to get the oppurtunity to register the call back before any other result filter + // starts writing to the response stream. + Order = int.MinValue + 100; + } + + // public int Order { get; set; } /// diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/SessionStateTempDataProvider.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/SessionStateTempDataProvider.cs index bfa32a70fd..f933e1163f 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/SessionStateTempDataProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/SessionStateTempDataProvider.cs @@ -2,18 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Reflection; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Internal; -using Microsoft.Extensions.Internal; -using Newtonsoft.Json; -using Newtonsoft.Json.Bson; -using Newtonsoft.Json.Linq; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; namespace Microsoft.AspNetCore.Mvc.ViewFeatures { @@ -24,34 +15,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures { // Internal for testing internal const string TempDataSessionStateKey = "__ControllerTempData"; + private readonly TempDataSerializer _tempDataSerializer; - private readonly JsonSerializer _jsonSerializer = JsonSerializer.Create( - new JsonSerializerSettings() - { - TypeNameHandling = TypeNameHandling.None - }); - - private static readonly MethodInfo _convertArrayMethodInfo = typeof(SessionStateTempDataProvider).GetMethod( - nameof(ConvertArray), BindingFlags.Static | BindingFlags.NonPublic); - private static readonly MethodInfo _convertDictMethodInfo = typeof(SessionStateTempDataProvider).GetMethod( - nameof(ConvertDictionary), BindingFlags.Static | BindingFlags.NonPublic); - - private static readonly ConcurrentDictionary> _arrayConverters = - new ConcurrentDictionary>(); - private static readonly ConcurrentDictionary> _dictionaryConverters = - new ConcurrentDictionary>(); - - private static readonly Dictionary _tokenTypeLookup = new Dictionary + public SessionStateTempDataProvider() { - { JTokenType.String, typeof(string) }, - { JTokenType.Integer, typeof(int) }, - { JTokenType.Boolean, typeof(bool) }, - { JTokenType.Float, typeof(float) }, - { JTokenType.Guid, typeof(Guid) }, - { JTokenType.Date, typeof(DateTime) }, - { JTokenType.TimeSpan, typeof(TimeSpan) }, - { JTokenType.Uri, typeof(Uri) }, - }; + _tempDataSerializer = new TempDataSerializer(); + } /// public virtual IDictionary LoadTempData(HttpContext context) @@ -64,103 +33,16 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures // Accessing Session property will throw if the session middleware is not enabled. var session = context.Session; - Dictionary tempDataDictionary = null; byte[] value; - if (session.TryGetValue(TempDataSessionStateKey, out value)) { // If we got it from Session, remove it so that no other request gets it session.Remove(TempDataSessionStateKey); - using (var memoryStream = new MemoryStream(value)) - using (var writer = new BsonReader(memoryStream)) - { - tempDataDictionary = _jsonSerializer.Deserialize>(writer); - if (tempDataDictionary == null) - { - return new Dictionary(StringComparer.OrdinalIgnoreCase); - } - } - - var convertedDictionary = new Dictionary( - tempDataDictionary, - StringComparer.OrdinalIgnoreCase); - foreach (var item in tempDataDictionary) - { - var jArrayValue = item.Value as JArray; - var jObjectValue = item.Value as JObject; - if (jArrayValue != null && jArrayValue.Count > 0) - { - var arrayType = jArrayValue[0].Type; - Type returnType; - if (_tokenTypeLookup.TryGetValue(arrayType, out returnType)) - { - var arrayConverter = _arrayConverters.GetOrAdd(returnType, type => - { - return (Func)_convertArrayMethodInfo - .MakeGenericMethod(type) - .CreateDelegate(typeof(Func)); - }); - var result = arrayConverter(jArrayValue); - - convertedDictionary[item.Key] = result; - } - else - { - var message = Resources.FormatTempData_CannotDeserializeToken(nameof(JToken), arrayType); - throw new InvalidOperationException(message); - } - } - else if (jObjectValue != null) - { - if (!jObjectValue.HasValues) - { - convertedDictionary[item.Key] = null; - continue; - } - - var jTokenType = jObjectValue.Properties().First().Value.Type; - Type valueType; - if (_tokenTypeLookup.TryGetValue(jTokenType, out valueType)) - { - var dictionaryConverter = _dictionaryConverters.GetOrAdd(valueType, type => - { - return (Func)_convertDictMethodInfo - .MakeGenericMethod(type) - .CreateDelegate(typeof(Func)); - }); - var result = dictionaryConverter(jObjectValue); - - convertedDictionary[item.Key] = result; - } - else - { - var message = Resources.FormatTempData_CannotDeserializeToken(nameof(JToken), jTokenType); - throw new InvalidOperationException(message); - } - } - else if (item.Value is long) - { - var longValue = (long)item.Value; - if (longValue >= int.MinValue && longValue <= int.MaxValue) - { - // BsonReader casts all ints to longs. We'll attempt to work around this by force converting - // longs to ints when there's no loss of precision. - convertedDictionary[item.Key] = (int)longValue; - } - } - } - - tempDataDictionary = convertedDictionary; - } - else - { - // Since we call Save() after the response has been sent, we need to initialize an empty session - // so that it is established before the headers are sent. - session.Set(TempDataSessionStateKey, EmptyArray.Instance); + return _tempDataSerializer.Deserialize(value); } - return tempDataDictionary ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + return new Dictionary(StringComparer.OrdinalIgnoreCase); } /// @@ -177,106 +59,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures var hasValues = (values != null && values.Count > 0); if (hasValues) { - foreach (var item in values.Values) - { - if (item != null) - { - // We want to allow only simple types to be serialized in session. - EnsureObjectCanBeSerialized(item); - } - } - - using (var memoryStream = new MemoryStream()) - { - using (var writer = new BsonWriter(memoryStream)) - { - _jsonSerializer.Serialize(writer, values); - session.Set(TempDataSessionStateKey, memoryStream.ToArray()); - } - } + var bytes = _tempDataSerializer.Serialize(values); + session.Set(TempDataSessionStateKey, bytes); } else { session.Remove(TempDataSessionStateKey); } } - - internal void EnsureObjectCanBeSerialized(object item) - { - var itemType = item.GetType(); - Type actualType = null; - - if (itemType.IsArray) - { - itemType = itemType.GetElementType(); - } - else if (itemType.GetTypeInfo().IsGenericType) - { - if (ClosedGenericMatcher.ExtractGenericInterface(itemType, typeof(IList<>)) != null) - { - var genericTypeArguments = itemType.GenericTypeArguments; - Debug.Assert(genericTypeArguments.Length == 1, "IList has one generic argument"); - actualType = genericTypeArguments[0]; - } - else if (ClosedGenericMatcher.ExtractGenericInterface(itemType, typeof(IDictionary<,>)) != null) - { - var genericTypeArguments = itemType.GenericTypeArguments; - Debug.Assert( - genericTypeArguments.Length == 2, - "IDictionary has two generic arguments"); - - // Throw if the key type of the dictionary is not string. - if (genericTypeArguments[0] != typeof(string)) - { - var message = Resources.FormatTempData_CannotSerializeDictionary( - typeof(SessionStateTempDataProvider).FullName, genericTypeArguments[0]); - throw new InvalidOperationException(message); - } - else - { - actualType = genericTypeArguments[1]; - } - } - } - - actualType = actualType ?? itemType; - if (!IsSimpleType(actualType)) - { - var underlyingType = Nullable.GetUnderlyingType(actualType) ?? actualType; - var message = Resources.FormatTempData_CannotSerializeToSession( - typeof(SessionStateTempDataProvider).FullName, underlyingType); - throw new InvalidOperationException(message); - } - } - - private static IList ConvertArray(JArray array) - { - return array.Values().ToArray(); - } - - private static IDictionary ConvertDictionary(JObject jObject) - { - var convertedDictionary = new Dictionary(StringComparer.Ordinal); - foreach (var item in jObject) - { - convertedDictionary.Add(item.Key, jObject.Value(item.Key)); - } - return convertedDictionary; - } - - private static bool IsSimpleType(Type type) - { - var typeInfo = type.GetTypeInfo(); - - return typeInfo.IsPrimitive || - typeInfo.IsEnum || - type.Equals(typeof(decimal)) || - type.Equals(typeof(string)) || - type.Equals(typeof(DateTime)) || - type.Equals(typeof(Guid)) || - type.Equals(typeof(DateTimeOffset)) || - type.Equals(typeof(TimeSpan)) || - type.Equals(typeof(Uri)); - } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/project.json b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/project.json index 3163542ae0..28b40ac98c 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/project.json +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/project.json @@ -22,6 +22,10 @@ }, "dependencies": { "Microsoft.AspNetCore.Antiforgery": "1.1.0-*", + "Microsoft.AspNetCore.ChunkingCookieManager.Sources": { + "version": "1.1.0-*", + "type": "build" + }, "Microsoft.AspNetCore.Diagnostics.Abstractions": "1.1.0-*", "Microsoft.AspNetCore.Html.Abstractions": "1.1.0-*", "Microsoft.AspNetCore.Mvc.Core": "1.1.0-*", @@ -35,6 +39,10 @@ "version": "1.1.0-*", "type": "build" }, + "Microsoft.Extensions.HashCodeCombiner.Sources": { + "version": "1.1.0-*", + "type": "build" + }, "Microsoft.Extensions.PropertyActivator.Sources": { "version": "1.1.0-*", "type": "build" @@ -43,10 +51,6 @@ "version": "1.1.0-*", "type": "build" }, - "Microsoft.Extensions.HashCodeCombiner.Sources": { - "version": "1.1.0-*", - "type": "build" - }, "Microsoft.Extensions.WebEncoders": "1.1.0-*", "Newtonsoft.Json": "9.0.1", "System.Buffers": "4.0.0-*" diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TempDataInCookiesTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TempDataInCookiesTest.cs new file mode 100644 index 0000000000..ed60b12c0d --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TempDataInCookiesTest.cs @@ -0,0 +1,54 @@ +// 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.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests +{ + public class TempDataInCookiesTest : TempDataTestBase, IClassFixture> + { + private const int DefaultChunkSize = 4070; + + public TempDataInCookiesTest(MvcTestFixture fixture) + { + Client = fixture.Client; + } + + protected override HttpClient Client { get; } + + [Theory] + [InlineData(DefaultChunkSize)] + [InlineData(DefaultChunkSize * 1.5)] + [InlineData(DefaultChunkSize * 2)] + [InlineData(DefaultChunkSize * 3)] + public async Task RoundTripLargeData_WorksWithChunkingCookies(int size) + { + // Arrange + var character = 'a'; + var expected = new string(character, size); + + // Act 1 + var response = await Client.GetAsync($"/TempData/SetLargeValueInTempData?size={size}&character={character}"); + + // Assert 1 + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + // Act 2 + response = await Client.SendAsync(GetRequest("/TempData/GetLargeValueFromTempData", response)); + + // Assert 2 + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + Assert.Equal(expected, body); + + // Act 3 + response = await Client.SendAsync(GetRequest("/TempData/GetLargeValueFromTempData", response)); + + // Assert 3 + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TempDataInSessionTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TempDataInSessionTest.cs new file mode 100644 index 0000000000..da2db04159 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TempDataInSessionTest.cs @@ -0,0 +1,18 @@ +// 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.Net.Http; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests +{ + public class TempDataInSessionTest : TempDataTestBase, IClassFixture> + { + public TempDataInSessionTest(MvcTestFixture fixture) + { + Client = fixture.Client; + } + + protected override HttpClient Client { get; } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TempDataTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TempDataTestBase.cs similarity index 72% rename from test/Microsoft.AspNetCore.Mvc.FunctionalTests/TempDataTest.cs rename to test/Microsoft.AspNetCore.Mvc.FunctionalTests/TempDataTestBase.cs index 95202807e5..0f88652357 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TempDataTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TempDataTestBase.cs @@ -1,4 +1,4 @@ -// 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. using System; @@ -7,24 +7,17 @@ using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.ViewFeatures; -using Microsoft.AspNetCore.Testing.xunit; using Microsoft.Net.Http.Headers; using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class TempDataTest : IClassFixture> + public abstract class TempDataTestBase { - public TempDataTest(MvcTestFixture fixture) - { - Client = fixture.Client; - } - - public HttpClient Client { get; } + protected abstract HttpClient Client { get; } [Fact] - public async Task TempData_PersistsJustForNextRequest() + public async Task PersistsJustForNextRequest() { // Arrange var nameValueCollection = new List> @@ -40,7 +33,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal(HttpStatusCode.OK, response.StatusCode); // Act 2 - response = await Client.SendAsync(GetRequest("TempData/GetTempData", response)); + response = await Client.SendAsync(GetRequest("/TempData/GetTempData", response)); // Assert 2 Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -48,7 +41,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal("Foo", body); // Act 3 - response = await Client.SendAsync(GetRequest("TempData/GetTempData", response)); + response = await Client.SendAsync(GetRequest("/TempData/GetTempData", response)); // Assert 3 Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); @@ -65,7 +58,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests var content = new FormUrlEncodedContent(nameValueCollection); // Act - var response = await Client.PostAsync("http://localhost/TempData/DisplayTempData", content); + var response = await Client.PostAsync("/TempData/DisplayTempData", content); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -73,9 +66,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal("Foo", body); } - [ConditionalFact] - // Mono issue - https://github.com/aspnet/External/issues/21 - [FrameworkSkipCondition(RuntimeFrameworks.Mono)] + [Fact] public async Task Redirect_RetainsTempData_EvenIfAccessed() { // Arrange @@ -139,10 +130,8 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal("Foo", body); } - [ConditionalFact] - // Mono issue - https://github.com/aspnet/External/issues/21 - [FrameworkSkipCondition(RuntimeFrameworks.Mono)] - public async Task TempData_ValidTypes_RoundTripProperly() + [Fact] + public async Task ValidTypes_RoundTripProperly() { // Arrange var testGuid = Guid.NewGuid(); @@ -173,17 +162,52 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal($"Foo 10 3 10/10/2010 00:00:00 {testGuid.ToString()}", body); } - private HttpRequestMessage GetRequest(string path, HttpResponseMessage response) + [Fact] + public async Task SetInActionResultExecution_AvailableForNextRequest() + { + // Arrange + var nameValueCollection = new List> + { + new KeyValuePair("Name", "Jordan"), + }; + var content = new FormUrlEncodedContent(nameValueCollection); + + // Act 1 + var response = await Client.GetAsync("/TempData/SetTempDataInActionResult"); + + // Assert 1 + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + // Act 2 + response = await Client.SendAsync(GetRequest("/TempData/GetTempDataSetInActionResult", response)); + + // Assert 2 + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + Assert.Equal("Michael", body); + + // Act 3 + response = await Client.SendAsync(GetRequest("/TempData/GetTempDataSetInActionResult", response)); + + // Assert 3 + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + } + + public HttpRequestMessage GetRequest(string path, HttpResponseMessage response) { var request = new HttpRequestMessage(HttpMethod.Get, path); IEnumerable values; if (response.Headers.TryGetValues("Set-Cookie", out values)) { - var cookie = SetCookieHeaderValue.ParseList(values.ToList()).First(); - request.Headers.Add("Cookie", new CookieHeaderValue(cookie.Name, cookie.Value).ToString()); + foreach (var cookie in SetCookieHeaderValue.ParseList(values.ToList())) + { + if (cookie.Expires == null || cookie.Expires >= DateTimeOffset.UtcNow) + { + request.Headers.Add("Cookie", new CookieHeaderValue(cookie.Name, cookie.Value).ToString()); + } + } } - return request; } } -} \ No newline at end of file +} diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/SaveTempDataFilterTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/SaveTempDataFilterTest.cs index 78ee33d72e..00f7fd061d 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/SaveTempDataFilterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/SaveTempDataFilterTest.cs @@ -1,7 +1,10 @@ // 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.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Routing; @@ -12,85 +15,335 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal { public class SaveTempDataFilterTest { - [Fact] - public void SaveTempDataFilter_OnResourceExecuted_SavesTempData() + public static TheoryData ActionResultsData { - // Arrange - var tempData = new Mock(MockBehavior.Strict); - tempData - .Setup(m => m.Save()) - .Verifiable(); - - var tempDataFactory = new Mock(MockBehavior.Strict); - tempDataFactory - .Setup(f => f.GetTempData(It.IsAny())) - .Returns(tempData.Object); - - var filter = new SaveTempDataFilter(tempDataFactory.Object); - - var context = new ResourceExecutedContext( - new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()), - new IFilterMetadata[] { }); - - // Act - filter.OnResourceExecuted(context); - - // Assert - tempData.Verify(); + get + { + return new TheoryData() + { + new TestActionResult(), + new TestKeepTempDataActionResult() + }; + } } [Fact] - public void SaveTempDataFilter_OnResultExecuted_KeepsTempData_ForIKeepTempDataResult() + public void OnResultExecuting_RegistersOnStartingCallback() { // Arrange - var tempData = new Mock(MockBehavior.Strict); - tempData - .Setup(m => m.Keep()) + var responseFeature = new Mock(MockBehavior.Strict); + responseFeature + .Setup(rf => rf.OnStarting(It.IsAny>(), It.IsAny())) .Verifiable(); - var tempDataFactory = new Mock(MockBehavior.Strict); tempDataFactory .Setup(f => f.GetTempData(It.IsAny())) - .Returns(tempData.Object); - + .Verifiable(); var filter = new SaveTempDataFilter(tempDataFactory.Object); + var httpContext = GetHttpContext(responseFeature.Object); + var context = GetResultExecutingContext(httpContext); - var context = new ResultExecutedContext( - new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()), - new IFilterMetadata[] { }, - new Mock().Object, - new object()); + // Act + filter.OnResultExecuting(context); + + // Assert + responseFeature.Verify(); + tempDataFactory.Verify(tdf => tdf.GetTempData(It.IsAny()), Times.Never()); + } + + [Fact] + public async Task OnResultExecuting_DoesNotSaveTempData_WhenTempDataAlreadySaved() + { + // Arrange + var responseFeature = new TestResponseFeature(); + var httpContext = GetHttpContext(responseFeature); + httpContext.Items[SaveTempDataFilter.TempDataSavedKey] = true; // indicate that tempdata was already saved + var tempDataFactory = new Mock(MockBehavior.Strict); + tempDataFactory + .Setup(f => f.GetTempData(It.IsAny())) + .Verifiable(); + var filter = new SaveTempDataFilter(tempDataFactory.Object); + var context = GetResultExecutingContext(httpContext); + filter.OnResultExecuting(context); // registers callback + + // Act + await responseFeature.FireOnSendingHeadersAsync(); + + // Assert + tempDataFactory.Verify(tdf => tdf.GetTempData(It.IsAny()), Times.Never()); + } + + [Theory] + [MemberData(nameof(ActionResultsData))] + public async Task OnResultExecuting_SavesTempData_WhenTempData_NotSavedAlready(IActionResult result) + { + // Arrange + var tempDataDictionary = GetTempDataDictionary(); + var filter = GetFilter(tempDataDictionary.Object); + var responseFeature = new TestResponseFeature(); + var actionContext = GetActionContext(GetHttpContext(responseFeature)); + var context = GetResultExecutingContext(actionContext, result); + filter.OnResultExecuting(context); // registers callback + + // Act + await responseFeature.FireOnSendingHeadersAsync(); + + // Assert + tempDataDictionary.Verify(tdd => tdd.Save(), Times.Once()); + } + + [Fact] + public async Task OnResultExecuting_KeepsTempData_ForIKeepTempDataResult() + { + // Arrange + var tempDataDictionary = GetTempDataDictionary(); + var filter = GetFilter(tempDataDictionary.Object); + var responseFeature = new TestResponseFeature(); + var actionContext = GetActionContext(GetHttpContext(responseFeature)); + var context = GetResultExecutingContext(actionContext, new TestKeepTempDataActionResult()); + filter.OnResultExecuting(context); // registers callback + + // Act + await responseFeature.FireOnSendingHeadersAsync(); + + // Assert + tempDataDictionary.Verify(tdf => tdf.Keep(), Times.Once()); + tempDataDictionary.Verify(tdf => tdf.Save(), Times.Once()); + } + + [Fact] + public async Task OnResultExecuting_DoesNotKeepTempData_ForNonIKeepTempDataResult() + { + // Arrange + var tempDataDictionary = GetTempDataDictionary(); + var filter = GetFilter(tempDataDictionary.Object); + var responseFeature = new TestResponseFeature(); + var actionContext = GetActionContext(GetHttpContext(responseFeature)); + var context = GetResultExecutingContext(actionContext, new TestActionResult()); + filter.OnResultExecuting(context); // registers callback + + // Act + await responseFeature.FireOnSendingHeadersAsync(); + + // Assert + tempDataDictionary.Verify(tdf => tdf.Keep(), Times.Never()); + tempDataDictionary.Verify(tdf => tdf.Save(), Times.Once()); + } + + [Fact] + public void OnResultExecuted_DoesNotSaveTempData_WhenResponseHas_AlreadyStarted() + { + // Arrange + var tempDataFactory = new Mock(MockBehavior.Strict); + tempDataFactory + .Setup(f => f.GetTempData(It.IsAny())) + .Verifiable(); + var filter = new SaveTempDataFilter(tempDataFactory.Object); + var httpContext = GetHttpContext(new TestResponseFeature(hasStarted: true)); + var context = GetResultExecutedContext(httpContext); // Act filter.OnResultExecuted(context); // Assert - tempData.Verify(); + tempDataFactory.Verify(tdf => tdf.GetTempData(It.IsAny()), Times.Never()); + } + + [Theory] + [MemberData(nameof(ActionResultsData))] + public void OnResultExecuted_SavesTempData_WhenResponseHas_NotStarted(IActionResult result) + { + // Arrange + var tempDataDictionary = GetTempDataDictionary(); + var filter = GetFilter(tempDataDictionary.Object); + var context = GetResultExecutedContext(actionResult: result); + + // Act + filter.OnResultExecuted(context); + + // Assert + tempDataDictionary.Verify(tdf => tdf.Save(), Times.Once()); } [Fact] - public void SaveTempDataFilter_OnResultExecuted_DoesNotKeepTempData_ForNonIKeepTempDataResult() + public void OnResultExecuted_KeepsTempData_ForIKeepTempDataResult() { // Arrange - var tempData = new Mock(MockBehavior.Strict); + var tempDataDictionary = GetTempDataDictionary(); + var filter = GetFilter(tempDataDictionary.Object); + var context = GetResultExecutedContext(actionResult: new TestKeepTempDataActionResult()); + // Act + filter.OnResultExecuted(context); + + // Assert + tempDataDictionary.Verify(tdf => tdf.Keep(), Times.Once()); + tempDataDictionary.Verify(tdf => tdf.Save(), Times.Once()); + } + + [Fact] + public void OnResultExecuted_DoesNotKeepTempData_ForNonIKeepTempDataResult() + { + // Arrange + var tempDataDictionary = GetTempDataDictionary(); + var filter = GetFilter(tempDataDictionary.Object); + var context = GetResultExecutedContext(actionResult: new TestActionResult()); + + // Act + filter.OnResultExecuted(context); + + // Assert + tempDataDictionary.Verify(tdf => tdf.Keep(), Times.Never()); + tempDataDictionary.Verify(tdf => tdf.Save(), Times.Once()); + } + + private SaveTempDataFilter GetFilter(ITempDataDictionary tempDataDictionary) + { + var tempDataFactory = GetTempDataDictionaryFactory(tempDataDictionary); + return new SaveTempDataFilter(tempDataFactory.Object); + } + + private Mock GetTempDataDictionaryFactory(ITempDataDictionary tempDataDictionary) + { var tempDataFactory = new Mock(MockBehavior.Strict); tempDataFactory .Setup(f => f.GetTempData(It.IsAny())) - .Returns(tempData.Object); + .Returns(tempDataDictionary); + return tempDataFactory; + } - var filter = new SaveTempDataFilter(tempDataFactory.Object); + private Mock GetTempDataDictionary() + { + var tempDataDictionary = new Mock(MockBehavior.Strict); + tempDataDictionary + .Setup(tdd => tdd.Keep()) + .Verifiable(); + tempDataDictionary + .Setup(tdd => tdd.Save()) + .Verifiable(); + return tempDataDictionary; + } - var context = new ResultExecutedContext( - new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()), + private ResultExecutedContext GetResultExecutedContext(HttpContext httpContext = null, IActionResult actionResult = null) + { + if (httpContext == null) + { + httpContext = GetHttpContext(); + } + if (actionResult == null) + { + actionResult = new TestActionResult(); + } + return new ResultExecutedContext( + new ActionContext(httpContext, new RouteData(), new ActionDescriptor()), + new IFilterMetadata[] { }, + actionResult, + new TestController()); + } + + private ResultExecutingContext GetResultExecutingContext(ActionContext actionContext, IActionResult actionResult = null) + { + if (actionResult == null) + { + actionResult = new TestActionResult(); + } + return new ResultExecutingContext( + actionContext, + new IFilterMetadata[] { }, + actionResult, + new TestController()); + } + + private ResultExecutingContext GetResultExecutingContext(HttpContext httpContext = null, IActionResult actionResult = null) + { + if (httpContext == null) + { + httpContext = new DefaultHttpContext(); + } + if (actionResult == null) + { + actionResult = new TestActionResult(); + } + return new ResultExecutingContext( + new ActionContext(httpContext, new RouteData(), new ActionDescriptor()), new IFilterMetadata[] { }, new Mock().Object, - new object()); + new TestController()); + } - // Act - filter.OnResultExecuted(context); + private HttpContext GetHttpContext(IHttpResponseFeature responseFeature = null) + { + if (responseFeature == null) + { + responseFeature = new TestResponseFeature(); + } + var httpContext = new DefaultHttpContext(); + httpContext.Features.Set(responseFeature); + return httpContext; + } - // Assert - The mock will throw if we do the wrong thing. + private ActionContext GetActionContext(HttpContext httpContext = null) + { + if (httpContext == null) + { + httpContext = GetHttpContext(); + } + return new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + } + + private class TestController : Controller + { + } + + private class TestActionResult : IActionResult + { + public Task ExecuteResultAsync(ActionContext context) + { + return context.HttpContext.Response.WriteAsync($"Hello from {nameof(TestActionResult)}"); + } + } + + private class TestKeepTempDataActionResult : IActionResult, IKeepTempDataResult + { + public Task ExecuteResultAsync(ActionContext context) + { + return context.HttpContext.Response.WriteAsync($"Hello from {nameof(TestKeepTempDataActionResult)}"); + } + } + + private class TestResponseFeature : HttpResponseFeature + { + private bool _hasStarted; + private Func _responseStartingAsync = () => Task.FromResult(true); + + public TestResponseFeature(bool hasStarted = false) + { + _hasStarted = hasStarted; + } + + public override bool HasStarted + { + get + { + return _hasStarted; + } + } + + public override void OnStarting(Func callback, object state) + { + var prior = _responseStartingAsync; + _responseStartingAsync = async () => + { + await callback(state); + await prior(); + }; + } + + public async Task FireOnSendingHeadersAsync() + { + await _responseStartingAsync(); + _hasStarted = true; + } } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/TempDataSerializerTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/TempDataSerializerTest.cs new file mode 100644 index 0000000000..9db91f81bb --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/TempDataSerializerTest.cs @@ -0,0 +1,146 @@ +// 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.Internal +{ + public class TempDataSerializerTest + { + public static TheoryData InvalidTypes + { + get + { + return new TheoryData + { + { new object(), typeof(object) }, + { new object[3], typeof(object) }, + { new TestItem(), typeof(TestItem) }, + { new List(), typeof(TestItem) }, + { new Dictionary(), typeof(TestItem) }, + }; + } + } + + [Theory] + [MemberData(nameof(InvalidTypes))] + public void EnsureObjectCanBeSerialized_ThrowsException_OnInvalidType(object value, Type type) + { + // Arrange + var testProvider = new TempDataSerializer(); + + // Act & Assert + var exception = Assert.Throws(() => + { + testProvider.EnsureObjectCanBeSerialized(value); + }); + Assert.Equal($"The '{typeof(TempDataSerializer).FullName}' cannot serialize " + + $"an object of type '{type}'.", + exception.Message); + } + + public static TheoryData InvalidDictionaryTypes + { + get + { + return new TheoryData + { + { new Dictionary(), typeof(int) }, + { new Dictionary(), typeof(Uri) }, + { new Dictionary(), typeof(object) }, + { new Dictionary(), typeof(TestItem) } + }; + } + } + + [Theory] + [MemberData(nameof(InvalidDictionaryTypes))] + public void EnsureObjectCanBeSerialized_ThrowsException_OnInvalidDictionaryType(object value, Type type) + { + // Arrange + var testProvider = new TempDataSerializer(); + + // Act & Assert + var exception = Assert.Throws(() => + { + testProvider.EnsureObjectCanBeSerialized(value); + }); + Assert.Equal($"The '{typeof(TempDataSerializer).FullName}' cannot serialize a dictionary " + + $"with a key of type '{type}'.", + exception.Message); + } + + public static TheoryData ValidTypes + { + get + { + return new TheoryData + { + { 10 }, + { new int[]{ 10, 20 } }, + { "FooValue" }, + { new Uri("http://Foo") }, + { Guid.NewGuid() }, + { new List { "foo", "bar" } }, + { new DateTimeOffset() }, + { 100.1m }, + { new Dictionary() }, + { new Uri[] { new Uri("http://Foo"), new Uri("http://Bar") } }, + { DayOfWeek.Sunday }, + }; + } + } + + [Theory] + [MemberData(nameof(ValidTypes))] + public void EnsureObjectCanBeSerialized_DoesNotThrow_OnValidType(object value) + { + // Arrange + var testProvider = new TempDataSerializer(); + + // Act & Assert (Does not throw) + testProvider.EnsureObjectCanBeSerialized(value); + } + + [Fact] + public void DeserializeTempData_ReturnsEmptyDictionary_DataIsEmpty() + { + // Arrange + var serializer = new TempDataSerializer(); + + // 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 TempDataSerializer(); + 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/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/CookieTempDataProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/CookieTempDataProviderTest.cs new file mode 100644 index 0000000000..819879e264 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/CookieTempDataProviderTest.cs @@ -0,0 +1,596 @@ +// 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; +using System.Collections.Generic; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures +{ + public class CookieTempDataProviderTest + { + [Fact] + public void LoadTempData_ReturnsEmptyDictionary_WhenNoCookieDataIsAvailable() + { + // Arrange + var tempDataProvider = GetProvider(); + + // Act + var tempDataDictionary = tempDataProvider.LoadTempData(new DefaultHttpContext()); + + // Assert + Assert.NotNull(tempDataDictionary); + Assert.Empty(tempDataDictionary); + } + + [Fact] + public void LoadTempData_Base64DecodesAnd_UnprotectsData_FromCookie() + { + // Arrange + var expectedValues = new Dictionary(); + expectedValues.Add("int", 10); + var tempDataProviderSerializer = new TempDataSerializer(); + var expectedDataToUnprotect = tempDataProviderSerializer.Serialize(expectedValues); + var base64EncodedDataInCookie = Convert.ToBase64String(expectedDataToUnprotect); + var dataProtector = new PassThroughDataProtector(); + var tempDataProvider = GetProvider(dataProtector); + var requestCookies = new RequestCookieCollection(new Dictionary() + { + { CookieTempDataProvider.CookieName, base64EncodedDataInCookie } + }); + var httpContext = new Mock(); + httpContext + .Setup(hc => hc.Request.Cookies) + .Returns(requestCookies); + + // Act + var actualValues = tempDataProvider.LoadTempData(httpContext.Object); + + // Assert + Assert.Equal(expectedDataToUnprotect, dataProtector.DataToUnprotect); + Assert.Equal(expectedValues, actualValues); + } + + [Fact] + public void SaveTempData_ProtectsAnd_Base64EncodesDataAnd_SetsCookie() + { + // Arrange + var values = new Dictionary(); + values.Add("int", 10); + var tempDataProviderStore = new TempDataSerializer(); + var expectedDataToProtect = tempDataProviderStore.Serialize(values); + var expectedDataInCookie = Convert.ToBase64String(expectedDataToProtect); + var dataProtector = new PassThroughDataProtector(); + var tempDataProvider = GetProvider(dataProtector); + var responseCookies = new MockResponseCookieCollection(); + var httpContext = new Mock(); + httpContext + .SetupGet(hc => hc.Request.PathBase) + .Returns("/"); + httpContext + .Setup(hc => hc.Response.Cookies) + .Returns(responseCookies); + + // Act + tempDataProvider.SaveTempData(httpContext.Object, values); + + // Assert + Assert.Equal(1, responseCookies.Count); + var cookieInfo = responseCookies[CookieTempDataProvider.CookieName]; + Assert.NotNull(cookieInfo); + Assert.Equal(expectedDataInCookie, cookieInfo.Value); + Assert.Equal(expectedDataToProtect, dataProtector.PlainTextToProtect); + } + + [Theory] + [InlineData("/")] + [InlineData("/vdir1")] + public void SaveTempData_DefaultProviderOptions_SetsCookie_WithAppropriateCookieOptions(string pathBase) + { + // Arrange + var values = new Dictionary(); + values.Add("int", 10); + var tempDataProviderStore = new TempDataSerializer(); + var expectedDataToProtect = tempDataProviderStore.Serialize(values); + var expectedDataInCookie = Convert.ToBase64String(expectedDataToProtect); + var dataProtector = new PassThroughDataProtector(); + var tempDataProvider = GetProvider(dataProtector); + var responseCookies = new MockResponseCookieCollection(); + var httpContext = new Mock(); + httpContext + .SetupGet(hc => hc.Request.PathBase) + .Returns(pathBase); + httpContext + .Setup(hc => hc.Response.Cookies) + .Returns(responseCookies); + + // Act + tempDataProvider.SaveTempData(httpContext.Object, values); + + // Assert + Assert.Equal(1, responseCookies.Count); + var cookieInfo = responseCookies[CookieTempDataProvider.CookieName]; + Assert.NotNull(cookieInfo); + Assert.Equal(expectedDataInCookie, cookieInfo.Value); + Assert.Equal(expectedDataToProtect, dataProtector.PlainTextToProtect); + Assert.Equal(pathBase, cookieInfo.Options.Path); + Assert.True(cookieInfo.Options.Secure); + Assert.True(cookieInfo.Options.HttpOnly); + Assert.Null(cookieInfo.Options.Expires); + Assert.Null(cookieInfo.Options.Domain); + } + + [Theory] + [InlineData("/", null, null, "/", null)] + [InlineData("/", "/vdir1", null, "/vdir1", null)] + [InlineData("/", "/vdir1", ".abc.com", "/vdir1", ".abc.com")] + [InlineData("/vdir1", "/", ".abc.com", "/", ".abc.com")] + public void SaveTempData_CustomProviderOptions_SetsCookie_WithAppropriateCookieOptions( + string requestPathBase, string optionsPath, string optionsDomain, string expectedCookiePath, string expectedDomain) + { + // Arrange + var values = new Dictionary(); + values.Add("int", 10); + var tempDataProviderStore = new TempDataSerializer(); + var expectedDataToProtect = tempDataProviderStore.Serialize(values); + var expectedDataInCookie = Convert.ToBase64String(expectedDataToProtect); + var dataProtector = new PassThroughDataProtector(); + var tempDataProvider = GetProvider( + dataProtector, + new CookieTempDataProviderOptions() { Path = optionsPath, Domain = optionsDomain }); + var responseCookies = new MockResponseCookieCollection(); + var httpContext = new Mock(); + httpContext + .SetupGet(hc => hc.Request.PathBase) + .Returns(requestPathBase); + httpContext + .Setup(hc => hc.Response.Cookies) + .Returns(responseCookies); + + // Act + tempDataProvider.SaveTempData(httpContext.Object, values); + + // Assert + Assert.Equal(1, responseCookies.Count); + var cookieInfo = responseCookies[CookieTempDataProvider.CookieName]; + Assert.NotNull(cookieInfo); + Assert.Equal(expectedDataInCookie, cookieInfo.Value); + Assert.Equal(expectedDataToProtect, dataProtector.PlainTextToProtect); + Assert.Equal(expectedCookiePath, cookieInfo.Options.Path); + Assert.Equal(expectedDomain, cookieInfo.Options.Domain); + Assert.True(cookieInfo.Options.Secure); + Assert.True(cookieInfo.Options.HttpOnly); + Assert.Null(cookieInfo.Options.Expires); + } + + [Fact] + public void SaveTempData_RemovesCookie_WhenNoDataToSave() + { + // Arrange + var values = new Dictionary(); + values.Add("int", 10); + var tempDataProviderStore = new TempDataSerializer(); + var serializedData = tempDataProviderStore.Serialize(values); + var base64EncodedData = Convert.ToBase64String(serializedData); + var dataProtector = new PassThroughDataProtector(); + var tempDataProvider = GetProvider(dataProtector); + var requestCookies = new RequestCookieCollection(new Dictionary() + { + { CookieTempDataProvider.CookieName, base64EncodedData } + }); + var responseCookies = new MockResponseCookieCollection(); + var httpContext = new Mock(); + httpContext + .SetupGet(hc => hc.Request.PathBase) + .Returns("/"); + httpContext + .Setup(hc => hc.Request.Cookies) + .Returns(requestCookies); + httpContext + .Setup(hc => hc.Response.Cookies) + .Returns(responseCookies); + httpContext + .Setup(hc => hc.Response.Headers) + .Returns(new HeaderDictionary()); + + // Act + tempDataProvider.SaveTempData(httpContext.Object, new Dictionary()); + + // Assert + Assert.Equal(1, responseCookies.Count); + var cookie = responseCookies[CookieTempDataProvider.CookieName]; + Assert.NotNull(cookie); + Assert.Equal(string.Empty, cookie.Value); + Assert.NotNull(cookie.Options.Expires); + Assert.True(cookie.Options.Expires.Value < DateTimeOffset.Now); // expired cookie + } + + [Fact] + public void SaveAndLoad_StringCanBeStoredAndLoaded() + { + // Arrange + var testProvider = GetProvider(); + var input = new Dictionary + { + { "string", "value" } + }; + var context = GetHttpContext(); + + // Act + testProvider.SaveTempData(context, input); + UpdateRequestWithCookies(context); + var TempData = testProvider.LoadTempData(context); + + // Assert + var stringVal = Assert.IsType(TempData["string"]); + Assert.Equal("value", stringVal); + } + + [Theory] + [InlineData(10)] + [InlineData(int.MaxValue)] + [InlineData(int.MinValue)] + public void SaveAndLoad_IntCanBeStoredAndLoaded(int expected) + { + // Arrange + var testProvider = GetProvider(); + var input = new Dictionary + { + { "int", expected } + }; + var context = GetHttpContext(); + + // Act + testProvider.SaveTempData(context, input); + UpdateRequestWithCookies(context); + var TempData = testProvider.LoadTempData(context); + + // Assert + var intVal = Assert.IsType(TempData["int"]); + Assert.Equal(expected, intVal); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void SaveAndLoad_BoolCanBeStoredAndLoaded(bool value) + { + // Arrange + var testProvider = GetProvider(); + var input = new Dictionary + { + { "bool", value } + }; + var context = GetHttpContext(); + + // Act + testProvider.SaveTempData(context, input); + UpdateRequestWithCookies(context); + var TempData = testProvider.LoadTempData(context); + + // Assert + var boolVal = Assert.IsType(TempData["bool"]); + Assert.Equal(value, boolVal); + } + + [Fact] + public void SaveAndLoad_DateTimeCanBeStoredAndLoaded() + { + // Arrange + var testProvider = GetProvider(); + var inputDatetime = new DateTime(2010, 12, 12, 1, 2, 3, DateTimeKind.Local); + var input = new Dictionary + { + { "DateTime", inputDatetime } + }; + var context = GetHttpContext(); + + // Act + testProvider.SaveTempData(context, input); + UpdateRequestWithCookies(context); + var TempData = testProvider.LoadTempData(context); + + // Assert + var datetime = Assert.IsType(TempData["DateTime"]); + Assert.Equal(inputDatetime, datetime); + } + + [Fact] + public void SaveAndLoad_GuidCanBeStoredAndLoaded() + { + // Arrange + var testProvider = GetProvider(); + var inputGuid = Guid.NewGuid(); + var input = new Dictionary + { + { "Guid", inputGuid } + }; + var context = GetHttpContext(); + + // Act + testProvider.SaveTempData(context, input); + UpdateRequestWithCookies(context); + var TempData = testProvider.LoadTempData(context); + + // Assert + var guidVal = Assert.IsType(TempData["Guid"]); + Assert.Equal(inputGuid, guidVal); + } + + [Fact] + public void SaveAndLoad_EnumCanBeSavedAndLoaded() + { + // Arrange + var key = "EnumValue"; + var testProvider = GetProvider(); + var expected = DayOfWeek.Friday; + var input = new Dictionary + { + { key, expected } + }; + var context = GetHttpContext(); + + // Act + testProvider.SaveTempData(context, input); + UpdateRequestWithCookies(context); + var TempData = testProvider.LoadTempData(context); + var result = TempData[key]; + + // Assert + var actual = (DayOfWeek)result; + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(3100000000L)] + [InlineData(-3100000000L)] + public void SaveAndLoad_LongCanBeSavedAndLoaded(long expected) + { + // Arrange + var key = "LongValue"; + var testProvider = GetProvider(); + var input = new Dictionary + { + { key, expected } + }; + var context = GetHttpContext(); + + // Act + testProvider.SaveTempData(context, input); + UpdateRequestWithCookies(context); + var TempData = testProvider.LoadTempData(context); + var result = TempData[key]; + + // Assert + var actual = Assert.IsType(result); + Assert.Equal(expected, actual); + } + + [Fact] + public void SaveAndLoad_ListCanBeStoredAndLoaded() + { + // Arrange + var testProvider = GetProvider(); + var input = new Dictionary + { + { "List`string", new List { "one", "two" } } + }; + var context = GetHttpContext(); + + // Act + testProvider.SaveTempData(context, input); + UpdateRequestWithCookies(context); + var TempData = testProvider.LoadTempData(context); + + // Assert + var list = (IList)TempData["List`string"]; + Assert.Equal(2, list.Count); + Assert.Equal("one", list[0]); + Assert.Equal("two", list[1]); + } + + [Fact] + public void SaveAndLoad_DictionaryCanBeStoredAndLoaded() + { + // Arrange + var testProvider = GetProvider(); + var inputDictionary = new Dictionary + { + { "Hello", "World" }, + }; + var input = new Dictionary + { + { "Dictionary", inputDictionary } + }; + var context = GetHttpContext(); + + // Act + testProvider.SaveTempData(context, input); + UpdateRequestWithCookies(context); + var TempData = testProvider.LoadTempData(context); + + // Assert + var dictionary = Assert.IsType>(TempData["Dictionary"]); + Assert.Equal("World", dictionary["Hello"]); + } + + [Fact] + public void SaveAndLoad_EmptyDictionary_RoundTripsAsNull() + { + // Arrange + var testProvider = GetProvider(); + var input = new Dictionary + { + { "EmptyDictionary", new Dictionary() } + }; + var context = GetHttpContext(); + + // Act + testProvider.SaveTempData(context, input); + UpdateRequestWithCookies(context); + var TempData = testProvider.LoadTempData(context); + + // Assert + var emptyDictionary = (IDictionary)TempData["EmptyDictionary"]; + Assert.Null(emptyDictionary); + } + + private static HttpContext GetHttpContext() + { + var context = new Mock(); + context + .SetupGet(hc => hc.Request.PathBase) + .Returns("/"); + context + .SetupGet(hc => hc.Response.Cookies) + .Returns(new MockResponseCookieCollection()); + return context.Object; + } + + private void UpdateRequestWithCookies(HttpContext httpContext) + { + var responseCookies = (MockResponseCookieCollection)httpContext.Response.Cookies; + + var values = new Dictionary(); + + foreach (var responseCookie in responseCookies) + { + values.Add(responseCookie.Key, responseCookie.Value); + } + + if (values.Count > 0) + { + httpContext.Request.Cookies = new RequestCookieCollection(values); + } + } + + private class MockResponseCookieCollection : IResponseCookies, IEnumerable + { + private Dictionary _cookies = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public int Count + { + get + { + return _cookies.Count; + } + } + + public CookieInfo this[string key] + { + get + { + return _cookies[key]; + } + } + + public void Append(string key, string value, CookieOptions options) + { + _cookies[key] = new CookieInfo() + { + Key = key, + Value = value, + Options = options + }; + } + + public void Append(string key, string value) + { + Append(key, value, new CookieOptions()); + } + + public void Delete(string key, CookieOptions options) + { + _cookies.Remove(key); + } + + public void Delete(string key) + { + _cookies.Remove(key); + } + + public IEnumerator GetEnumerator() + { + return _cookies.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + private CookieTempDataProvider GetProvider(IDataProtector dataProtector = null, CookieTempDataProviderOptions options = null) + { + if(dataProtector == null) + { + dataProtector = new PassThroughDataProtector(); + } + if(options == null) + { + options = new CookieTempDataProviderOptions(); + } + + var testOptions = new Mock>(); + testOptions.SetupGet(o => o.Value).Returns(options); + + return new CookieTempDataProvider(new PassThroughDataProtectionProvider(dataProtector), testOptions.Object); + } + + private class PassThroughDataProtectionProvider : IDataProtectionProvider + { + private readonly IDataProtector _dataProtector; + + public PassThroughDataProtectionProvider(IDataProtector dataProtector) + { + _dataProtector = dataProtector; + } + + public IDataProtector CreateProtector(string purpose) + { + return _dataProtector; + } + } + + private class PassThroughDataProtector : IDataProtector + { + public byte[] DataToUnprotect { get; private set; } + public byte[] PlainTextToProtect { get; private set; } + public string Purpose { get; private set; } + + public IDataProtector CreateProtector(string purpose) + { + Purpose = purpose; + return this; + } + + public byte[] Protect(byte[] plaintext) + { + PlainTextToProtect = plaintext; + return PlainTextToProtect; + } + + public byte[] Unprotect(byte[] protectedData) + { + DataToUnprotect = protectedData; + return DataToUnprotect; + } + } + + private class CookieInfo + { + public string Key { get; set; } + + public string Value { get; set; } + + public CookieOptions Options { get; set; } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/SessionStateTempDataProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/SessionStateTempDataProviderTest.cs index ba8f46a4d6..1d3887ca32 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/SessionStateTempDataProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/SessionStateTempDataProviderTest.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Xunit; namespace Microsoft.AspNetCore.Mvc.ViewFeatures @@ -54,116 +55,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures Assert.Empty(tempDataDictionary); } - [Fact] - public void Load_ReturnsEmptyDictionary_WhenSessionDataIsEmpty() - { - // Arrange - var testProvider = new SessionStateTempDataProvider(); - var httpContext = GetHttpContext(); - httpContext.Session.Set(SessionStateTempDataProvider.TempDataSessionStateKey, new byte[] { }); - - // Act - var tempDataDictionary = testProvider.LoadTempData(httpContext); - - // Assert - Assert.Empty(tempDataDictionary); - } - - public static TheoryData InvalidTypes - { - get - { - return new TheoryData - { - { new object(), typeof(object) }, - { new object[3], typeof(object) }, - { new TestItem(), typeof(TestItem) }, - { new List(), typeof(TestItem) }, - { new Dictionary(), typeof(TestItem) }, - }; - } - } - - [Theory] - [MemberData(nameof(InvalidTypes))] - public void EnsureObjectCanBeSerialized_ThrowsException_OnInvalidType(object value, Type type) - { - // Arrange - var testProvider = new SessionStateTempDataProvider(); - - // Act & Assert - var exception = Assert.Throws(() => - { - testProvider.EnsureObjectCanBeSerialized(value); - }); - Assert.Equal($"The '{typeof(SessionStateTempDataProvider).FullName}' cannot serialize " + - $"an object of type '{type}' to session state.", - exception.Message); - } - - public static TheoryData InvalidDictionaryTypes - { - get - { - return new TheoryData - { - { new Dictionary(), typeof(int) }, - { new Dictionary(), typeof(Uri) }, - { new Dictionary(), typeof(object) }, - { new Dictionary(), typeof(TestItem) } - }; - } - } - - [Theory] - [MemberData(nameof(InvalidDictionaryTypes))] - public void EnsureObjectCanBeSerialized_ThrowsException_OnInvalidDictionaryType(object value, Type type) - { - // Arrange - var testProvider = new SessionStateTempDataProvider(); - - // Act & Assert - var exception = Assert.Throws(() => - { - testProvider.EnsureObjectCanBeSerialized(value); - }); - Assert.Equal($"The '{typeof(SessionStateTempDataProvider).FullName}' cannot serialize a dictionary " + - $"with a key of type '{type}' to session state.", - exception.Message); - } - - public static TheoryData ValidTypes - { - get - { - return new TheoryData - { - { 10 }, - { new int[]{ 10, 20 } }, - { "FooValue" }, - { new Uri("http://Foo") }, - { Guid.NewGuid() }, - { new List { "foo", "bar" } }, - { new DateTimeOffset() }, - { 100.1m }, - { new Dictionary() }, - { new Uri[] { new Uri("http://Foo"), new Uri("http://Bar") } }, - { DayOfWeek.Sunday }, - }; - } - } - - [Theory] - [MemberData(nameof(ValidTypes))] - public void EnsureObjectCanBeSerialized_DoesNotThrow_OnValidType(object value) - { - // Arrange - var testProvider = new SessionStateTempDataProvider(); - - // Act & Assert (Does not throw) - testProvider.EnsureObjectCanBeSerialized(value); - } - [Fact] public void SaveAndLoad_StringCanBeStoredAndLoaded() { @@ -384,26 +275,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures Assert.Null(emptyDictionary); } - [Fact] - public void SaveAndLoad_NullValue_RoundTripsSuccessfully() - { - // Arrange - var testProvider = new SessionStateTempDataProvider(); - var input = new Dictionary - { - { "NullKey", null } - }; - var context = GetHttpContext(); - - // Act - testProvider.SaveTempData(context, input); - var TempData = testProvider.LoadTempData(context); - - // Assert - Assert.True(TempData.ContainsKey("NullKey")); - Assert.Null(TempData["NullKey"]); - } - private class TestItem { public int DummyInt { get; set; } diff --git a/test/WebSites/BasicWebSite/Controllers/TempDataController.cs b/test/WebSites/BasicWebSite/Controllers/TempDataController.cs index 79acc4e752..b1383b00d8 100644 --- a/test/WebSites/BasicWebSite/Controllers/TempDataController.cs +++ b/test/WebSites/BasicWebSite/Controllers/TempDataController.cs @@ -68,5 +68,30 @@ namespace BasicWebSite.Controllers var value5 = (Guid)TempData["key5"]; return $"{value1} {value2.ToString()} {value3.Count.ToString()} {value4.ToString()} {value5.ToString()}"; } + + [HttpGet] + public IActionResult SetTempDataInActionResult() + { + return new StoreIntoTempDataActionResult(); + } + + [HttpGet] + public string GetTempDataSetInActionResult() + { + return TempData["Name"]?.ToString(); + } + + [HttpGet] + public IActionResult SetLargeValueInTempData(int size, char character) + { + TempData["LargeValue"] = new string(character, size); + return Ok(); + } + + [HttpGet] + public string GetLargeValueFromTempData() + { + return TempData["LargeValue"]?.ToString(); + } } } diff --git a/test/WebSites/BasicWebSite/Program.cs b/test/WebSites/BasicWebSite/Program.cs new file mode 100644 index 0000000000..c2a444d9d9 --- /dev/null +++ b/test/WebSites/BasicWebSite/Program.cs @@ -0,0 +1,23 @@ +// 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.IO; +using Microsoft.AspNetCore.Hosting; + +namespace BasicWebSite +{ + public class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseStartup() + .UseKestrel() + .UseIISIntegration() + .Build(); + + host.Run(); + } + } +} diff --git a/test/WebSites/BasicWebSite/Startup.cs b/test/WebSites/BasicWebSite/Startup.cs index b14d3b6ca8..726628bb5e 100644 --- a/test/WebSites/BasicWebSite/Startup.cs +++ b/test/WebSites/BasicWebSite/Startup.cs @@ -53,18 +53,6 @@ namespace BasicWebSite }); } - - public static void Main(string[] args) - { - var host = new WebHostBuilder() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseStartup() - .UseKestrel() - .UseIISIntegration() - .Build(); - - host.Run(); - } } } diff --git a/test/WebSites/BasicWebSite/StartupWithCookieTempDataProvider.cs b/test/WebSites/BasicWebSite/StartupWithCookieTempDataProvider.cs new file mode 100644 index 0000000000..5ecaa675c3 --- /dev/null +++ b/test/WebSites/BasicWebSite/StartupWithCookieTempDataProvider.cs @@ -0,0 +1,25 @@ +// 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.Builder; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.Extensions.DependencyInjection; + +namespace BasicWebSite +{ + public class StartupWithCookieTempDataProvider + { + public void ConfigureServices(IServiceCollection services) + { + services.AddMvc(); + services.AddSingleton(); + } + + public void Configure(IApplicationBuilder app) + { + app.UseCultureReplacer(); + app.UseMvcWithDefaultRoute(); + } + } +} + diff --git a/test/WebSites/BasicWebSite/StoreIntoTempDataActionResult.cs b/test/WebSites/BasicWebSite/StoreIntoTempDataActionResult.cs new file mode 100644 index 0000000000..38c7b3a31d --- /dev/null +++ b/test/WebSites/BasicWebSite/StoreIntoTempDataActionResult.cs @@ -0,0 +1,25 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Mvc.ViewFeatures; + +namespace BasicWebSite +{ + public class StoreIntoTempDataActionResult : IActionResult + { + public Task ExecuteResultAsync(ActionContext context) + { + // store information in temp data + var httpContext = context.HttpContext; + var tempDataDictionaryFactory = httpContext.RequestServices.GetRequiredService(); + var tempDataDictionary = tempDataDictionaryFactory.GetTempData(httpContext); + tempDataDictionary["Name"] = "Michael"; + + return httpContext.Response.WriteAsync($"Hello from {nameof(StoreIntoTempDataActionResult)}"); + } + } +}