Skip to content
This repository was archived by the owner on Dec 14, 2018. It is now read-only.

Serialize only simple types to session in TempData #2317

Merged
merged 1 commit into from
Apr 17, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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.ComponentModel.DataAnnotations;
using System.Reflection;
using Newtonsoft.Json;
Expand Down
48 changes: 48 additions & 0 deletions src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/Microsoft.AspNet.Mvc.Core/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -451,4 +451,13 @@
<data name="ModelType_WrongType" xml:space="preserve">
<value>The model's runtime type '{0}' is not assignable to the 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>
<data name="TempData_CannotDeserializeToken" xml:space="preserve">
<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>
</data>
</root>
165 changes: 161 additions & 4 deletions src/Microsoft.AspNet.Mvc.Core/SessionStateTempDataProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@
// 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.AspNet.Http;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.Framework.Internal;
using Newtonsoft.Json;
using Newtonsoft.Json.Bson;
using Newtonsoft.Json.Linq;

namespace Microsoft.AspNet.Mvc
{
Expand All @@ -16,8 +22,34 @@ namespace Microsoft.AspNet.Mvc
/// </summary>
public class SessionStateTempDataProvider : ITempDataProvider
{
private static JsonSerializer jsonSerializer = new JsonSerializer();
private static string TempDataSessionStateKey = "__ControllerTempData";
private const string TempDataSessionStateKey = "__ControllerTempData";
private readonly JsonSerializer _jsonSerializer = JsonSerializer.Create(
new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blowdart FYI - be happy 😄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When am I ever HAPPY? I am content.

});

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) },
{ 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 />
public virtual IDictionary<string, object> LoadTempData([NotNull] HttpContext context)
Expand All @@ -37,9 +69,68 @@ public virtual IDictionary<string, object> LoadTempData([NotNull] HttpContext co
using (var memoryStream = new MemoryStream(value))
using (var writer = new BsonReader(memoryStream))
{
tempDataDictionary = jsonSerializer.Deserialize<Dictionary<string, object>>(writer);
tempDataDictionary = _jsonSerializer.Deserialize<Dictionary<string, object>>(writer);
}

var convertedDictionary = new Dictionary<string, object>(tempDataDictionary, StringComparer.OrdinalIgnoreCase);
foreach (var item in tempDataDictionary)
{
var jArrayValue = item.Value as JArray;
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
{
var jObjectValue = item.Value as JObject;
if (jObjectValue == null)
{
continue;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we assign a null value when jObjectValue.HasValues == false? It will avoid throwing a cast exception in user code:

TempData["key"] = new Dictionary<string, string>();

...
var myDictionary = (IDictionary<string, string>)TempData["key"]; // will throw now

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it depends on how you'd get into this state, no? What would the user have tried to store in TempData such that they would end up with !HasValue?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An empty dictionary would cause this (I think) - it's not particularly far fetched to run into that scenario.

}
else 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);
}
}
}

tempDataDictionary = convertedDictionary;

// If we got it from Session, remove it so that no other request gets it
session.Remove(TempDataSessionStateKey);
}
Expand All @@ -59,13 +150,19 @@ public virtual void SaveTempData([NotNull] HttpContext context, IDictionary<stri
var hasValues = (values != null && values.Count > 0);
if (hasValues)
{
foreach (var item in values.Values)
{
// We want to allow only simple types to be serialized in session.
EnsureObjectCanBeSerialized(item);
}

// Accessing Session property will throw if the session middleware is not enabled.
var session = context.Session;

using (var memoryStream = new MemoryStream())
using (var writer = new BsonWriter(memoryStream))
{
jsonSerializer.Serialize(writer, values);
_jsonSerializer.Serialize(writer, values);
session[TempDataSessionStateKey] = memoryStream.ToArray();
}
}
Expand All @@ -80,5 +177,65 @@ private static bool IsSessionEnabled(HttpContext context)
{
return context.GetFeature<ISessionFeature>() != null;
}

internal void EnsureObjectCanBeSerialized(object item)
{
var itemType = item.GetType();
Type actualType = null;

if (itemType.IsArray)
{
itemType = itemType.GetElementType();
}
else if (itemType.GetTypeInfo().IsGenericType)
{
if (itemType.ExtractGenericInterface(typeof(IList<>)) != null)
{
var genericTypeArguments = itemType.GetGenericArguments();
Debug.Assert(genericTypeArguments.Length == 1, "IList<T> has one generic argument");
actualType = genericTypeArguments[0];
}
else if (itemType.ExtractGenericInterface(typeof(IDictionary<,>)) != null)
{
var genericTypeArguments = itemType.GetGenericArguments();
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 (!TypeHelper.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;
}
}
}
3 changes: 2 additions & 1 deletion src/Microsoft.AspNet.Mvc/MvcServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,9 @@ public static IServiceCollection GetDefaultServices()
services.AddTransient<IApiDescriptionProvider, DefaultApiDescriptionProvider>();

// Temp Data
services.AddSingleton<ITempDataProvider, SessionStateTempDataProvider>();
services.AddScoped<ITempDataDictionary, TempDataDictionary>();
// This does caching so it should stay singleton
services.AddSingleton<ITempDataProvider, SessionStateTempDataProvider>();

return services;
}
Expand Down
Loading