Skip to content
Draft
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7c557e2
Basic E2E test
dariatiurina Dec 10, 2025
8f21417
First simple implementation that holds values no matter what
dariatiurina Dec 10, 2025
43608b0
Made TempData connected to the HttpContext and improved TempData
dariatiurina Dec 11, 2025
5612308
Fixed E2E test to be SSR
dariatiurina Dec 11, 2025
c73f370
Add for manual testing
dariatiurina Dec 11, 2025
9948bcd
Added new E2E tests
dariatiurina Dec 12, 2025
edb2244
Merge branch 'main' into 49683-tempdata
dariatiurina Dec 12, 2025
a9385a6
Merge branch '49683-tempdata' of https://github.com/dariatiurina/aspn…
dariatiurina Dec 12, 2025
9869aa7
Added XML comment
dariatiurina Dec 12, 2025
d93d266
Added new tests
dariatiurina Dec 15, 2025
7630764
Added inheritance from IDictionary
dariatiurina Dec 15, 2025
8b5bbe2
Added IDataProtector to TempDataService
dariatiurina Dec 15, 2025
1d2fc6d
Fix + Enumerator
dariatiurina Dec 15, 2025
31ead26
Fix + unit tests
dariatiurina Dec 15, 2025
68f401d
Merge branch 'main' into 49683-tempdata
dariatiurina Dec 15, 2025
21a3c3b
Fix
dariatiurina Dec 16, 2025
7b50676
TempDataTest
dariatiurina Dec 16, 2025
ecebef8
Fix
dariatiurina Dec 16, 2025
6002ab1
Clean-up
dariatiurina Dec 16, 2025
902cdc9
Add limit for encoded value
dariatiurina Dec 16, 2025
eff02b6
Small decoupling
dariatiurina Dec 17, 2025
1f23e69
Moved to Endpoints
dariatiurina Dec 18, 2025
98ece70
Lazy loading
dariatiurina Dec 18, 2025
bfcace9
Added ITempDataProvider and CookieTempDataProvider
dariatiurina Dec 23, 2025
e5e26c6
Fixed visibility modifiers for TempData and CookieTempDataProvider
dariatiurina Dec 23, 2025
5817356
Decoupling and small fix
dariatiurina Dec 23, 2025
677c014
Update serializer
dariatiurina Dec 23, 2025
c5b0365
Fixes
dariatiurina Dec 23, 2025
8f91662
Fix
dariatiurina Dec 23, 2025
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 @@ -74,6 +74,7 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection
services.TryAddCascadingValue(sp => sp.GetRequiredService<EndpointHtmlRenderer>().HttpContext);
services.TryAddScoped<WebAssemblySettingsEmitter>();
services.TryAddScoped<ResourcePreloadService>();
services.AddTempDataValueProvider();

services.TryAddScoped<ResourceCollectionProvider>();
RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration<ResourceCollectionProvider>(services, RenderMode.InteractiveWebAssembly);
Expand Down
216 changes: 216 additions & 0 deletions src/Components/Endpoints/src/DependencyInjection/TempDataService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
// 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.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Components.Endpoints;

internal sealed partial class TempDataService
{
private const string CookieName = ".AspNetCore.Components.TempData";
private const string PurposeString = "Microsoft.AspNetCore.Components.Endpoints.TempDataService";
private const int MaxEncodedLength = 4050;

private static IDataProtector GetDataProtector(HttpContext httpContext)
{
var dataProtectionProvider = httpContext.RequestServices.GetRequiredService<IDataProtectionProvider>();
return dataProtectionProvider.CreateProtector(PurposeString);
}

public static TempData CreateEmpty(HttpContext httpContext)
{
return new TempData(() => Load(httpContext));
}

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

var protectedBytes = WebEncoders.Base64UrlDecode(serializedDataFromCookie);
var unprotectedBytes = GetDataProtector(httpContext).Unprotect(protectedBytes);

var dataFromCookie = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(unprotectedBytes);

if (dataFromCookie is null)
{
return returnTempData;
}

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

returnTempData.Load(convertedData);
return returnTempData;
}
catch (Exception ex)
{
// If any error occurs during loading (e.g. data protection key changed, malformed cookie),
// return an empty TempData dictionary.
if (httpContext.RequestServices.GetService<ILogger<TempDataService>>() is { } logger)
{
Log.TempDataCookieLoadFailure(logger, CookieName, ex);
}

httpContext.Response.Cookies.Delete(CookieName, new CookieOptions
{
Path = httpContext.Request.PathBase.HasValue ? httpContext.Request.PathBase.Value : "/",
});
return new TempData();
}
}

public static void Save(HttpContext httpContext, TempData tempData)
{
var dataToSave = tempData.Save();
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;
}

var bytes = JsonSerializer.SerializeToUtf8Bytes(dataToSave);
var protectedBytes = GetDataProtector(httpContext).Protect(bytes);
var encodedValue = WebEncoders.Base64UrlEncode(protectedBytes);

if (encodedValue.Length > MaxEncodedLength)
{
if (httpContext.RequestServices.GetService<ILogger<TempDataService>>() is { } logger)
{
Log.TempDataCookieSaveFailure(logger, CookieName);
}

httpContext.Response.Cookies.Delete(CookieName, new CookieOptions
{
Path = httpContext.Request.PathBase.HasValue ? httpContext.Request.PathBase.Value : "/",
});
return;
}

httpContext.Response.Cookies.Append(CookieName, encodedValue, new CookieOptions
{
HttpOnly = true,
IsEssential = true,
SameSite = SameSiteMode.Lax,
Secure = httpContext.Request.IsHttps,
Path = httpContext.Request.PathBase.HasValue ? httpContext.Request.PathBase.Value : "/",
});
}

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

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

private static partial class Log
{
[LoggerMessage(3, LogLevel.Warning, "The temp data cookie {CookieName} could not be loaded.", EventName = "TempDataCookieLoadFailure")]
public static partial void TempDataCookieLoadFailure(ILogger logger, string cookieName, Exception exception);

[LoggerMessage(3, LogLevel.Warning, "The temp data cookie {CookieName} could not be saved, because it is too large to fit in a single cookie.", EventName = "TempDataCookieSaveFailure")]
public static partial void TempDataCookieSaveFailure(ILogger logger, string cookieName);
}
}
21 changes: 21 additions & 0 deletions src/Components/Endpoints/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
#nullable enable
Microsoft.AspNetCore.Components.Endpoints.BasePath
Microsoft.AspNetCore.Components.Endpoints.BasePath.BasePath() -> void
Microsoft.AspNetCore.Components.Endpoints.TempDataProviderServiceCollectionExtensions
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.Peek(string! key) -> object?
Microsoft.AspNetCore.Components.TempData
Microsoft.AspNetCore.Components.TempData.TempData() -> void

Check failure on line 11 in src/Components/Endpoints/src/PublicAPI.Unshipped.txt

View check run for this annotation

Azure Pipelines / aspnetcore-quarantined-pr (Tests: Ubuntu x64)

src/Components/Endpoints/src/PublicAPI.Unshipped.txt#L11

src/Components/Endpoints/src/PublicAPI.Unshipped.txt(11,1): error RS0017: (NETCORE_ENGINEERING_TELEMETRY=Build) Symbol 'Microsoft.AspNetCore.Components.TempData.TempData() -> void' is part of the declared API, but is either not public or could not be found (https://github.com/dotnet/roslyn-analyzers/blob/main/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md)

Check failure on line 11 in src/Components/Endpoints/src/PublicAPI.Unshipped.txt

View check run for this annotation

Azure Pipelines / aspnetcore-quarantined-pr (Tests: macOS)

src/Components/Endpoints/src/PublicAPI.Unshipped.txt#L11

src/Components/Endpoints/src/PublicAPI.Unshipped.txt(11,1): error RS0017: (NETCORE_ENGINEERING_TELEMETRY=Build) Symbol 'Microsoft.AspNetCore.Components.TempData.TempData() -> void' is part of the declared API, but is either not public or could not be found (https://github.com/dotnet/roslyn-analyzers/blob/main/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md)

Check failure on line 11 in src/Components/Endpoints/src/PublicAPI.Unshipped.txt

View check run for this annotation

Azure Pipelines / aspnetcore-quarantined-pr

src/Components/Endpoints/src/PublicAPI.Unshipped.txt#L11

src/Components/Endpoints/src/PublicAPI.Unshipped.txt(11,1): error RS0017: (NETCORE_ENGINEERING_TELEMETRY=Build) Symbol 'Microsoft.AspNetCore.Components.TempData.TempData() -> void' is part of the declared API, but is either not public or could not be found (https://github.com/dotnet/roslyn-analyzers/blob/main/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md)

Check failure on line 11 in src/Components/Endpoints/src/PublicAPI.Unshipped.txt

View check run for this annotation

Azure Pipelines / aspnetcore-quarantined-pr

src/Components/Endpoints/src/PublicAPI.Unshipped.txt#L11

src/Components/Endpoints/src/PublicAPI.Unshipped.txt(11,1): error RS0017: (NETCORE_ENGINEERING_TELEMETRY=Build) Symbol 'Microsoft.AspNetCore.Components.TempData.TempData() -> void' is part of the declared API, but is either not public or could not be found (https://github.com/dotnet/roslyn-analyzers/blob/main/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md)

Check failure on line 11 in src/Components/Endpoints/src/PublicAPI.Unshipped.txt

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Test: Ubuntu x64)

src/Components/Endpoints/src/PublicAPI.Unshipped.txt#L11

src/Components/Endpoints/src/PublicAPI.Unshipped.txt(11,1): error RS0017: (NETCORE_ENGINEERING_TELEMETRY=Build) Symbol 'Microsoft.AspNetCore.Components.TempData.TempData() -> void' is part of the declared API, but is either not public or could not be found (https://github.com/dotnet/roslyn-analyzers/blob/main/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md)

Check failure on line 11 in src/Components/Endpoints/src/PublicAPI.Unshipped.txt

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: macOS x64)

src/Components/Endpoints/src/PublicAPI.Unshipped.txt#L11

src/Components/Endpoints/src/PublicAPI.Unshipped.txt(11,1): error RS0017: (NETCORE_ENGINEERING_TELEMETRY=Build) Symbol 'Microsoft.AspNetCore.Components.TempData.TempData() -> void' is part of the declared API, but is either not public or could not be found (https://github.com/dotnet/roslyn-analyzers/blob/main/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md)

Check failure on line 11 in src/Components/Endpoints/src/PublicAPI.Unshipped.txt

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: macOS arm64)

src/Components/Endpoints/src/PublicAPI.Unshipped.txt#L11

src/Components/Endpoints/src/PublicAPI.Unshipped.txt(11,1): error RS0017: (NETCORE_ENGINEERING_TELEMETRY=Build) Symbol 'Microsoft.AspNetCore.Components.TempData.TempData() -> void' is part of the declared API, but is either not public or could not be found (https://github.com/dotnet/roslyn-analyzers/blob/main/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md)

Check failure on line 11 in src/Components/Endpoints/src/PublicAPI.Unshipped.txt

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Test: macOS)

src/Components/Endpoints/src/PublicAPI.Unshipped.txt#L11

src/Components/Endpoints/src/PublicAPI.Unshipped.txt(11,1): error RS0017: (NETCORE_ENGINEERING_TELEMETRY=Build) Symbol 'Microsoft.AspNetCore.Components.TempData.TempData() -> void' is part of the declared API, but is either not public or could not be found (https://github.com/dotnet/roslyn-analyzers/blob/main/src/PublicApiAnalyzers/PublicApiAnalyzers.Help.md)
Microsoft.AspNetCore.Components.TempData.Clear() -> void
Microsoft.AspNetCore.Components.TempData.this[string! key].get -> object?
Microsoft.AspNetCore.Components.TempData.this[string! key].set -> void
Microsoft.AspNetCore.Components.TempData.ContainsKey(string! key) -> bool
Microsoft.AspNetCore.Components.TempData.ContainsValue(object? value) -> bool
Microsoft.AspNetCore.Components.TempData.Get(string! key) -> object?
Microsoft.AspNetCore.Components.TempData.Peek(string! key) -> object?
Microsoft.AspNetCore.Components.TempData.Keep() -> void
Microsoft.AspNetCore.Components.TempData.Keep(string! key) -> void
Microsoft.AspNetCore.Components.TempData.Remove(string! key) -> bool
Microsoft.AspNetCore.Components.TempData.Load(System.Collections.Generic.IDictionary<string!, object?>! data) -> void
Microsoft.AspNetCore.Components.TempData.Save() -> System.Collections.Generic.IDictionary<string!, object?>!
static Microsoft.AspNetCore.Components.Endpoints.TempDataProviderServiceCollectionExtensions.AddTempDataValueProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
32 changes: 32 additions & 0 deletions src/Components/Endpoints/src/TempData/ITempData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// 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 : IDictionary<string, object?>
{
/// <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);
}
Loading
Loading