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

Commit 89bd6dc

Browse files
committed
[Fixes #5212] Added a cookie based ITempDataProvider
1 parent 4cca6b0 commit 89bd6dc

21 files changed

+1716
-467
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Http;
5+
using Microsoft.AspNetCore.Mvc.ViewFeatures;
6+
7+
namespace Microsoft.AspNetCore.Mvc
8+
{
9+
/// <summary>
10+
/// Provides programmatic configuration for cookies set by <see cref="CookieTempDataProvider"/>
11+
/// </summary>
12+
public class CookieTempDataProviderOptions
13+
{
14+
/// <summary>
15+
/// The path set for a cookie. If not specified, current request's <see cref="HttpRequest.PathBase"/> value is used.
16+
/// </summary>
17+
public string Path { get; set; }
18+
19+
/// <summary>
20+
/// The domain set on a cookie. Defaults to <c>null</c>.
21+
/// </summary>
22+
public string Domain { get; set; }
23+
}
24+
}

src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/SaveTempDataFilter.cs

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using Microsoft.AspNetCore.Mvc.Filters;
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.AspNetCore.Mvc.Internal;
57

68
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
79
{
@@ -10,6 +12,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
1012
/// </summary>
1113
public class SaveTempDataFilter : IResourceFilter, IResultFilter
1214
{
15+
// Internal for unit testing
16+
internal static readonly object TempDataSavedKey = new object();
17+
1318
private readonly ITempDataDictionaryFactory _factory;
1419

1520
/// <summary>
@@ -29,21 +34,68 @@ public void OnResourceExecuting(ResourceExecutingContext context)
2934
/// <inheritdoc />
3035
public void OnResourceExecuted(ResourceExecutedContext context)
3136
{
32-
_factory.GetTempData(context.HttpContext).Save();
3337
}
3438

3539
/// <inheritdoc />
3640
public void OnResultExecuting(ResultExecutingContext context)
3741
{
42+
context.HttpContext.Response.OnStarting((state) =>
43+
{
44+
var saveTempDataContext = (SaveTempDataContext)state;
45+
46+
// If temp data was already saved, skip trying to save again as the calls here would potentially fail
47+
// because the session feature might not be available at this point.
48+
// Example: An action returns NoContentResult and since NoContentResult does not write anything to
49+
// the body of the response, this delegate would get executed way late in the pipeline at which point
50+
// the session feature would have been removed.
51+
object obj;
52+
if (saveTempDataContext.HttpContext.Items.TryGetValue(TempDataSavedKey, out obj))
53+
{
54+
return TaskCache.CompletedTask;
55+
}
56+
57+
SaveTempData(
58+
saveTempDataContext.ActionResult,
59+
saveTempDataContext.TempDataDictionaryFactory,
60+
saveTempDataContext.HttpContext);
61+
62+
return TaskCache.CompletedTask;
63+
},
64+
state: new SaveTempDataContext()
65+
{
66+
HttpContext = context.HttpContext,
67+
ActionResult = context.Result,
68+
TempDataDictionaryFactory = _factory
69+
});
3870
}
3971

4072
/// <inheritdoc />
4173
public void OnResultExecuted(ResultExecutedContext context)
4274
{
43-
if (context.Result is IKeepTempDataResult)
75+
// We are doing this here again because the OnStarting delegate above might get fired too late in scenarios
76+
// where the action result doesn't write anything to the body. This causes the delegate to be executed
77+
// late in the pipeline at which point SessionFeature would not be available.
78+
if (!context.HttpContext.Response.HasStarted)
79+
{
80+
SaveTempData(context.Result, _factory, context.HttpContext);
81+
context.HttpContext.Items.Add(TempDataSavedKey, true);
82+
}
83+
}
84+
85+
private static void SaveTempData(IActionResult result, ITempDataDictionaryFactory factory, HttpContext httpContext)
86+
{
87+
if (result is IKeepTempDataResult)
4488
{
45-
_factory.GetTempData(context.HttpContext).Keep();
89+
factory.GetTempData(httpContext).Keep();
4690
}
91+
factory.GetTempData(httpContext).Save();
92+
}
93+
94+
private class SaveTempDataContext
95+
{
96+
public HttpContext HttpContext { get; set; }
97+
public IActionResult ActionResult { get; set; }
98+
public ITempDataDictionaryFactory TempDataDictionaryFactory { get; set; }
4799
}
48100
}
49101
}
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Concurrent;
6+
using System.Collections.Generic;
7+
using System.Diagnostics;
8+
using System.IO;
9+
using System.Linq;
10+
using System.Reflection;
11+
using Microsoft.AspNetCore.Mvc.Formatters;
12+
using Microsoft.Extensions.Internal;
13+
using Newtonsoft.Json;
14+
using Newtonsoft.Json.Bson;
15+
using Newtonsoft.Json.Linq;
16+
17+
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
18+
{
19+
public class TempDataSerializer
20+
{
21+
private readonly JsonSerializer _jsonSerializer =
22+
JsonSerializer.Create(JsonSerializerSettingsProvider.CreateSerializerSettings());
23+
24+
private static readonly MethodInfo _convertArrayMethodInfo = typeof(TempDataSerializer).GetMethod(
25+
nameof(ConvertArray), BindingFlags.Static | BindingFlags.NonPublic);
26+
private static readonly MethodInfo _convertDictionaryMethodInfo = typeof(TempDataSerializer).GetMethod(
27+
nameof(ConvertDictionary), BindingFlags.Static | BindingFlags.NonPublic);
28+
29+
private static readonly ConcurrentDictionary<Type, Func<JArray, object>> _arrayConverters =
30+
new ConcurrentDictionary<Type, Func<JArray, object>>();
31+
private static readonly ConcurrentDictionary<Type, Func<JObject, object>> _dictionaryConverters =
32+
new ConcurrentDictionary<Type, Func<JObject, object>>();
33+
34+
private static readonly Dictionary<JTokenType, Type> _tokenTypeLookup = new Dictionary<JTokenType, Type>
35+
{
36+
{ JTokenType.String, typeof(string) },
37+
{ JTokenType.Integer, typeof(int) },
38+
{ JTokenType.Boolean, typeof(bool) },
39+
{ JTokenType.Float, typeof(float) },
40+
{ JTokenType.Guid, typeof(Guid) },
41+
{ JTokenType.Date, typeof(DateTime) },
42+
{ JTokenType.TimeSpan, typeof(TimeSpan) },
43+
{ JTokenType.Uri, typeof(Uri) },
44+
};
45+
46+
public IDictionary<string, object> Deserialize(byte[] value)
47+
{
48+
Dictionary<string, object> tempDataDictionary = null;
49+
50+
using (var memoryStream = new MemoryStream(value))
51+
using (var writer = new BsonReader(memoryStream))
52+
{
53+
tempDataDictionary = _jsonSerializer.Deserialize<Dictionary<string, object>>(writer);
54+
if (tempDataDictionary == null)
55+
{
56+
return new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
57+
}
58+
}
59+
60+
var convertedDictionary = new Dictionary<string, object>(
61+
tempDataDictionary,
62+
StringComparer.OrdinalIgnoreCase);
63+
foreach (var item in tempDataDictionary)
64+
{
65+
var jArrayValue = item.Value as JArray;
66+
var jObjectValue = item.Value as JObject;
67+
if (jArrayValue != null && jArrayValue.Count > 0)
68+
{
69+
var arrayType = jArrayValue[0].Type;
70+
Type returnType;
71+
if (_tokenTypeLookup.TryGetValue(arrayType, out returnType))
72+
{
73+
var arrayConverter = _arrayConverters.GetOrAdd(returnType, type =>
74+
{
75+
return (Func<JArray, object>)_convertArrayMethodInfo
76+
.MakeGenericMethod(type)
77+
.CreateDelegate(typeof(Func<JArray, object>));
78+
});
79+
var result = arrayConverter(jArrayValue);
80+
81+
convertedDictionary[item.Key] = result;
82+
}
83+
else
84+
{
85+
var message = Resources.FormatTempData_CannotDeserializeToken(nameof(JToken), arrayType);
86+
throw new InvalidOperationException(message);
87+
}
88+
}
89+
else if (jObjectValue != null)
90+
{
91+
if (!jObjectValue.HasValues)
92+
{
93+
convertedDictionary[item.Key] = null;
94+
continue;
95+
}
96+
97+
var jTokenType = jObjectValue.Properties().First().Value.Type;
98+
Type valueType;
99+
if (_tokenTypeLookup.TryGetValue(jTokenType, out valueType))
100+
{
101+
var dictionaryConverter = _dictionaryConverters.GetOrAdd(valueType, type =>
102+
{
103+
return (Func<JObject, object>)_convertDictionaryMethodInfo
104+
.MakeGenericMethod(type)
105+
.CreateDelegate(typeof(Func<JObject, object>));
106+
});
107+
var result = dictionaryConverter(jObjectValue);
108+
109+
convertedDictionary[item.Key] = result;
110+
}
111+
else
112+
{
113+
var message = Resources.FormatTempData_CannotDeserializeToken(nameof(JToken), jTokenType);
114+
throw new InvalidOperationException(message);
115+
}
116+
}
117+
else if (item.Value is long)
118+
{
119+
var longValue = (long)item.Value;
120+
if (longValue >= int.MinValue && longValue <= int.MaxValue)
121+
{
122+
// BsonReader casts all ints to longs. We'll attempt to work around this by force converting
123+
// longs to ints when there's no loss of precision.
124+
convertedDictionary[item.Key] = (int)longValue;
125+
}
126+
}
127+
}
128+
129+
return convertedDictionary ?? new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
130+
}
131+
132+
public byte[] Serialize(IDictionary<string, object> values)
133+
{
134+
var hasValues = (values != null && values.Count > 0);
135+
if (hasValues)
136+
{
137+
foreach (var item in values.Values)
138+
{
139+
if (item != null)
140+
{
141+
// We want to allow only simple types to be serialized.
142+
EnsureObjectCanBeSerialized(item);
143+
}
144+
}
145+
146+
using (var memoryStream = new MemoryStream())
147+
{
148+
using (var writer = new BsonWriter(memoryStream))
149+
{
150+
_jsonSerializer.Serialize(writer, values);
151+
return memoryStream.ToArray();
152+
}
153+
}
154+
}
155+
else
156+
{
157+
return new byte[0];
158+
}
159+
}
160+
161+
public void EnsureObjectCanBeSerialized(object item)
162+
{
163+
var itemType = item.GetType();
164+
Type actualType = null;
165+
166+
if (itemType.IsArray)
167+
{
168+
itemType = itemType.GetElementType();
169+
}
170+
else if (itemType.GetTypeInfo().IsGenericType)
171+
{
172+
if (ClosedGenericMatcher.ExtractGenericInterface(itemType, typeof(IList<>)) != null)
173+
{
174+
var genericTypeArguments = itemType.GenericTypeArguments;
175+
Debug.Assert(genericTypeArguments.Length == 1, "IList<T> has one generic argument");
176+
actualType = genericTypeArguments[0];
177+
}
178+
else if (ClosedGenericMatcher.ExtractGenericInterface(itemType, typeof(IDictionary<,>)) != null)
179+
{
180+
var genericTypeArguments = itemType.GenericTypeArguments;
181+
Debug.Assert(
182+
genericTypeArguments.Length == 2,
183+
"IDictionary<TKey, TValue> has two generic arguments");
184+
185+
// Throw if the key type of the dictionary is not string.
186+
if (genericTypeArguments[0] != typeof(string))
187+
{
188+
var message = Resources.FormatTempData_CannotSerializeDictionary(
189+
typeof(TempDataSerializer).FullName, genericTypeArguments[0]);
190+
throw new InvalidOperationException(message);
191+
}
192+
else
193+
{
194+
actualType = genericTypeArguments[1];
195+
}
196+
}
197+
}
198+
199+
actualType = actualType ?? itemType;
200+
if (!IsSimpleType(actualType))
201+
{
202+
var underlyingType = Nullable.GetUnderlyingType(actualType) ?? actualType;
203+
var message = Resources.FormatTempData_CannotSerializeType(
204+
typeof(TempDataSerializer).FullName, underlyingType);
205+
throw new InvalidOperationException(message);
206+
}
207+
}
208+
209+
private static IList<TVal> ConvertArray<TVal>(JArray array)
210+
{
211+
return array.Values<TVal>().ToArray();
212+
}
213+
214+
private static IDictionary<string, TVal> ConvertDictionary<TVal>(JObject jObject)
215+
{
216+
var convertedDictionary = new Dictionary<string, TVal>(StringComparer.Ordinal);
217+
foreach (var item in jObject)
218+
{
219+
convertedDictionary.Add(item.Key, jObject.Value<TVal>(item.Key));
220+
}
221+
return convertedDictionary;
222+
}
223+
224+
private static bool IsSimpleType(Type type)
225+
{
226+
var typeInfo = type.GetTypeInfo();
227+
228+
return typeInfo.IsPrimitive ||
229+
typeInfo.IsEnum ||
230+
type.Equals(typeof(decimal)) ||
231+
type.Equals(typeof(string)) ||
232+
type.Equals(typeof(DateTime)) ||
233+
type.Equals(typeof(Guid)) ||
234+
type.Equals(typeof(DateTimeOffset)) ||
235+
type.Equals(typeof(TimeSpan)) ||
236+
type.Equals(typeof(Uri));
237+
}
238+
}
239+
}

src/Microsoft.AspNetCore.Mvc.ViewFeatures/Properties/Resources.Designer.cs

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)