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