SupplyParameterFromTempData support for Blazor#65306
SupplyParameterFromTempData support for Blazor#65306dariatiurina wants to merge 71 commits intodotnet:mainfrom
Conversation
…etcore into 49683-tempdata
There was a problem hiding this comment.
Does setting the property causes it to be saved to temp data for the next navigation?
There was a problem hiding this comment.
Yes. But reading through this property always goes through Get() so it only persists once.
There was a problem hiding this comment.
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
SupplyParameterFromTempDataAttributein Components assembly for marking properties ITempDataValueMapperinterface andTempDataValueMapperimplementation for managing read/write operationsSupplyParameterFromTempDataValueProvideras the cascading value supplier- Automatic registration in
AddRazorComponents()with service collection extensions - Integration hooks in
EndpointHtmlRendererandTempDataServicefor 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 |
src/Components/Components/src/SupplyParameterFromTempDataAttribute.cs
Outdated
Show resolved
Hide resolved
src/Components/Components/src/SupplyParameterFromTempDataValueProvider.cs
Outdated
Show resolved
Hide resolved
src/Components/test/E2ETest/Tests/TempDataSessionStorageTest.cs
Outdated
Show resolved
Hide resolved
src/Components/Endpoints/src/TempData/SupplyParameterFromTempDataValueProvider.cs
Outdated
Show resolved
Hide resolved
src/Components/test/E2ETest/Tests/TempDataSessionStorageTest.cs
Outdated
Show resolved
Hide resolved
src/Components/Endpoints/src/TempData/SupplyParameterFromTempDataValueProvider.cs
Outdated
Show resolved
Hide resolved
src/Components/Components/src/SupplyParameterFromTempDataServiceCollectionExtensions.cs
Outdated
Show resolved
Hide resolved
...test/testassets/Components.TestServer/RazorComponents/Pages/TempData/TempDataComponent.razor
Outdated
Show resolved
Hide resolved
…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>
src/Components/Endpoints/src/Rendering/EndpointComponentState.cs
Outdated
Show resolved
Hide resolved
|
Looks like this PR hasn't been active for some time and the codebase could have been changed in the meantime. |
javiercn
left a comment
There was a problem hiding this comment.
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 : CascadingParameterAttributeBaseThe 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.Componentsshrinks to justCascadingParameterSubscriptionand theAddCascadingValueSupplieroverload — neither of which is TempData-specific. ITempDataValueMapperandSupplyParameterFromTempDataServiceCollectionExtensionsgo 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 reuseAddCascadingValueSupplierandCascadingParameterSubscriptionwithout needing a new interface, a new extension class, or access to the internalICascadingValueSupplier.
Let me know what you think!
ilonatommy
left a comment
There was a problem hiding this comment.
It looks very good. If we really wanted to change something, my test coverage report shows no tests for CreateSubscription.
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] ITempDataapproach, many scenarios only need simple read/write of a single value. The attribute-based approach:SupplyParameterFrom*attributesTempData["key"]accessDesign
Attribute
Framework abstractions
Two new types enable attribute-driven cascading value suppliers without building a full
ICascadingValueSupplierfrom scratch:CascadingParameterSubscription— an abstract base class representing an active subscription to a cascading parameter:CascadingParameterValueProvider<TAttribute>— an internal genericICascadingValueSupplierthat manages subscriptions for a given attribute type. Consumers provide a factoryFunc<ComponentState, TAttribute, CascadingParameterInfo, CascadingParameterSubscription>and the provider handlesCanSupplyValue,GetCurrentValue,Subscribe, andUnsubscribeby delegating to subscription instances.TryAddCascadingValueSupplier<TAttribute>— a new public extension method onIServiceCollectionthat registers aCascadingParameterValueProvider<TAttribute>usingTryAddEnumerable:TempDataCascadingValueSupplier
The internal
TempDataCascadingValueSupplierclass 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 aTempDataSubscription.GetValue(string, Type)— reads from TempData usingGet()semantics (marks for deletion). Handles enum conversion (int → enum), type mismatches (returnsnull), and deserialization errors (caught, logged, returnsnull).RegisterValueCallback(string, Func<object?>)— stores a getter invoked at persist time. ThrowsInvalidOperationExceptionfor 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
TempDataCascadingValueSupplier.SetRequestContext(HttpContext)is called duringEndpointHtmlRenderer.InitializeStandardComponentServicesAsync, wiring the supplier to the current request.CascadingParameterValueProvider<SupplyParameterFromTempDataAttribute>callsTempDataCascadingValueSupplier.CreateSubscription(), which uses reflection to build a property getter and registers a callback viaRegisterValueCallback().TempDataSubscription.GetCurrentValue()delegates toTempDataCascadingValueSupplier.GetValue(), which reads from TempData (marks for deletion). Enum values stored asintare converted to the target enum type. Type mismatches and deserialization errors are caught and logged, returningnull.TempDataService.Save()is called, it invokesTempDataCascadingValueSupplier.PersistValues(tempData), which iterates all registered callbacks, reads the current property values from the components, and writes them back into TempData.TempDataSubscription.Dispose()callsDeleteValueCallback()to remove the registered callback.Implementation details
StringComparer.OrdinalIgnoreCase.RegisterValueCallbackthrowsInvalidOperationExceptionif a callback is already registered for the same key — multiple components cannot bind to the same TempData key.TempDataCascadingValueSupplierreads TempData fromHttpContext.Items[typeof(ITempData)], the same single instance used by the cascading parameter approach.intare converted viaEnum.ToObject. If the stored value's type is not assignable to the target type,nullis returned instead of throwing.TempDataPersistFail(callback exception during persist) andTempDataDeserializeFail(exception during read).Registration
Automatically enabled when calling
AddRazorComponents():Usage
Basic:
Custom key:
Form with redirect:
Testing
TempDataCascadingValueSupplierTest.cs): 17 tests coveringGetValue(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), andDeleteValueCallback.SupplyParameterFromTempDataReadsAndSavesValuesadded to bothTempDataCookieTestandTempDataSessionStorageTest, verifying read/write roundtrip through the attribute.Out of scope
Peek()andKeep()semantics — use[CascadingParameter] ITempDatafor advanced controlRisks
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