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
37 changes: 37 additions & 0 deletions src/Components/Components/src/ITempData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Components;

/// <summary>
/// Provides a dictionary for storing data that is needed for subsequent requests.
/// Data stored in TempData is automatically removed after it is read unless
/// <see cref="Keep()"/> or <see cref="Keep(string)"/> is called, or it is accessed via <see cref="Peek(string)"/>.
/// </summary>
public interface ITempData
{
/// <summary>
/// Gets or sets the value associated with the specified key.
/// </summary>
object? this[string key] { get; set; }

/// <summary>
/// Gets the value associated with the specified key and then schedules it for deletion.
/// </summary>
object? Get(string key);

/// <summary>
/// Gets the value associated with the specified key without scheduling it for deletion.
/// </summary>
object? Peek(string key);

/// <summary>
/// Makes all of the keys currently in TempData persist for another request.
/// </summary>
void Keep();

/// <summary>
/// Makes the element with the <paramref name="key"/> persist for another request.
/// </summary>
void Keep(string key);
}
17 changes: 17 additions & 0 deletions src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
#nullable enable
Microsoft.AspNetCore.Components.ITempData
Microsoft.AspNetCore.Components.ITempData.Get(string! key) -> object?
Microsoft.AspNetCore.Components.ITempData.Keep() -> void
Microsoft.AspNetCore.Components.ITempData.Keep(string! key) -> void
Microsoft.AspNetCore.Components.ITempData.this[string! key].get -> object?
Microsoft.AspNetCore.Components.ITempData.this[string! key].set -> void
Microsoft.AspNetCore.Components.ITempData.Peek(string! key) -> object?
Microsoft.AspNetCore.Components.TempData
Microsoft.AspNetCore.Components.TempData.Get(string! key) -> object?
Microsoft.AspNetCore.Components.TempData.GetDataToSave() -> System.Collections.Generic.IDictionary<string!, object?>!
Microsoft.AspNetCore.Components.TempData.Keep() -> void
Microsoft.AspNetCore.Components.TempData.Keep(string! key) -> void
Microsoft.AspNetCore.Components.TempData.LoadDataFromCookie(System.Collections.Generic.IDictionary<string!, object?>! data) -> void
Microsoft.AspNetCore.Components.TempData.Peek(string! key) -> object?
Microsoft.AspNetCore.Components.TempData.TempData() -> void
Microsoft.AspNetCore.Components.TempData.this[string! key].get -> object?
Microsoft.AspNetCore.Components.TempData.this[string! key].set -> void
*REMOVED*Microsoft.AspNetCore.Components.ResourceAsset.ResourceAsset(string! url, System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Components.ResourceAssetProperty!>? properties) -> void
*REMOVED*Microsoft.AspNetCore.Components.Routing.Router.PreferExactMatches.get -> bool
*REMOVED*Microsoft.AspNetCore.Components.Routing.Router.PreferExactMatches.set -> void
76 changes: 76 additions & 0 deletions src/Components/Components/src/TempData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Components;

/// <inheritdoc/>
public class TempData : ITempData
{
private readonly Dictionary<string, object?> _data = new();
private readonly HashSet<string> _retainedKeys = new();

/// <inheritdoc/>
public object? this[string key]
{
get => Get(key);
set
{
_data[key] = value;
_retainedKeys.Add(key);
}
}

/// <inheritdoc/>
public object? Get(string key)
{
_retainedKeys.Remove(key);
return _data.GetValueOrDefault(key);
}

/// <inheritdoc/>
public object? Peek(string key)
{
return _data.GetValueOrDefault(key);
}

/// <inheritdoc/>
public void Keep()
{
foreach (var key in _data.Keys)
{
_retainedKeys.Add(key);
}
}

/// <inheritdoc/>
public void Keep(string key)
{
if (_data.ContainsKey(key))
{
_retainedKeys.Add(key);
}
}

/// <inheritdoc/>
public IDictionary<string, object?> GetDataToSave()
{
var dataToSave = new Dictionary<string, object?>();
foreach (var key in _retainedKeys)
{
dataToSave[key] = _data[key];
}
return dataToSave;
}

/// <inheritdoc/>
public void LoadDataFromCookie(IDictionary<string, object?> data)
{
_data.Clear();
_retainedKeys.Clear();
foreach (var kvp in data)
{
_data[kvp.Key] = kvp.Value;
_retainedKeys.Add(kvp.Key);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,26 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection
services.TryAddCascadingValue(sp => sp.GetRequiredService<EndpointHtmlRenderer>().HttpContext);
services.TryAddScoped<WebAssemblySettingsEmitter>();
services.TryAddScoped<ResourcePreloadService>();
services.TryAddCascadingValue(sp =>
{
var httpContext = sp.GetRequiredService<EndpointHtmlRenderer>().HttpContext;
if (httpContext is null)
{
return null!;
}
var key = typeof(ITempData);
if (!httpContext.Items.TryGetValue(key, out var tempData))
{
var tempDataInstance = TempDataService.Load(httpContext);
httpContext.Items[key] = tempDataInstance;
httpContext.Response.OnStarting(() =>
{
TempDataService.Save(httpContext, tempDataInstance);
return Task.CompletedTask;
});
}
return (ITempData)httpContext.Items[key]!;
});

services.TryAddScoped<ResourceCollectionProvider>();
RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration<ResourceCollectionProvider>(services, RenderMode.InteractiveWebAssembly);
Expand Down
154 changes: 154 additions & 0 deletions src/Components/Endpoints/src/DependencyInjection/TempDataService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.Json;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Components.Endpoints;

internal sealed class TempDataService
{
private const string CookieName = ".AspNetCore.Components.TempData";

public TempDataService()
{
// TO-DO: Add encoding later if needed
}

public static TempData Load(HttpContext httpContext)
{
var returnTempData = new TempData();
var serializedDataFromCookie = httpContext.Request.Cookies[CookieName];
if (serializedDataFromCookie is null)
{
return returnTempData;
}

var dataFromCookie = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(serializedDataFromCookie);
if (dataFromCookie is null)
{
return returnTempData;
}

var convertedData = new Dictionary<string, object?>();
foreach (var kvp in dataFromCookie)
{
convertedData[kvp.Key] = ConvertJsonElement(kvp.Value);
}

returnTempData.LoadDataFromCookie(convertedData);
return returnTempData;
}

private static object? ConvertJsonElement(JsonElement element)
{
switch (element.ValueKind)
{
case JsonValueKind.String:
if (element.TryGetGuid(out var guid))
{
return guid;
}
if (element.TryGetDateTime(out var dateTime))
{
return dateTime;
}
return element.GetString();
case JsonValueKind.Number:
return element.GetInt32();
case JsonValueKind.True:
case JsonValueKind.False:
return element.GetBoolean();
case JsonValueKind.Null:
return null;
case JsonValueKind.Array:
return DeserializeArray(element);
case JsonValueKind.Object:
return DeserializeDictionaryEntry(element);
default:
throw new InvalidOperationException($"TempData cannot deserialize value of type '{element.ValueKind}'.");
}
}

private static object? DeserializeArray(JsonElement arrayElement)
{
var arrayLength = arrayElement.GetArrayLength();
if (arrayLength == 0)
{
return null;
}
if (arrayElement[0].ValueKind == JsonValueKind.String)
{
var array = new List<string?>(arrayLength);
foreach (var item in arrayElement.EnumerateArray())
{
array.Add(item.GetString());
}
return array.ToArray();
}
else if (arrayElement[0].ValueKind == JsonValueKind.Number)
{
var array = new List<int>(arrayLength);
foreach (var item in arrayElement.EnumerateArray())
{
array.Add(item.GetInt32());
}
return array.ToArray();
}
throw new InvalidOperationException($"TempData cannot deserialize array of type '{arrayElement[0].ValueKind}'.");
}

private static Dictionary<string, string?> DeserializeDictionaryEntry(JsonElement objectElement)
{
var dictionary = new Dictionary<string, string?>(StringComparer.Ordinal);
foreach (var item in objectElement.EnumerateObject())
{
dictionary[item.Name] = item.Value.GetString();
}
return dictionary;
}

public static void Save(HttpContext httpContext, TempData tempData)
{
var dataToSave = tempData.GetDataToSave();
foreach (var kvp in dataToSave)
{
if (!CanSerializeType(kvp.Value?.GetType() ?? typeof(object)))
{
throw new InvalidOperationException($"TempData cannot store values of type '{kvp.Value?.GetType()}'.");
}
}

if (dataToSave.Count == 0)
{
httpContext.Response.Cookies.Delete(CookieName, new CookieOptions
{
Path = httpContext.Request.PathBase.HasValue ? httpContext.Request.PathBase.Value : "/",
});
return;
}
httpContext.Response.Cookies.Append(CookieName, JsonSerializer.Serialize(dataToSave), new CookieOptions
{
HttpOnly = true,
IsEssential = true,
SameSite = SameSiteMode.Lax,
Secure = httpContext.Request.IsHttps,
Path = httpContext.Request.PathBase.HasValue ? httpContext.Request.PathBase.Value : "/",
});
}

private static bool CanSerializeType(Type type)
{
type = Nullable.GetUnderlyingType(type) ?? type;
return
type.IsEnum ||
type == typeof(int) ||
type == typeof(string) ||
type == typeof(bool) ||
type == typeof(DateTime) ||
type == typeof(Guid) ||
typeof(ICollection<int>).IsAssignableFrom(type) ||
typeof(ICollection<string>).IsAssignableFrom(type) ||
typeof(IDictionary<string, string>).IsAssignableFrom(type);
}
}
Loading
Loading