SupplyParameterFromTempData support for Blazor#65306
SupplyParameterFromTempData support for Blazor#65306dariatiurina merged 76 commits intodotnet:mainfrom
Conversation
…etcore into 49683-tempdata
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.
javiercn
left a comment
There was a problem hiding this comment.
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.
….com/dariatiurina/aspnetcore into 49683-supply-parameter-from-tempdata
| /// <returns>The <see cref="IServiceCollection"/>.</returns> | ||
| public static IServiceCollection TryAddCascadingValueSupplier<TAttribute>( | ||
| this IServiceCollection serviceCollection, | ||
| Func<IServiceProvider, Func<ComponentState, TAttribute, CascadingParameterInfo, CascadingParameterSubscription>> subscribeFactory) |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Sounds reasonable. Thanks for the explanation 👍
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— a new public 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 keyed by(ComponentState, PropertyName).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 (PropertyGetter) to create a property getter for the decorated property, registers a value callback, and returns aTempDataSubscription.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).TempDataSubscription
The nested
TempDataSubscriptionclass (insideTempDataCascadingValueSupplier) implements value retrieval directly:GetCurrentValue(): reads from TempData via_owner.GetTempData()usingITempData.Get()semantics (marks for deletion). Handles enum conversion (int→ enum viaEnum.ToObject), type mismatches (returnsnull), and deserialization errors (caught, logged, returnsnull).GetCurrentValue(): returns the component's current property value via the stored getter, avoiding overriding modifications the component made during rendering.Lifecycle
TempDataCascadingValueSupplier.SetRequestContext(HttpContext)is called duringEndpointHtmlRenderer.InitializeStandardComponentServicesAsync, wiring the supplier to the current request.CascadingParameterValueProvider<TAttribute>callsTempDataCascadingValueSupplier.CreateSubscription(), which uses reflection to build a property getter and registers a callback viaRegisterValueCallback().TempDataSubscription.GetCurrentValue()reads from TempData usingGet()semantics (marks for deletion). Enum values stored asintare converted to the target enum type. Type mismatches and deserialization errors are caught and logged, returningnull.GetCurrentValue()returns the component's current property value to avoid overwriting changes made by the component during rendering.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.TempDataCascadingValueSupplier.GetTempData()reads theITempDatainstance fromHttpContext.Itemsusing the staticTempDataProviderServiceCollectionExtensions.HttpContextItemKeysentinel — 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, 8 tests): CoversRegisterValueCallback(add, duplicate key guard),PersistValues(single/multiple keys, null values, callback exceptions, case-insensitivity), andDeleteValueCallback.TempDataSubscriptionTest.cs, 13 tests): CoversGetCurrentValue(null cases, missing keys, case-insensitivity, enum conversion, nullable enum, type mismatches, deserialization errors, first-delivery semantics), andCreateSubscriptionintegration with the supplier.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