Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 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
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
@@ -0,0 +1,125 @@
// 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 CookieTempDataProvider : ITempDataProvider
{
private const string CookieName = ".AspNetCore.Components.TempData";
private const string Purpose = "Microsoft.AspNetCore.Components.CookieTempDataProviderToken.v1";
private const int MaxEncodedLength = 4050;
private readonly IDataProtector _dataProtector;

public CookieTempDataProvider(
IDataProtectionProvider dataProtectionProvider)
{
_dataProtector = dataProtectionProvider.CreateProtector(Purpose);
}

public IDictionary<string, object?> LoadTempData(HttpContext context)
{
try
{
var serializedDataFromCookie = context.Request.Cookies[CookieName];
if (serializedDataFromCookie is null)
{
return new Dictionary<string, object?>();
}

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

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

if (dataFromCookie is null)
{
return new Dictionary<string, object?>();
}

var convertedData = new Dictionary<string, object?>();
foreach (var kvp in dataFromCookie)
{
convertedData[kvp.Key] = TempDataSerializer.ConvertJsonElement(kvp.Value);
}
return convertedData;
}
catch (Exception ex)
{
// If any error occurs during loading (e.g. data protection key changed, malformed cookie),
// return an empty TempData dictionary.
if (context.RequestServices.GetService<ILogger<CookieTempDataProvider>>() is { } logger)
{
Log.TempDataCookieLoadFailure(logger, CookieName, ex);
}

context.Response.Cookies.Delete(CookieName, new CookieOptions
{
Path = context.Request.PathBase.HasValue ? context.Request.PathBase.Value : "/",
});
return new Dictionary<string, object?>();
}
}

public void SaveTempData(HttpContext context, IDictionary<string, object?> values)
{
foreach (var kvp in values)
{
if (!TempDataSerializer.CanSerializeType(kvp.Value?.GetType() ?? typeof(object)))
{
throw new InvalidOperationException($"TempData cannot store values of type '{kvp.Value?.GetType()}'.");
}
}

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

var bytes = JsonSerializer.SerializeToUtf8Bytes(values);
var protectedBytes = _dataProtector.Protect(bytes);
var encodedValue = WebEncoders.Base64UrlEncode(protectedBytes);

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

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

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

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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Components.Endpoints;

/// <summary>
/// Provides an abstraction for a provider that stores and retrieves temporary data.
/// </summary>
public interface ITempDataProvider
{
/// <summary>
/// Loads temporary data from the given <see cref="HttpContext"/>.
/// </summary>
IDictionary<string, object?> LoadTempData(HttpContext context);

/// <summary>
/// Saves temporary data to the given <see cref="HttpContext"/>.
/// </summary>
void SaveTempData(HttpContext context, IDictionary<string, object?> values);
}
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Components.Endpoints;

internal sealed partial class TempDataService
{
private readonly ITempDataProvider _tempDataProvider;

public TempDataService(ITempDataProvider tempDataProvider)
{
_tempDataProvider = tempDataProvider;
}

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

public IDictionary<string, object?> Load(HttpContext httpContext)
{
return _tempDataProvider.LoadTempData(httpContext);
}

public void Save(HttpContext httpContext, TempData tempData)
{
_tempDataProvider.SaveTempData(httpContext, tempData.Save());
}
}
11 changes: 11 additions & 0 deletions src/Components/Endpoints/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
#nullable enable
Microsoft.AspNetCore.Components.Endpoints.BasePath
Microsoft.AspNetCore.Components.Endpoints.BasePath.BasePath() -> void
Microsoft.AspNetCore.Components.Endpoints.ITempDataProvider
Microsoft.AspNetCore.Components.Endpoints.ITempDataProvider.LoadTempData(Microsoft.AspNetCore.Http.HttpContext! context) -> System.Collections.Generic.IDictionary<string!, object?>!
Microsoft.AspNetCore.Components.Endpoints.ITempDataProvider.SaveTempData(Microsoft.AspNetCore.Http.HttpContext! context, System.Collections.Generic.IDictionary<string!, object?>! values) -> void
Microsoft.AspNetCore.Components.Endpoints.TempDataProviderServiceCollectionExtensions
Microsoft.AspNetCore.Components.ITempData
Microsoft.AspNetCore.Components.ITempData.ContainsValue(object! value) -> bool
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?
static Microsoft.AspNetCore.Components.Endpoints.TempDataProviderServiceCollectionExtensions.AddTempDataValueProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
37 changes: 37 additions & 0 deletions src/Components/Endpoints/src/TempData/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 : 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);

/// <summary>
/// Returns true if the TempData dictionary contains the specified <paramref name="value"/>.
/// </summary>
bool ContainsValue(object value);
}
Loading
Loading