[Fixes #5212] Added a cookie based ITempDataProvider

This commit is contained in:
Kiran Challa 2016-09-07 11:54:41 -07:00
parent 4cca6b09f0
commit 89bd6dc1cd
21 changed files with 1730 additions and 481 deletions

View File

@ -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
{
/// <summary>
/// Provides programmatic configuration for cookies set by <see cref="CookieTempDataProvider"/>
/// </summary>
public class CookieTempDataProviderOptions
{
/// <summary>
/// The path set for a cookie. If not specified, current request's <see cref="HttpRequest.PathBase"/> value is used.
/// </summary>
public string Path { get; set; }
/// <summary>
/// The domain set on a cookie. Defaults to <c>null</c>.
/// </summary>
public string Domain { get; set; }
}
}

View File

@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // 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.Mvc.Filters;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Internal;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{ {
@ -10,6 +12,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
/// </summary> /// </summary>
public class SaveTempDataFilter : IResourceFilter, IResultFilter public class SaveTempDataFilter : IResourceFilter, IResultFilter
{ {
// Internal for unit testing
internal static readonly object TempDataSavedKey = new object();
private readonly ITempDataDictionaryFactory _factory; private readonly ITempDataDictionaryFactory _factory;
/// <summary> /// <summary>
@ -29,21 +34,68 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
/// <inheritdoc /> /// <inheritdoc />
public void OnResourceExecuted(ResourceExecutedContext context) public void OnResourceExecuted(ResourceExecutedContext context)
{ {
_factory.GetTempData(context.HttpContext).Save();
} }
/// <inheritdoc /> /// <inheritdoc />
public void OnResultExecuting(ResultExecutingContext context) 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
});
} }
/// <inheritdoc /> /// <inheritdoc />
public void OnResultExecuted(ResultExecutedContext context) 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; }
}
} }
} }

View File

@ -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<Type, Func<JArray, object>> _arrayConverters =
new ConcurrentDictionary<Type, Func<JArray, object>>();
private static readonly ConcurrentDictionary<Type, Func<JObject, object>> _dictionaryConverters =
new ConcurrentDictionary<Type, Func<JObject, object>>();
private static readonly Dictionary<JTokenType, Type> _tokenTypeLookup = new Dictionary<JTokenType, Type>
{
{ 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<string, object> Deserialize(byte[] value)
{
Dictionary<string, object> tempDataDictionary = null;
using (var memoryStream = new MemoryStream(value))
using (var writer = new BsonReader(memoryStream))
{
tempDataDictionary = _jsonSerializer.Deserialize<Dictionary<string, object>>(writer);
if (tempDataDictionary == null)
{
return new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
}
}
var convertedDictionary = new Dictionary<string, object>(
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<JArray, object>)_convertArrayMethodInfo
.MakeGenericMethod(type)
.CreateDelegate(typeof(Func<JArray, object>));
});
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<JObject, object>)_convertDictionaryMethodInfo
.MakeGenericMethod(type)
.CreateDelegate(typeof(Func<JObject, object>));
});
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<string, object>(StringComparer.OrdinalIgnoreCase);
}
public byte[] Serialize(IDictionary<string, object> 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<T> 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<TKey, TValue> 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<TVal> ConvertArray<TVal>(JArray array)
{
return array.Values<TVal>().ToArray();
}
private static IDictionary<string, TVal> ConvertDictionary<TVal>(JObject jObject)
{
var convertedDictionary = new Dictionary<string, TVal>(StringComparer.Ordinal);
foreach (var item in jObject)
{
convertedDictionary.Add(item.Key, jObject.Value<TVal>(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));
}
}
}

View File

@ -779,7 +779,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
} }
/// <summary> /// <summary>
/// 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}'.
/// </summary> /// </summary>
internal static string TempData_CannotSerializeDictionary internal static string TempData_CannotSerializeDictionary
{ {
@ -787,7 +787,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
} }
/// <summary> /// <summary>
/// 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}'.
/// </summary> /// </summary>
internal static string FormatTempData_CannotSerializeDictionary(object p0, object p1) internal static string FormatTempData_CannotSerializeDictionary(object p0, object p1)
{ {
@ -795,19 +795,19 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
} }
/// <summary> /// <summary>
/// The '{0}' cannot serialize an object of type '{1}' to session state. /// The '{0}' cannot serialize an object of type '{1}'.
/// </summary> /// </summary>
internal static string TempData_CannotSerializeToSession internal static string TempData_CannotSerializeType
{ {
get { return GetString("TempData_CannotSerializeToSession"); } get { return GetString("TempData_CannotSerializeType"); }
} }
/// <summary> /// <summary>
/// The '{0}' cannot serialize an object of type '{1}' to session state. /// The '{0}' cannot serialize an object of type '{1}'.
/// </summary> /// </summary>
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);
} }
/// <summary> /// <summary>

View File

@ -263,10 +263,10 @@
<value>Cannot deserialize {0} of type '{1}'.</value> <value>Cannot deserialize {0} of type '{1}'.</value>
</data> </data>
<data name="TempData_CannotSerializeDictionary" xml:space="preserve"> <data name="TempData_CannotSerializeDictionary" xml:space="preserve">
<value>The '{0}' cannot serialize a dictionary with a key of type '{1}' to session state.</value> <value>The '{0}' cannot serialize a dictionary with a key of type '{1}'.</value>
</data> </data>
<data name="TempData_CannotSerializeToSession" xml:space="preserve"> <data name="TempData_CannotSerializeType" xml:space="preserve">
<value>The '{0}' cannot serialize an object of type '{1}' to session state.</value> <value>The '{0}' cannot serialize an object of type '{1}'.</value>
</data> </data>
<data name="Dictionary_DuplicateKey" xml:space="preserve"> <data name="Dictionary_DuplicateKey" xml:space="preserve">
<value>The collection already contains an entry with key '{0}'.</value> <value>The collection already contains an entry with key '{0}'.</value>

View File

@ -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
{
/// <summary>
/// Provides data from cookie to the current <see cref="ITempDataDictionary"/> object.
/// </summary>
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<CookieTempDataProviderOptions> _options;
public CookieTempDataProvider(IDataProtectionProvider dataProtectionProvider, IOptions<CookieTempDataProviderOptions> options)
{
_dataProtector = dataProtectionProvider.CreateProtector(Purpose);
_tempDataSerializer = new TempDataSerializer();
_chunkingCookieManager = new ChunkingCookieManager();
_options = options;
}
public IDictionary<string, object> 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<string, object>(StringComparer.OrdinalIgnoreCase);
}
public void SaveTempData(HttpContext context, IDictionary<string, object> 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);
}
}
}
}

View File

@ -14,7 +14,15 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class SaveTempDataAttribute : Attribute, IFilterFactory, IOrderedFilter public class SaveTempDataAttribute : Attribute, IFilterFactory, IOrderedFilter
{ {
/// <inheritdoc /> 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;
}
// <inheritdoc />
public int Order { get; set; } public int Order { get; set; }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -2,18 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.Extensions.Internal;
using Newtonsoft.Json;
using Newtonsoft.Json.Bson;
using Newtonsoft.Json.Linq;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{ {
@ -24,34 +15,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{ {
// Internal for testing // Internal for testing
internal const string TempDataSessionStateKey = "__ControllerTempData"; internal const string TempDataSessionStateKey = "__ControllerTempData";
private readonly TempDataSerializer _tempDataSerializer;
private readonly JsonSerializer _jsonSerializer = JsonSerializer.Create( public SessionStateTempDataProvider()
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<Type, Func<JArray, object>> _arrayConverters =
new ConcurrentDictionary<Type, Func<JArray, object>>();
private static readonly ConcurrentDictionary<Type, Func<JObject, object>> _dictionaryConverters =
new ConcurrentDictionary<Type, Func<JObject, object>>();
private static readonly Dictionary<JTokenType, Type> _tokenTypeLookup = new Dictionary<JTokenType, Type>
{ {
{ JTokenType.String, typeof(string) }, _tempDataSerializer = new TempDataSerializer();
{ 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) },
};
/// <inheritdoc /> /// <inheritdoc />
public virtual IDictionary<string, object> LoadTempData(HttpContext context) public virtual IDictionary<string, object> LoadTempData(HttpContext context)
@ -64,103 +33,16 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
// Accessing Session property will throw if the session middleware is not enabled. // Accessing Session property will throw if the session middleware is not enabled.
var session = context.Session; var session = context.Session;
Dictionary<string, object> tempDataDictionary = null;
byte[] value; byte[] value;
if (session.TryGetValue(TempDataSessionStateKey, out value)) if (session.TryGetValue(TempDataSessionStateKey, out value))
{ {
// If we got it from Session, remove it so that no other request gets it // If we got it from Session, remove it so that no other request gets it
session.Remove(TempDataSessionStateKey); session.Remove(TempDataSessionStateKey);
using (var memoryStream = new MemoryStream(value)) return _tempDataSerializer.Deserialize(value);
using (var writer = new BsonReader(memoryStream))
{
tempDataDictionary = _jsonSerializer.Deserialize<Dictionary<string, object>>(writer);
if (tempDataDictionary == null)
{
return new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
}
}
var convertedDictionary = new Dictionary<string, object>(
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<JArray, object>)_convertArrayMethodInfo
.MakeGenericMethod(type)
.CreateDelegate(typeof(Func<JArray, object>));
});
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<JObject, object>)_convertDictMethodInfo
.MakeGenericMethod(type)
.CreateDelegate(typeof(Func<JObject, object>));
});
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<byte>.Instance);
} }
return tempDataDictionary ?? new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); return new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -177,106 +59,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
var hasValues = (values != null && values.Count > 0); var hasValues = (values != null && values.Count > 0);
if (hasValues) if (hasValues)
{ {
foreach (var item in values.Values) var bytes = _tempDataSerializer.Serialize(values);
{ session.Set(TempDataSessionStateKey, bytes);
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());
}
}
} }
else else
{ {
session.Remove(TempDataSessionStateKey); 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<T> 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<TKey, TValue> 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<TVal> ConvertArray<TVal>(JArray array)
{
return array.Values<TVal>().ToArray();
}
private static IDictionary<string, TVal> ConvertDictionary<TVal>(JObject jObject)
{
var convertedDictionary = new Dictionary<string, TVal>(StringComparer.Ordinal);
foreach (var item in jObject)
{
convertedDictionary.Add(item.Key, jObject.Value<TVal>(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));
}
} }
} }

View File

@ -22,6 +22,10 @@
}, },
"dependencies": { "dependencies": {
"Microsoft.AspNetCore.Antiforgery": "1.1.0-*", "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.Diagnostics.Abstractions": "1.1.0-*",
"Microsoft.AspNetCore.Html.Abstractions": "1.1.0-*", "Microsoft.AspNetCore.Html.Abstractions": "1.1.0-*",
"Microsoft.AspNetCore.Mvc.Core": "1.1.0-*", "Microsoft.AspNetCore.Mvc.Core": "1.1.0-*",
@ -35,6 +39,10 @@
"version": "1.1.0-*", "version": "1.1.0-*",
"type": "build" "type": "build"
}, },
"Microsoft.Extensions.HashCodeCombiner.Sources": {
"version": "1.1.0-*",
"type": "build"
},
"Microsoft.Extensions.PropertyActivator.Sources": { "Microsoft.Extensions.PropertyActivator.Sources": {
"version": "1.1.0-*", "version": "1.1.0-*",
"type": "build" "type": "build"
@ -43,10 +51,6 @@
"version": "1.1.0-*", "version": "1.1.0-*",
"type": "build" "type": "build"
}, },
"Microsoft.Extensions.HashCodeCombiner.Sources": {
"version": "1.1.0-*",
"type": "build"
},
"Microsoft.Extensions.WebEncoders": "1.1.0-*", "Microsoft.Extensions.WebEncoders": "1.1.0-*",
"Newtonsoft.Json": "9.0.1", "Newtonsoft.Json": "9.0.1",
"System.Buffers": "4.0.0-*" "System.Buffers": "4.0.0-*"

View File

@ -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<MvcTestFixture<BasicWebSite.StartupWithCookieTempDataProvider>>
{
private const int DefaultChunkSize = 4070;
public TempDataInCookiesTest(MvcTestFixture<BasicWebSite.StartupWithCookieTempDataProvider> 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);
}
}
}

View File

@ -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<MvcTestFixture<BasicWebSite.Startup>>
{
public TempDataInSessionTest(MvcTestFixture<BasicWebSite.Startup> fixture)
{
Client = fixture.Client;
}
protected override HttpClient Client { get; }
}
}

View File

@ -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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -7,24 +7,17 @@ using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Testing.xunit;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{ {
public class TempDataTest : IClassFixture<MvcTestFixture<BasicWebSite.Startup>> public abstract class TempDataTestBase
{ {
public TempDataTest(MvcTestFixture<BasicWebSite.Startup> fixture) protected abstract HttpClient Client { get; }
{
Client = fixture.Client;
}
public HttpClient Client { get; }
[Fact] [Fact]
public async Task TempData_PersistsJustForNextRequest() public async Task PersistsJustForNextRequest()
{ {
// Arrange // Arrange
var nameValueCollection = new List<KeyValuePair<string, string>> var nameValueCollection = new List<KeyValuePair<string, string>>
@ -40,7 +33,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode);
// Act 2 // Act 2
response = await Client.SendAsync(GetRequest("TempData/GetTempData", response)); response = await Client.SendAsync(GetRequest("/TempData/GetTempData", response));
// Assert 2 // Assert 2
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@ -48,7 +41,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("Foo", body); Assert.Equal("Foo", body);
// Act 3 // Act 3
response = await Client.SendAsync(GetRequest("TempData/GetTempData", response)); response = await Client.SendAsync(GetRequest("/TempData/GetTempData", response));
// Assert 3 // Assert 3
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
@ -65,7 +58,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
var content = new FormUrlEncodedContent(nameValueCollection); var content = new FormUrlEncodedContent(nameValueCollection);
// Act // Act
var response = await Client.PostAsync("http://localhost/TempData/DisplayTempData", content); var response = await Client.PostAsync("/TempData/DisplayTempData", content);
// Assert // Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@ -73,9 +66,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("Foo", body); Assert.Equal("Foo", body);
} }
[ConditionalFact] [Fact]
// Mono issue - https://github.com/aspnet/External/issues/21
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
public async Task Redirect_RetainsTempData_EvenIfAccessed() public async Task Redirect_RetainsTempData_EvenIfAccessed()
{ {
// Arrange // Arrange
@ -139,10 +130,8 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("Foo", body); Assert.Equal("Foo", body);
} }
[ConditionalFact] [Fact]
// Mono issue - https://github.com/aspnet/External/issues/21 public async Task ValidTypes_RoundTripProperly()
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
public async Task TempData_ValidTypes_RoundTripProperly()
{ {
// Arrange // Arrange
var testGuid = Guid.NewGuid(); var testGuid = Guid.NewGuid();
@ -173,16 +162,51 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal($"Foo 10 3 10/10/2010 00:00:00 {testGuid.ToString()}", body); 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<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("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); var request = new HttpRequestMessage(HttpMethod.Get, path);
IEnumerable<string> values; IEnumerable<string> values;
if (response.Headers.TryGetValues("Set-Cookie", out values)) if (response.Headers.TryGetValues("Set-Cookie", out values))
{ {
var cookie = SetCookieHeaderValue.ParseList(values.ToList()).First(); foreach (var cookie in SetCookieHeaderValue.ParseList(values.ToList()))
request.Headers.Add("Cookie", new CookieHeaderValue(cookie.Name, cookie.Value).ToString()); {
if (cookie.Expires == null || cookie.Expires >= DateTimeOffset.UtcNow)
{
request.Headers.Add("Cookie", new CookieHeaderValue(cookie.Name, cookie.Value).ToString());
}
}
} }
return request; return request;
} }
} }

View File

@ -1,7 +1,10 @@
// 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. // 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;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
@ -12,85 +15,335 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{ {
public class SaveTempDataFilterTest public class SaveTempDataFilterTest
{ {
[Fact] public static TheoryData<IActionResult> ActionResultsData
public void SaveTempDataFilter_OnResourceExecuted_SavesTempData()
{ {
// Arrange get
var tempData = new Mock<ITempDataDictionary>(MockBehavior.Strict); {
tempData return new TheoryData<IActionResult>()
.Setup(m => m.Save()) {
.Verifiable(); new TestActionResult(),
new TestKeepTempDataActionResult()
var tempDataFactory = new Mock<ITempDataDictionaryFactory>(MockBehavior.Strict); };
tempDataFactory }
.Setup(f => f.GetTempData(It.IsAny<HttpContext>()))
.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();
} }
[Fact] [Fact]
public void SaveTempDataFilter_OnResultExecuted_KeepsTempData_ForIKeepTempDataResult() public void OnResultExecuting_RegistersOnStartingCallback()
{ {
// Arrange // Arrange
var tempData = new Mock<ITempDataDictionary>(MockBehavior.Strict); var responseFeature = new Mock<IHttpResponseFeature>(MockBehavior.Strict);
tempData responseFeature
.Setup(m => m.Keep()) .Setup(rf => rf.OnStarting(It.IsAny<System.Func<object, Task>>(), It.IsAny<object>()))
.Verifiable(); .Verifiable();
var tempDataFactory = new Mock<ITempDataDictionaryFactory>(MockBehavior.Strict); var tempDataFactory = new Mock<ITempDataDictionaryFactory>(MockBehavior.Strict);
tempDataFactory tempDataFactory
.Setup(f => f.GetTempData(It.IsAny<HttpContext>())) .Setup(f => f.GetTempData(It.IsAny<HttpContext>()))
.Returns(tempData.Object); .Verifiable();
var filter = new SaveTempDataFilter(tempDataFactory.Object); var filter = new SaveTempDataFilter(tempDataFactory.Object);
var httpContext = GetHttpContext(responseFeature.Object);
var context = GetResultExecutingContext(httpContext);
var context = new ResultExecutedContext( // Act
new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()), filter.OnResultExecuting(context);
new IFilterMetadata[] { },
new Mock<IKeepTempDataResult>().Object, // Assert
new object()); responseFeature.Verify();
tempDataFactory.Verify(tdf => tdf.GetTempData(It.IsAny<HttpContext>()), 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<ITempDataDictionaryFactory>(MockBehavior.Strict);
tempDataFactory
.Setup(f => f.GetTempData(It.IsAny<HttpContext>()))
.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<HttpContext>()), 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<ITempDataDictionaryFactory>(MockBehavior.Strict);
tempDataFactory
.Setup(f => f.GetTempData(It.IsAny<HttpContext>()))
.Verifiable();
var filter = new SaveTempDataFilter(tempDataFactory.Object);
var httpContext = GetHttpContext(new TestResponseFeature(hasStarted: true));
var context = GetResultExecutedContext(httpContext);
// Act // Act
filter.OnResultExecuted(context); filter.OnResultExecuted(context);
// Assert // Assert
tempData.Verify(); tempDataFactory.Verify(tdf => tdf.GetTempData(It.IsAny<HttpContext>()), 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] [Fact]
public void SaveTempDataFilter_OnResultExecuted_DoesNotKeepTempData_ForNonIKeepTempDataResult() public void OnResultExecuted_KeepsTempData_ForIKeepTempDataResult()
{ {
// Arrange // Arrange
var tempData = new Mock<ITempDataDictionary>(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<ITempDataDictionaryFactory> GetTempDataDictionaryFactory(ITempDataDictionary tempDataDictionary)
{
var tempDataFactory = new Mock<ITempDataDictionaryFactory>(MockBehavior.Strict); var tempDataFactory = new Mock<ITempDataDictionaryFactory>(MockBehavior.Strict);
tempDataFactory tempDataFactory
.Setup(f => f.GetTempData(It.IsAny<HttpContext>())) .Setup(f => f.GetTempData(It.IsAny<HttpContext>()))
.Returns(tempData.Object); .Returns(tempDataDictionary);
return tempDataFactory;
}
var filter = new SaveTempDataFilter(tempDataFactory.Object); private Mock<ITempDataDictionary> GetTempDataDictionary()
{
var tempDataDictionary = new Mock<ITempDataDictionary>(MockBehavior.Strict);
tempDataDictionary
.Setup(tdd => tdd.Keep())
.Verifiable();
tempDataDictionary
.Setup(tdd => tdd.Save())
.Verifiable();
return tempDataDictionary;
}
var context = new ResultExecutedContext( private ResultExecutedContext GetResultExecutedContext(HttpContext httpContext = null, IActionResult actionResult = null)
new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()), {
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 IFilterMetadata[] { },
new Mock<IActionResult>().Object, new Mock<IActionResult>().Object,
new object()); new TestController());
}
// Act private HttpContext GetHttpContext(IHttpResponseFeature responseFeature = null)
filter.OnResultExecuted(context); {
if (responseFeature == null)
{
responseFeature = new TestResponseFeature();
}
var httpContext = new DefaultHttpContext();
httpContext.Features.Set<IHttpResponseFeature>(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<Task> _responseStartingAsync = () => Task.FromResult(true);
public TestResponseFeature(bool hasStarted = false)
{
_hasStarted = hasStarted;
}
public override bool HasStarted
{
get
{
return _hasStarted;
}
}
public override void OnStarting(Func<object, Task> callback, object state)
{
var prior = _responseStartingAsync;
_responseStartingAsync = async () =>
{
await callback(state);
await prior();
};
}
public async Task FireOnSendingHeadersAsync()
{
await _responseStartingAsync();
_hasStarted = true;
}
} }
} }
} }

View File

@ -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<object, Type> InvalidTypes
{
get
{
return new TheoryData<object, Type>
{
{ new object(), typeof(object) },
{ new object[3], typeof(object) },
{ new TestItem(), typeof(TestItem) },
{ new List<TestItem>(), typeof(TestItem) },
{ new Dictionary<string, TestItem>(), 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<InvalidOperationException>(() =>
{
testProvider.EnsureObjectCanBeSerialized(value);
});
Assert.Equal($"The '{typeof(TempDataSerializer).FullName}' cannot serialize " +
$"an object of type '{type}'.",
exception.Message);
}
public static TheoryData<object, Type> InvalidDictionaryTypes
{
get
{
return new TheoryData<object, Type>
{
{ new Dictionary<int, string>(), typeof(int) },
{ new Dictionary<Uri, Guid>(), typeof(Uri) },
{ new Dictionary<object, string>(), typeof(object) },
{ new Dictionary<TestItem, TestItem>(), 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<InvalidOperationException>(() =>
{
testProvider.EnsureObjectCanBeSerialized(value);
});
Assert.Equal($"The '{typeof(TempDataSerializer).FullName}' cannot serialize a dictionary " +
$"with a key of type '{type}'.",
exception.Message);
}
public static TheoryData<object> ValidTypes
{
get
{
return new TheoryData<object>
{
{ 10 },
{ new int[]{ 10, 20 } },
{ "FooValue" },
{ new Uri("http://Foo") },
{ Guid.NewGuid() },
{ new List<string> { "foo", "bar" } },
{ new DateTimeOffset() },
{ 100.1m },
{ new Dictionary<string, int>() },
{ 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<string, object>
{
{ 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; }
}
}
}

View File

@ -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<string, object>();
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<string, string>()
{
{ CookieTempDataProvider.CookieName, base64EncodedDataInCookie }
});
var httpContext = new Mock<HttpContext>();
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<string, object>();
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>();
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<string, object>();
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>();
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<string, object>();
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>();
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<string, object>();
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<string, string>()
{
{ CookieTempDataProvider.CookieName, base64EncodedData }
});
var responseCookies = new MockResponseCookieCollection();
var httpContext = new Mock<HttpContext>();
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<string, object>());
// 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, object>
{
{ "string", "value" }
};
var context = GetHttpContext();
// Act
testProvider.SaveTempData(context, input);
UpdateRequestWithCookies(context);
var TempData = testProvider.LoadTempData(context);
// Assert
var stringVal = Assert.IsType<string>(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<string, object>
{
{ "int", expected }
};
var context = GetHttpContext();
// Act
testProvider.SaveTempData(context, input);
UpdateRequestWithCookies(context);
var TempData = testProvider.LoadTempData(context);
// Assert
var intVal = Assert.IsType<int>(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<string, object>
{
{ "bool", value }
};
var context = GetHttpContext();
// Act
testProvider.SaveTempData(context, input);
UpdateRequestWithCookies(context);
var TempData = testProvider.LoadTempData(context);
// Assert
var boolVal = Assert.IsType<bool>(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<string, object>
{
{ "DateTime", inputDatetime }
};
var context = GetHttpContext();
// Act
testProvider.SaveTempData(context, input);
UpdateRequestWithCookies(context);
var TempData = testProvider.LoadTempData(context);
// Assert
var datetime = Assert.IsType<DateTime>(TempData["DateTime"]);
Assert.Equal(inputDatetime, datetime);
}
[Fact]
public void SaveAndLoad_GuidCanBeStoredAndLoaded()
{
// Arrange
var testProvider = GetProvider();
var inputGuid = Guid.NewGuid();
var input = new Dictionary<string, object>
{
{ "Guid", inputGuid }
};
var context = GetHttpContext();
// Act
testProvider.SaveTempData(context, input);
UpdateRequestWithCookies(context);
var TempData = testProvider.LoadTempData(context);
// Assert
var guidVal = Assert.IsType<Guid>(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<string, object>
{
{ 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<string, object>
{
{ 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<long>(result);
Assert.Equal(expected, actual);
}
[Fact]
public void SaveAndLoad_ListCanBeStoredAndLoaded()
{
// Arrange
var testProvider = GetProvider();
var input = new Dictionary<string, object>
{
{ "List`string", new List<string> { "one", "two" } }
};
var context = GetHttpContext();
// Act
testProvider.SaveTempData(context, input);
UpdateRequestWithCookies(context);
var TempData = testProvider.LoadTempData(context);
// Assert
var list = (IList<string>)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<string, string>
{
{ "Hello", "World" },
};
var input = new Dictionary<string, object>
{
{ "Dictionary", inputDictionary }
};
var context = GetHttpContext();
// Act
testProvider.SaveTempData(context, input);
UpdateRequestWithCookies(context);
var TempData = testProvider.LoadTempData(context);
// Assert
var dictionary = Assert.IsType<Dictionary<string, string>>(TempData["Dictionary"]);
Assert.Equal("World", dictionary["Hello"]);
}
[Fact]
public void SaveAndLoad_EmptyDictionary_RoundTripsAsNull()
{
// Arrange
var testProvider = GetProvider();
var input = new Dictionary<string, object>
{
{ "EmptyDictionary", new Dictionary<string, int>() }
};
var context = GetHttpContext();
// Act
testProvider.SaveTempData(context, input);
UpdateRequestWithCookies(context);
var TempData = testProvider.LoadTempData(context);
// Assert
var emptyDictionary = (IDictionary<string, int>)TempData["EmptyDictionary"];
Assert.Null(emptyDictionary);
}
private static HttpContext GetHttpContext()
{
var context = new Mock<HttpContext>();
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<string, string>();
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<CookieInfo>
{
private Dictionary<string, CookieInfo> _cookies = new Dictionary<string, CookieInfo>(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<CookieInfo> 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<IOptions<CookieTempDataProviderOptions>>();
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; }
}
}
}

View File

@ -7,6 +7,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures namespace Microsoft.AspNetCore.Mvc.ViewFeatures
@ -54,116 +55,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
Assert.Empty(tempDataDictionary); 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<object, Type> InvalidTypes
{
get
{
return new TheoryData<object, Type>
{
{ new object(), typeof(object) },
{ new object[3], typeof(object) },
{ new TestItem(), typeof(TestItem) },
{ new List<TestItem>(), typeof(TestItem) },
{ new Dictionary<string, TestItem>(), 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<InvalidOperationException>(() =>
{
testProvider.EnsureObjectCanBeSerialized(value);
});
Assert.Equal($"The '{typeof(SessionStateTempDataProvider).FullName}' cannot serialize " +
$"an object of type '{type}' to session state.",
exception.Message);
}
public static TheoryData<object, Type> InvalidDictionaryTypes
{
get
{
return new TheoryData<object, Type>
{
{ new Dictionary<int, string>(), typeof(int) },
{ new Dictionary<Uri, Guid>(), typeof(Uri) },
{ new Dictionary<object, string>(), typeof(object) },
{ new Dictionary<TestItem, TestItem>(), 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<InvalidOperationException>(() =>
{
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<object> ValidTypes
{
get
{
return new TheoryData<object>
{
{ 10 },
{ new int[]{ 10, 20 } },
{ "FooValue" },
{ new Uri("http://Foo") },
{ Guid.NewGuid() },
{ new List<string> { "foo", "bar" } },
{ new DateTimeOffset() },
{ 100.1m },
{ new Dictionary<string, int>() },
{ 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] [Fact]
public void SaveAndLoad_StringCanBeStoredAndLoaded() public void SaveAndLoad_StringCanBeStoredAndLoaded()
{ {
@ -384,26 +275,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
Assert.Null(emptyDictionary); Assert.Null(emptyDictionary);
} }
[Fact]
public void SaveAndLoad_NullValue_RoundTripsSuccessfully()
{
// Arrange
var testProvider = new SessionStateTempDataProvider();
var input = new Dictionary<string, object>
{
{ "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 private class TestItem
{ {
public int DummyInt { get; set; } public int DummyInt { get; set; }

View File

@ -68,5 +68,30 @@ namespace BasicWebSite.Controllers
var value5 = (Guid)TempData["key5"]; var value5 = (Guid)TempData["key5"];
return $"{value1} {value2.ToString()} {value3.Count.ToString()} {value4.ToString()} {value5.ToString()}"; 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();
}
} }
} }

View File

@ -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<Startup>()
.UseKestrel()
.UseIISIntegration()
.Build();
host.Run();
}
}
}

View File

@ -53,18 +53,6 @@ namespace BasicWebSite
}); });
} }
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<Startup>()
.UseKestrel()
.UseIISIntegration()
.Build();
host.Run();
}
} }
} }

View File

@ -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<ITempDataProvider, CookieTempDataProvider>();
}
public void Configure(IApplicationBuilder app)
{
app.UseCultureReplacer();
app.UseMvcWithDefaultRoute();
}
}
}

View File

@ -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<ITempDataDictionaryFactory>();
var tempDataDictionary = tempDataDictionaryFactory.GetTempData(httpContext);
tempDataDictionary["Name"] = "Michael";
return httpContext.Response.WriteAsync($"Hello from {nameof(StoreIntoTempDataActionResult)}");
}
}
}