[Fixes #5212] Added a cookie based ITempDataProvider
This commit is contained in:
parent
4cca6b09f0
commit
89bd6dc1cd
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|||
/// </summary>
|
||||
public class SaveTempDataFilter : IResourceFilter, IResultFilter
|
||||
{
|
||||
// Internal for unit testing
|
||||
internal static readonly object TempDataSavedKey = new object();
|
||||
|
||||
private readonly ITempDataDictionaryFactory _factory;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -29,21 +34,68 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
|||
/// <inheritdoc />
|
||||
public void OnResourceExecuted(ResourceExecutedContext context)
|
||||
{
|
||||
_factory.GetTempData(context.HttpContext).Save();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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 />
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -779,7 +779,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
}
|
||||
|
||||
/// <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>
|
||||
internal static string TempData_CannotSerializeDictionary
|
||||
{
|
||||
|
|
@ -787,7 +787,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
}
|
||||
|
||||
/// <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>
|
||||
internal static string FormatTempData_CannotSerializeDictionary(object p0, object p1)
|
||||
{
|
||||
|
|
@ -795,19 +795,19 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// The '{0}' cannot serialize an object of type '{1}' to session state.
|
||||
/// The '{0}' cannot serialize an object of type '{1}'.
|
||||
/// </summary>
|
||||
internal static string TempData_CannotSerializeToSession
|
||||
internal static string TempData_CannotSerializeType
|
||||
{
|
||||
get { return GetString("TempData_CannotSerializeToSession"); }
|
||||
get { return GetString("TempData_CannotSerializeType"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The '{0}' cannot serialize an object of type '{1}' to session state.
|
||||
/// The '{0}' cannot serialize an object of type '{1}'.
|
||||
/// </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>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
|
|
@ -26,36 +26,36 @@
|
|||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
|
|
@ -263,10 +263,10 @@
|
|||
<value>Cannot deserialize {0} of type '{1}'.</value>
|
||||
</data>
|
||||
<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 name="TempData_CannotSerializeToSession" xml:space="preserve">
|
||||
<value>The '{0}' cannot serialize an object of type '{1}' to session state.</value>
|
||||
<data name="TempData_CannotSerializeType" xml:space="preserve">
|
||||
<value>The '{0}' cannot serialize an object of type '{1}'.</value>
|
||||
</data>
|
||||
<data name="Dictionary_DuplicateKey" xml:space="preserve">
|
||||
<value>The collection already contains an entry with key '{0}'.</value>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,15 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
||||
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; }
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
|
|
@ -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<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>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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.
|
||||
var session = context.Session;
|
||||
|
||||
Dictionary<string, object> 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<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 _tempDataSerializer.Deserialize(value);
|
||||
}
|
||||
|
||||
return tempDataDictionary ?? new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
return new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -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<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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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-*"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<MvcTestFixture<BasicWebSite.Startup>>
|
||||
public abstract class TempDataTestBase
|
||||
{
|
||||
public TempDataTest(MvcTestFixture<BasicWebSite.Startup> 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<KeyValuePair<string, string>>
|
||||
|
|
@ -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<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);
|
||||
IEnumerable<string> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IActionResult> ActionResultsData
|
||||
{
|
||||
// Arrange
|
||||
var tempData = new Mock<ITempDataDictionary>(MockBehavior.Strict);
|
||||
tempData
|
||||
.Setup(m => m.Save())
|
||||
.Verifiable();
|
||||
|
||||
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();
|
||||
get
|
||||
{
|
||||
return new TheoryData<IActionResult>()
|
||||
{
|
||||
new TestActionResult(),
|
||||
new TestKeepTempDataActionResult()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SaveTempDataFilter_OnResultExecuted_KeepsTempData_ForIKeepTempDataResult()
|
||||
public void OnResultExecuting_RegistersOnStartingCallback()
|
||||
{
|
||||
// Arrange
|
||||
var tempData = new Mock<ITempDataDictionary>(MockBehavior.Strict);
|
||||
tempData
|
||||
.Setup(m => m.Keep())
|
||||
var responseFeature = new Mock<IHttpResponseFeature>(MockBehavior.Strict);
|
||||
responseFeature
|
||||
.Setup(rf => rf.OnStarting(It.IsAny<System.Func<object, Task>>(), It.IsAny<object>()))
|
||||
.Verifiable();
|
||||
|
||||
var tempDataFactory = new Mock<ITempDataDictionaryFactory>(MockBehavior.Strict);
|
||||
tempDataFactory
|
||||
.Setup(f => f.GetTempData(It.IsAny<HttpContext>()))
|
||||
.Returns(tempData.Object);
|
||||
|
||||
.Verifiable();
|
||||
var filter = new SaveTempDataFilter(tempDataFactory.Object);
|
||||
var httpContext = GetHttpContext(responseFeature.Object);
|
||||
var context = GetResultExecutingContext(httpContext);
|
||||
|
||||
var context = new ResultExecutedContext(
|
||||
new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()),
|
||||
new IFilterMetadata[] { },
|
||||
new Mock<IKeepTempDataResult>().Object,
|
||||
new object());
|
||||
// Act
|
||||
filter.OnResultExecuting(context);
|
||||
|
||||
// Assert
|
||||
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
|
||||
filter.OnResultExecuted(context);
|
||||
|
||||
// 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]
|
||||
public void SaveTempDataFilter_OnResultExecuted_DoesNotKeepTempData_ForNonIKeepTempDataResult()
|
||||
public void OnResultExecuted_KeepsTempData_ForIKeepTempDataResult()
|
||||
{
|
||||
// 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);
|
||||
tempDataFactory
|
||||
.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(
|
||||
new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()),
|
||||
private ResultExecutedContext GetResultExecutedContext(HttpContext httpContext = null, IActionResult actionResult = null)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
httpContext = GetHttpContext();
|
||||
}
|
||||
if (actionResult == null)
|
||||
{
|
||||
actionResult = new TestActionResult();
|
||||
}
|
||||
return new ResultExecutedContext(
|
||||
new ActionContext(httpContext, new RouteData(), new ActionDescriptor()),
|
||||
new IFilterMetadata[] { },
|
||||
actionResult,
|
||||
new TestController());
|
||||
}
|
||||
|
||||
private ResultExecutingContext GetResultExecutingContext(ActionContext actionContext, IActionResult actionResult = null)
|
||||
{
|
||||
if (actionResult == null)
|
||||
{
|
||||
actionResult = new TestActionResult();
|
||||
}
|
||||
return new ResultExecutingContext(
|
||||
actionContext,
|
||||
new IFilterMetadata[] { },
|
||||
actionResult,
|
||||
new TestController());
|
||||
}
|
||||
|
||||
private ResultExecutingContext GetResultExecutingContext(HttpContext httpContext = null, IActionResult actionResult = null)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
httpContext = new DefaultHttpContext();
|
||||
}
|
||||
if (actionResult == null)
|
||||
{
|
||||
actionResult = new TestActionResult();
|
||||
}
|
||||
return new ResultExecutingContext(
|
||||
new ActionContext(httpContext, new RouteData(), new ActionDescriptor()),
|
||||
new IFilterMetadata[] { },
|
||||
new Mock<IActionResult>().Object,
|
||||
new object());
|
||||
new TestController());
|
||||
}
|
||||
|
||||
// Act
|
||||
filter.OnResultExecuted(context);
|
||||
private HttpContext GetHttpContext(IHttpResponseFeature responseFeature = null)
|
||||
{
|
||||
if (responseFeature == null)
|
||||
{
|
||||
responseFeature = new TestResponseFeature();
|
||||
}
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Features.Set<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ using System.Linq;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||
|
|
@ -54,116 +55,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
Assert.Empty(tempDataDictionary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_ReturnsEmptyDictionary_WhenSessionDataIsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
var testProvider = new SessionStateTempDataProvider();
|
||||
var httpContext = GetHttpContext();
|
||||
httpContext.Session.Set(SessionStateTempDataProvider.TempDataSessionStateKey, new byte[] { });
|
||||
|
||||
// Act
|
||||
var tempDataDictionary = testProvider.LoadTempData(httpContext);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(tempDataDictionary);
|
||||
}
|
||||
|
||||
public static TheoryData<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]
|
||||
public void SaveAndLoad_StringCanBeStoredAndLoaded()
|
||||
{
|
||||
|
|
@ -384,26 +275,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
Assert.Null(emptyDictionary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SaveAndLoad_NullValue_RoundTripsSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var testProvider = new SessionStateTempDataProvider();
|
||||
var input = new Dictionary<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
|
||||
{
|
||||
public int DummyInt { get; set; }
|
||||
|
|
|
|||
|
|
@ -68,5 +68,30 @@ namespace BasicWebSite.Controllers
|
|||
var value5 = (Guid)TempData["key5"];
|
||||
return $"{value1} {value2.ToString()} {value3.Count.ToString()} {value4.ToString()} {value5.ToString()}";
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult SetTempDataInActionResult()
|
||||
{
|
||||
return new StoreIntoTempDataActionResult();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public string GetTempDataSetInActionResult()
|
||||
{
|
||||
return TempData["Name"]?.ToString();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult SetLargeValueInTempData(int size, char character)
|
||||
{
|
||||
TempData["LargeValue"] = new string(character, size);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public string GetLargeValueFromTempData()
|
||||
{
|
||||
return TempData["LargeValue"]?.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue