Skip to content

SupplyParameterFromTempData support for Blazor#65306

Open
dariatiurina wants to merge 71 commits intodotnet:mainfrom
dariatiurina:49683-supply-parameter-from-tempdata
Open

SupplyParameterFromTempData support for Blazor#65306
dariatiurina wants to merge 71 commits intodotnet:mainfrom
dariatiurina:49683-supply-parameter-from-tempdata

Conversation

@dariatiurina
Copy link
Contributor

@dariatiurina dariatiurina commented Feb 3, 2026

SupplyParameterFromTempData

Summary

Provides [SupplyParameterFromTempData] attribute for Blazor SSR components to read and write TempData values, consistent with [SupplyParameterFromQuery] and [SupplyParameterFromForm] patterns.

Motivation

While TempData is accessible via the [CascadingParameter] ITempData approach, many scenarios only need simple read/write of a single value. The attribute-based approach:

  • Reduces boilerplate for common use cases
  • Provides consistency with existing SupplyParameterFrom* attributes
  • Enables automatic two-way binding without manual TempData["key"] access

Design

Attribute

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class SupplyParameterFromTempDataAttribute : CascadingParameterAttributeBase
{
    /// Gets or sets the TempData key. If not specified, the property name will be used.
    public string? Name { get; set; }

    internal override bool SingleDelivery => false;
}

Framework abstractions

Two new types enable attribute-driven cascading value suppliers without building a full ICascadingValueSupplier from scratch:

CascadingParameterSubscription — an abstract base class representing an active subscription to a cascading parameter:

public abstract class CascadingParameterSubscription : IDisposable
{
    public abstract object? GetCurrentValue();
    public abstract void Dispose();
}

CascadingParameterValueProvider<TAttribute> — an internal generic ICascadingValueSupplier that manages subscriptions for a given attribute type. Consumers provide a factory Func<ComponentState, TAttribute, CascadingParameterInfo, CascadingParameterSubscription> and the provider handles CanSupplyValue, GetCurrentValue, Subscribe, and Unsubscribe by delegating to subscription instances.

TryAddCascadingValueSupplier<TAttribute> — a new public extension method on IServiceCollection that registers a CascadingParameterValueProvider<TAttribute> using TryAddEnumerable:

public static IServiceCollection TryAddCascadingValueSupplier<TAttribute>(
    this IServiceCollection serviceCollection,
    Func<IServiceProvider, Func<ComponentState, TAttribute, CascadingParameterInfo, CascadingParameterSubscription>> subscribeFactory)
    where TAttribute : CascadingParameterAttributeBase;

TempDataCascadingValueSupplier

The internal TempDataCascadingValueSupplier class bridges the cascading value provider and the underlying TempData store:

  • SetRequestContext(HttpContext) — wires the supplier to the current request.
  • CreateSubscription(ComponentState, SupplyParameterFromTempDataAttribute, CascadingParameterInfo) — uses reflection to create a property getter for the decorated property, registers a value callback, and returns a TempDataSubscription.
  • GetValue(string, Type) — reads from TempData using Get() semantics (marks for deletion). Handles enum conversion (int → enum), type mismatches (returns null), and deserialization errors (caught, logged, returns null).
  • RegisterValueCallback(string, Func<object?>) — stores a getter invoked at persist time. Throws InvalidOperationException for duplicate keys.
  • PersistValues(ITempData) — iterates all registered callbacks, reads current property values, and writes them into TempData. Callback exceptions are caught, logged, and do not prevent other keys from being persisted.
  • DeleteValueCallback(string) — removes a registered callback (called on component unsubscribe to prevent memory leaks).

Lifecycle

  1. Initialize: TempDataCascadingValueSupplier.SetRequestContext(HttpContext) is called during EndpointHtmlRenderer.InitializeStandardComponentServicesAsync, wiring the supplier to the current request.
  2. Subscribe: When a component renders, CascadingParameterValueProvider<SupplyParameterFromTempDataAttribute> calls TempDataCascadingValueSupplier.CreateSubscription(), which uses reflection to build a property getter and registers a callback via RegisterValueCallback().
  3. GetValue: TempDataSubscription.GetCurrentValue() delegates to TempDataCascadingValueSupplier.GetValue(), which reads from TempData (marks for deletion). Enum values stored as int are converted to the target enum type. Type mismatches and deserialization errors are caught and logged, returning null.
  4. Persist: When TempDataService.Save() is called, it invokes TempDataCascadingValueSupplier.PersistValues(tempData), which iterates all registered callbacks, reads the current property values from the components, and writes them back into TempData.
  5. Unsubscribe: When a component is disposed, TempDataSubscription.Dispose() calls DeleteValueCallback() to remove the registered callback.

Implementation details

  • Case-insensitive callbacks: The internal callback dictionary uses StringComparer.OrdinalIgnoreCase.
  • Duplicate key guard: RegisterValueCallback throws InvalidOperationException if a callback is already registered for the same key — multiple components cannot bind to the same TempData key.
  • TempData resolution: TempDataCascadingValueSupplier reads TempData from HttpContext.Items[typeof(ITempData)], the same single instance used by the cascading parameter approach.
  • Type safety: Enum values stored as int are converted via Enum.ToObject. If the stored value's type is not assignable to the target type, null is returned instead of throwing.
  • Error logging: Two log events — TempDataPersistFail (callback exception during persist) and TempDataDeserializeFail (exception during read).

Registration

Automatically enabled when calling AddRazorComponents():

services.TryAddScoped<TempDataCascadingValueSupplier>();
services.TryAddCascadingValueSupplier<SupplyParameterFromTempDataAttribute>(
    sp => sp.GetRequiredService<TempDataCascadingValueSupplier>().CreateSubscription);

Usage

Basic:

@code {
    [SupplyParameterFromTempData]
    public string? Message { get; set; }
}

Custom key:

@code {
    [SupplyParameterFromTempData(Name = "flash_message")]
    public string? Message { get; set; }
}

Form with redirect:

@page "/form"

<p>@Message</p>

<form method="post" @formname="myform" @onsubmit="Submit">
    <AntiforgeryToken />
    <button type="submit">Submit</button>
</form>

@code {
    [SupplyParameterFromTempData]
    public string? Message { get; set; }

    void Submit()
    {
        Message = "Success!";
        NavigationManager.NavigateTo("/form", forceLoad: true);
    }
}

Testing

  • Unit tests (TempDataCascadingValueSupplierTest.cs): 17 tests covering GetValue (null cases, case-insensitivity, enum conversion, nullable enum, type mismatches, deserialization errors), RegisterValueCallback (add, duplicate key guard), PersistValues (single/multiple keys, null values, callback exceptions, case-insensitivity), and DeleteValueCallback.
  • E2E tests: SupplyParameterFromTempDataReadsAndSavesValues added to both TempDataCookieTest and TempDataSessionStorageTest, verifying read/write roundtrip through the attribute.

Out of scope

  • Peek() and Keep() semantics — use [CascadingParameter] ITempData for advanced control
  • Custom serialization

Risks

  • Callback ordering: When a component uses both TempData["key"] directly and [SupplyParameterFromTempData] for the same key, the final persisted value depends on execution order. Mitigation: Document that mixing approaches for the same key is unsupported.

Fixes #49683
Fixes #65039

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does setting the property causes it to be saved to temp data for the next navigation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. But reading through this property always goes through Get() so it only persists once.

Copilot AI review requested due to automatic review settings February 6, 2026 16:11
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds a new [SupplyParameterFromTempData] attribute for Blazor server-side rendering components, enabling automatic read/write of TempData values. This follows the established pattern of [SupplyParameterFromQuery] and [SupplyParameterFromForm] attributes, providing a more convenient alternative to manually accessing TempData via cascading parameters. The implementation includes a value mapper service that reads TempData values on component initialization and persists property values back to TempData before the response is sent.

Changes:

  • New SupplyParameterFromTempDataAttribute in Components assembly for marking properties
  • ITempDataValueMapper interface and TempDataValueMapper implementation for managing read/write operations
  • SupplyParameterFromTempDataValueProvider as the cascading value supplier
  • Automatic registration in AddRazorComponents() with service collection extensions
  • Integration hooks in EndpointHtmlRenderer and TempDataService for initialization and persistence
  • Unit and E2E tests covering basic scenarios

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
src/Components/Components/src/SupplyParameterFromTempDataAttribute.cs New attribute definition inheriting from CascadingParameterAttributeBase
src/Components/Endpoints/src/TempData/ITempDataValueMapper.cs Public interface for TempData value mapping operations
src/Components/Endpoints/src/TempData/TempDataValueMapper.cs Core implementation handling read/write with callback registration
src/Components/Endpoints/src/TempData/SupplyParameterFromTempDataValueProvider.cs ICascadingValueSupplier implementation providing TempData values to components
src/Components/Endpoints/src/TempData/SupplyParameterFromTempDataServiceCollectionExtensions.cs Service registration extension method
src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs Adds initialization hook for TempDataValueMapper
src/Components/Endpoints/src/DependencyInjection/TempDataService.cs Adds persistence hook to invoke registered callbacks
src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs Registers TempDataValueMapper and provider services automatically
src/Components/Endpoints/test/TempData/TempDataValueMapperTest.cs Comprehensive unit tests for the mapper implementation
src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/TempData/TempDataComponent.razor Test component demonstrating usage
src/Components/test/E2ETest/Tests/TempDataCookieTest.cs E2E test for cookie-based TempData provider
src/Components/test/E2ETest/Tests/TempDataSessionStorageTest.cs E2E test for session storage TempData provider
src/Components/Endpoints/src/PublicAPI.Unshipped.txt API surface additions
src/Components/Components/src/PublicAPI.Unshipped.txt API surface additions for attribute
src/Components/Components/src/Microsoft.AspNetCore.Components.csproj Adds InternalsVisibleTo for Endpoints assembly
src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs Visibility modifier updates
src/Components/Endpoints/src/Rendering/EndpointComponentState.cs Visibility modifier updates
src/Framework/App.Runtime/src/CompatibilitySuppressions.xml BOM character removal

dariatiurina and others added 9 commits February 9, 2026 15:10
…bute.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…onents/Pages/TempData/TempDataComponent.razor

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@dotnet-policy-service
Copy link
Contributor

Looks like this PR hasn't been active for some time and the codebase could have been changed in the meantime.
To make sure no conflicting changes have occurred, please rerun validation before merging. You can do this by leaving an /azp run comment here (requires commit rights), or by simply closing and reopening.

@dotnet-policy-service dotnet-policy-service bot added the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Feb 20, 2026
Copy link
Member

@javiercn javiercn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pushing through this.

Overall looks good, but the ITempDataValueMapper in components is a symptom that something is missing here. I've poked a bit around it and I'd like to suggest an alternative shape for the design that I think keeps the same behavior while reducing what we add to Microsoft.AspNetCore.Components and making the pattern more reusable. Happy to discuss further if any of this doesn't seem worthwhile.


The main thing I noticed is that ITempDataValueMapper ends up being a public type in the base Components library solely to serve as a seam between the Components and Endpoints assemblies. It carries a three-method protocol (GetValue, RegisterValueCallback, DeleteValueCallback) where the ordering is implicit, and none of it means anything outside TempData specifically. I wonder if we could avoid putting that in the base library entirely.

My suggestion is to introduce two small general-purpose types instead:

CascadingParameterSubscription — a public abstract class that represents one component's subscription to a cascading value source. It pairs value retrieval with cleanup in a single object:

public abstract class CascadingParameterSubscription : IDisposable
{
    public abstract object? GetCurrentValue();
    public abstract void Dispose();
}

AddCascadingValueSupplier<TAttribute> — a new overload on CascadingValueServiceCollectionExtensions that wires up a scoped ICascadingValueSupplier for any attribute type via a subscribe factory:

public static IServiceCollection AddCascadingValueSupplier<TAttribute>(
    this IServiceCollection serviceCollection,
    Func<IServiceProvider, Func<ComponentState, TAttribute, CascadingParameterInfo, CascadingParameterSubscription>> subscribeFactoryResolver)
    where TAttribute : CascadingParameterAttributeBase

The internal plumbing (CascadingParameterValueProvider<TAttribute>) holds a Dictionary<ComponentState, CascadingParameterSubscription> and just routes the framework's lifecycle calls through to the factory:

void ICascadingValueSupplier.Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
    => _subscriptions[subscriber] = _subscribeFactory(subscriber, (TAttribute)parameterInfo.Attribute, parameterInfo);

object? ICascadingValueSupplier.GetCurrentValue(object? key, in CascadingParameterInfo parameterInfo)
    => key is ComponentState s && _subscriptions.TryGetValue(s, out var sub) ? sub.GetCurrentValue() : null;

void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
    { if (_subscriptions.Remove(subscriber, out var sub)) sub.Dispose(); }

On the Endpoints side, TempDataCascadingValueSupplier (renamed from TempDataValueMapper) doesn't need to implement any interface at all. All the reflection, property getter construction, and HTTP context access stays inside one class in the Endpoints assembly. The only thing it exposes is a CreateSubscription method. The concrete subscription is a small private nested class:

private sealed class TempDataSubscription : CascadingParameterSubscription
{
    public override object? GetCurrentValue() => _owner.GetValue(_key, _propertyType);
    public override void Dispose() => _owner._registeredValues.Remove(_key);
}

I'd also move SupplyParameterFromTempDataAttribute to Microsoft.AspNetCore.Components.Web, since it only makes sense in an HTTP context and the base library targets non-HTTP environments too (Blazor WASM, MAUI Hybrid). The registration then becomes:

services.TryAddScoped<TempDataCascadingValueSupplier>();
services.AddCascadingValueSupplier<SupplyParameterFromTempDataAttribute>(
    sp => sp.GetRequiredService<TempDataCascadingValueSupplier>().CreateSubscription);

A few reasons I think this shape is worth considering:

  • The public API addition to Microsoft.AspNetCore.Components shrinks to just CascadingParameterSubscription and the AddCascadingValueSupplier overload — neither of which is TempData-specific.
  • ITempDataValueMapper and SupplyParameterFromTempDataServiceCollectionExtensions go away entirely.
  • The Components base library no longer needs to know about value callbacks, key deletion, or anything else that belongs to the TempData implementation.
  • Any future per-component cascading parameter attribute (say, [SupplyParameterFromSessionData]) could reuse AddCascadingValueSupplier and CascadingParameterSubscription without needing a new interface, a new extension class, or access to the internal ICascadingValueSupplier.

Let me know what you think!

@dariatiurina dariatiurina removed the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Mar 11, 2026
Copy link
Member

@ilonatommy ilonatommy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks very good. If we really wanted to change something, my test coverage report shows no tests for CreateSubscription.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-blazor Includes: Blazor, Razor Components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TempData design proposal Blazor TempData

4 participants