Skip to content

SupplyParameterFromTempData support for Blazor#65306

Merged
dariatiurina merged 76 commits intodotnet:mainfrom
dariatiurina:49683-supply-parameter-from-tempdata
Apr 17, 2026
Merged

SupplyParameterFromTempData support for Blazor#65306
dariatiurina merged 76 commits intodotnet:mainfrom
dariatiurina:49683-supply-parameter-from-tempdata

Conversation

@dariatiurina
Copy link
Copy Markdown
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 — a new public 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 keyed by (ComponentState, PropertyName).

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 (PropertyGetter) to create a property getter for the decorated property, registers a value callback, and returns a TempDataSubscription.
  • 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).

TempDataSubscription

The nested TempDataSubscription class (inside TempDataCascadingValueSupplier) implements value retrieval directly:

  • First call to GetCurrentValue(): reads from TempData via _owner.GetTempData() using ITempData.Get() semantics (marks for deletion). Handles enum conversion (int → enum via Enum.ToObject), type mismatches (returns null), and deserialization errors (caught, logged, returns null).
  • Subsequent calls to GetCurrentValue(): returns the component's current property value via the stored getter, avoiding overriding modifications the component made during rendering.

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<TAttribute> calls TempDataCascadingValueSupplier.CreateSubscription(), which uses reflection to build a property getter and registers a callback via RegisterValueCallback().
  3. First delivery: TempDataSubscription.GetCurrentValue() reads from TempData using Get() semantics (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. Subsequent reads: After the first delivery, GetCurrentValue() returns the component's current property value to avoid overwriting changes made by the component during rendering.
  5. 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.
  6. 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.GetTempData() reads the ITempData instance from HttpContext.Items using the static TempDataProviderServiceCollectionExtensions.HttpContextItemKey sentinel — 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="SetMessage" @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, 8 tests): Covers RegisterValueCallback (add, duplicate key guard), PersistValues (single/multiple keys, null values, callback exceptions, case-insensitivity), and DeleteValueCallback.
  • Unit tests (TempDataSubscriptionTest.cs, 13 tests): Covers GetCurrentValue (null cases, missing keys, case-insensitivity, enum conversion, nullable enum, type mismatches, deserialization errors, first-delivery semantics), and CreateSubscription integration with the supplier.
  • 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

@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
Copy Markdown
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
Copy Markdown
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.

Comment thread src/Components/Endpoints/src/TempData/TempDataCascadingValueSupplier.cs Outdated
Comment thread src/Components/Endpoints/src/TempData/TempDataCascadingValueSupplier.cs Outdated
Comment thread src/Components/Endpoints/src/TempData/TempDataCascadingValueSupplier.cs Outdated
Comment thread src/Components/Endpoints/src/TempData/TempDataCascadingValueSupplier.cs Outdated
Comment thread src/Components/Endpoints/src/TempData/TempDataCascadingValueSupplier.cs Outdated
Copy link
Copy Markdown
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.

Looks great so far!

I have some smaller comments, and I need to take a look at some of the unit tests, but otherwise, it looks good.

@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 Mar 28, 2026
@dariatiurina dariatiurina removed the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Apr 16, 2026
/// <returns>The <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection TryAddCascadingValueSupplier<TAttribute>(
this IServiceCollection serviceCollection,
Func<IServiceProvider, Func<ComponentState, TAttribute, CascadingParameterInfo, CascadingParameterSubscription>> subscribeFactory)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a public API and a top-level IServiceCollection extension that uses ComponentState in its signature. While ComponentState is also public, it is in the Rendering namespace and is explicitly documented as being an internal implementation detail of Renderer. Is that intentional? (@javiercn)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's a trade-off between this and having to put TempData and all other similar concepts in Components. The main consumers for this are other libraries on the stack, and while I don't love it. The alternative of having to put the attribute on a core assembly and a bunch of the infrastructure there, is worse.

The key tension here is that hosts, lack the ability to define their own host specific cascading values that aren't top level (AddCascadingValue exists, but it's a single value (like we do for HttpContext))

We can review what we use componentstate for (we use it as a key) and consider doing something else (like wrapping it into a struct and call it something like StateKey) but I don't think we get much out of it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The tradeoff is what we do in persistent component state and what we would need to also do in SupplyParameterFromSession, so we will end up with N host specific method registrations in the core assembly. With this approach, the hosts can register them in their setups using this common method.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sounds reasonable. Thanks for the explanation 👍

Comment thread src/Components/Endpoints/src/TempData/TempDataCascadingValueSupplier.cs Outdated
Comment thread src/Components/Endpoints/src/TempData/TempDataCascadingValueSupplier.cs Outdated
@dariatiurina dariatiurina enabled auto-merge (squash) April 17, 2026 15:23
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

5 participants