Skip to content
Draft
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
Expand Up @@ -13,10 +13,10 @@ internal interface ITempDataProvider
/// <summary>
/// Loads temporary data from the given <see cref="HttpContext"/>.
/// </summary>
IDictionary<string, object?> LoadTempData(HttpContext context);
IDictionary<string, (object? Value, Type? Type)> LoadTempData(HttpContext context);

/// <summary>
/// Saves temporary data to the given <see cref="HttpContext"/>.
/// </summary>
void SaveTempData(HttpContext context, IDictionary<string, object?> values);
void SaveTempData(HttpContext context, IDictionary<string, (object? Value, Type? Type)> values);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public TempData CreateEmpty(HttpContext httpContext)
return new TempData(() => Load(httpContext));
}

public IDictionary<string, object?> Load(HttpContext httpContext)
public IDictionary<string, (object? Value, Type? Type)> Load(HttpContext httpContext)
{
return _tempDataProvider.LoadTempData(httpContext);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public CookieTempDataProvider(
_logger = logger;
}

public IDictionary<string, object?> LoadTempData(HttpContext context)
public IDictionary<string, (object? Value, Type? Type)> LoadTempData(HttpContext context)
{
ArgumentNullException.ThrowIfNull(context);
var cookieName = _options.TempDataCookie.Name ?? CookieName;
Expand All @@ -48,12 +48,12 @@ public CookieTempDataProvider(
if (!context.Request.Cookies.ContainsKey(cookieName))
{
Log.TempDataCookieNotFound(_logger, cookieName);
return ReadOnlyDictionary<string, object?>.Empty;
return ReadOnlyDictionary<string, (object? Value, Type? Type)>.Empty;
}
var serializedDataFromCookie = _chunkingCookieManager.GetRequestCookie(context, cookieName);
if (serializedDataFromCookie is null)
{
return ReadOnlyDictionary<string, object?>.Empty;
return ReadOnlyDictionary<string, (object? Value, Type? Type)>.Empty;
}

byte[]? rentedDecodeBuffer = null;
Expand Down Expand Up @@ -82,7 +82,7 @@ public CookieTempDataProvider(

if (dataFromCookie is null)
{
return ReadOnlyDictionary<string, object?>.Empty;
return ReadOnlyDictionary<string, (object? Value, Type? Type)>.Empty;
}
var convertedData = _tempDataSerializer.DeserializeData(dataFromCookie);
Log.TempDataCookieLoadSuccess(_logger, cookieName);
Expand All @@ -103,19 +103,19 @@ public CookieTempDataProvider(
var cookieOptions = _options.TempDataCookie.Build(context);
SetCookiePath(context, cookieOptions);
context.Response.Cookies.Delete(cookieName, cookieOptions);
return ReadOnlyDictionary<string, object?>.Empty;
return ReadOnlyDictionary<string, (object? Value, Type? Type)>.Empty;
}
}

public void SaveTempData(HttpContext context, IDictionary<string, object?> values)
public void SaveTempData(HttpContext context, IDictionary<string, (object? Value, Type? Type)> values)
{
ArgumentNullException.ThrowIfNull(context);

foreach (var kvp in values)
{
if (kvp.Value is not null && !_tempDataSerializer.CanSerialize(kvp.Value.GetType()))
if (kvp.Value.Value is not null && !_tempDataSerializer.CanSerialize(kvp.Value.Type!))
{
throw new InvalidOperationException($"TempData cannot store values of type '{kvp.Value.GetType()}'.");
throw new InvalidOperationException($"TempData cannot store values of type '{kvp.Value.Type}'.");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ namespace Microsoft.AspNetCore.Components.Endpoints;

internal interface ITempDataSerializer
{
public IDictionary<string, object?> DeserializeData(IDictionary<string, JsonElement> data);
public IDictionary<string, (object? Value, Type? Type)> DeserializeData(IDictionary<string, JsonElement> data);

public byte[] SerializeData(IDictionary<string, object?> data);
public byte[] SerializeData(IDictionary<string, (object? Value, Type? Type)> data);

public bool CanSerialize(Type type);
}
245 changes: 112 additions & 133 deletions src/Components/Endpoints/src/TempData/JsonTempDataSerializer.cs
Original file line number Diff line number Diff line change
@@ -1,195 +1,174 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Microsoft.AspNetCore.Components.Endpoints;

internal class JsonTempDataSerializer : ITempDataSerializer
{
public IDictionary<string, object?> DeserializeData(IDictionary<string, JsonElement> data)
private static readonly HashSet<Type> _supportedTypes = [typeof(int), typeof(bool), typeof(string), typeof(Guid), typeof(DateTime)];

private static readonly Dictionary<string, Type> _nameToType = BuildNameToType();

private static Dictionary<string, Type> BuildNameToType()
{
var dataDict = new Dictionary<string, object?>(data.Count);
foreach (var kvp in data)
var map = new Dictionary<string, Type>();
foreach (var type in _supportedTypes)
{
dataDict[kvp.Key] = DeserializeElement(kvp.Value);
map[type.FullName!] = type;
map[type.MakeArrayType().FullName!] = type.MakeArrayType();
var dictType = typeof(Dictionary<,>).MakeGenericType(typeof(string), type);
map[dictType.FullName!] = dictType;
}
return dataDict;
return map;
}

private static object? DeserializeElement(JsonElement element)
public IDictionary<string, (object? Value, Type? Type)> DeserializeData(IDictionary<string, JsonElement> data)
{
return element.ValueKind switch
{
JsonValueKind.Null => null,
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.Number => element.GetInt32(),
JsonValueKind.String => DeserializeString(element),
JsonValueKind.Array => DeserializeArray(element),
JsonValueKind.Object => DeserializeObject(element),
_ => throw new NotSupportedException($"Unsupported JSON value kind: {element.ValueKind}")
};
}
var result = new Dictionary<string, (object? Value, Type? Type)>(data.Count);

private static object? DeserializeString(JsonElement element)
{
var type = GetStringType(element);
return type switch
foreach (var (key, element) in data)
{
Type t when t == typeof(Guid) => element.GetGuid(),
Type t when t == typeof(DateTime) => element.GetDateTime(),
_ => element.GetString(),
};
if (element.ValueKind is JsonValueKind.Null)
{
result[key] = (null, null);
continue;
}

var typeName = element.GetProperty("type").GetString()!;
var valueElement = element.GetProperty("value");

if (!_nameToType.TryGetValue(typeName, out var type))
{
throw new InvalidOperationException($"Cannot deserialize type '{typeName}'.");
}

var value = JsonSerializer.Deserialize(valueElement, type);
result[key] = (value, type);
}
return result;
}

private static object? DeserializeArray(JsonElement element)
public bool CanSerialize(Type type)
{
var length = element.GetArrayLength();
if (length == 0)
if (_supportedTypes.Contains(type) || type.IsEnum)
{
return Array.Empty<object?>();
return true;
}

var array = Array.CreateInstance(GetArrayTypeInfo(element[0]), length);
var index = 0;
foreach (var item in element.EnumerateArray())
if (type.IsArray && (_supportedTypes.Contains(type.GetElementType()!) || type.GetElementType()!.IsEnum))
{
array.SetValue(DeserializeElement(item), index++);
return true;
}
return array;
}

private static Dictionary<string, object?> DeserializeObject(JsonElement element)
{
var dictionary = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
foreach (var property in element.EnumerateObject())
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>) && type.GetGenericArguments()[0] == typeof(string) && _supportedTypes.Contains(type.GetGenericArguments()[1]))
{
dictionary[property.Name] = DeserializeElement(property.Value);
return true;
}
return dictionary;
}

private static Type GetArrayTypeInfo(JsonElement element)
{
return element.ValueKind switch
var collectionElementType = GetCollectionElementType(type);
if (collectionElementType is not null)
{
JsonValueKind.True => typeof(bool),
JsonValueKind.False => typeof(bool),
JsonValueKind.Number => typeof(int),
JsonValueKind.String => GetStringType(element),
_ => typeof(object)
};
return _supportedTypes.Contains(collectionElementType) || collectionElementType.IsEnum;
}

return false;
}

private static Type GetStringType(JsonElement element)
public byte[] SerializeData(IDictionary<string, (object? Value, Type? Type)> data)
{
if (element.TryGetGuid(out _))
{
return typeof(Guid);
}
if (element.TryGetDateTime(out _))
using var buffer = new MemoryStream();
using var writer = new Utf8JsonWriter(buffer);

writer.WriteStartObject();

foreach (var (key, (value, type)) in data)
{
return typeof(DateTime);
writer.WritePropertyName(key);

if (value is null)
{
writer.WriteNullValue();
continue;
}

if (type is null || !CanSerialize(type))
{
throw new InvalidOperationException($"Cannot serialize type '{type}'.");
}

var collectionElementType = GetCollectionElementType(type);
var (writeValue, writeType) = NormalizeValue(value, type, collectionElementType);

writer.WriteStartObject();
writer.WriteString("type", writeType.FullName);
writer.WritePropertyName("value");
JsonSerializer.Serialize(writer, writeValue, writeType);
writer.WriteEndObject();
}
return typeof(string);

writer.WriteEndObject();
writer.Flush();

return buffer.ToArray();
}

public bool CanSerialize(Type type)
private static (object Value, Type Type) NormalizeValue(object value, Type type, Type? collectionElementType)
{
if (type == typeof(object) || type == typeof(object[]))
if (type.IsEnum)
{
return false;
return (Convert.ToInt32(value, CultureInfo.InvariantCulture), typeof(int));
}

if (type.IsEnum)
if (collectionElementType?.IsEnum == true)
{
return true;
return (ConvertEnumsToInts((IEnumerable)value), typeof(int[]));
}

if (JsonTempDataSerializerContext.Default.GetTypeInfo(type) is not null)
if (collectionElementType is not null && !type.IsArray)
{
return true;
return (ConvertToArray((IEnumerable)value, collectionElementType), collectionElementType.MakeArrayType());
}

var dictionaryInterface = type.GetInterface(typeof(IDictionary<,>).Name);
if (dictionaryInterface is not null)
return (value, type);
}

private static Type? GetCollectionElementType(Type type)
{
if (type.IsArray)
{
var args = dictionaryInterface.GetGenericArguments();
if (args[0] == typeof(string) && CanSerialize(args[1]))
{
return true;
}
return false;
return type.GetElementType();
}

var collectionInterface = type.GetInterface(typeof(ICollection<>).Name);
if (collectionInterface is not null)
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
{
var elementType = collectionInterface.GetGenericArguments()[0];
if (CanSerialize(elementType))
{
return true;
}
return false;
return null;
}

return false;
var collectionInterface = type.GetInterface(typeof(ICollection<>).Name);
return collectionInterface?.GetGenericArguments()[0];
}

public byte[] SerializeData(IDictionary<string, object?> data)
private static int[] ConvertEnumsToInts(IEnumerable values)
{
var normalizedData = new Dictionary<string, object?>(data.Count);
foreach (var kvp in data)
var result = new List<int>();
foreach (var item in values)
{
normalizedData[kvp.Key] = kvp.Value is Enum enumValue
? Convert.ToInt32(enumValue, CultureInfo.InvariantCulture)
: kvp.Value;
result.Add(Convert.ToInt32(item, CultureInfo.InvariantCulture));
}
return JsonSerializer.SerializeToUtf8Bytes<IDictionary<string, object?>>(normalizedData, JsonTempDataSerializerContext.Default.Options);
return result.ToArray();
}
}

// Simple types (non-nullable)
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(Guid))]
[JsonSerializable(typeof(DateTime))]
// Simple types (nullable)
[JsonSerializable(typeof(int?))]
[JsonSerializable(typeof(bool?))]
[JsonSerializable(typeof(Guid?))]
[JsonSerializable(typeof(DateTime?))]
// Collections of simple types (non-nullable)
[JsonSerializable(typeof(ICollection<int>))]
[JsonSerializable(typeof(ICollection<bool>))]
[JsonSerializable(typeof(ICollection<string>))]
[JsonSerializable(typeof(ICollection<Guid>))]
[JsonSerializable(typeof(ICollection<DateTime>))]
// Collections of simple types (nullable)
[JsonSerializable(typeof(ICollection<int?>))]
[JsonSerializable(typeof(ICollection<bool?>))]
[JsonSerializable(typeof(ICollection<Guid?>))]
[JsonSerializable(typeof(ICollection<DateTime?>))]
// Dictionaries of simple types (non-nullable)
[JsonSerializable(typeof(IDictionary<string, int>))]
[JsonSerializable(typeof(IDictionary<string, bool>))]
[JsonSerializable(typeof(IDictionary<string, string>))]
[JsonSerializable(typeof(IDictionary<string, Guid>))]
[JsonSerializable(typeof(IDictionary<string, DateTime>))]
// Dictionaries of simple types (nullable)
[JsonSerializable(typeof(IDictionary<string, int?>))]
[JsonSerializable(typeof(IDictionary<string, bool?>))]
[JsonSerializable(typeof(IDictionary<string, Guid?>))]
[JsonSerializable(typeof(IDictionary<string, DateTime?>))]
// Object arrays for nested/empty arrays
[JsonSerializable(typeof(object[]))]
[JsonSerializable(typeof(ICollection<object>))]
// Serialization of the TempData dictionary
[JsonSerializable(typeof(IDictionary<string, object?>))]
internal sealed partial class JsonTempDataSerializerContext : JsonSerializerContext
{
private static Array ConvertToArray(IEnumerable values, Type elementType)
{
var list = new ArrayList();
foreach (var item in values)
{
list.Add(item);
}
return list.ToArray(elementType);
}
}
Loading
Loading