Description
Overview and Motivation
Currently, Blazor's persistent component state feature requires imperative API calls to store and retrieve state during prerendering. This proposal introduces a declarative model using attributes to simplify state persistence, allowing developers to annotate properties in components and services that should be persisted during prerendering and restored when the application becomes interactive.
Proposed API
namespace Microsoft.AspNetCore.Components
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class SupplyParameterFromPersistentComponentStateAttribute : Attribute
{
public SupplyParameterFromPersistentComponentStateAttribute() { }
}
}
namespace Microsoft.AspNetCore.Components.Infrastructure
{
public class ComponentStatePersistenceManager
{
public ComponentStatePersistenceManager(ILogger<ComponentStatePersistenceManager> logger, IServiceProvider serviceProvider) { }
public void SetPlatformRenderMode(IComponentRenderMode renderMode) { }
}
public static class RegisterPersistentComponentStateServiceCollectionExtensions
{
public static IServiceCollection AddPersistentServiceRegistration<TService>(
this IServiceCollection services,
IComponentRenderMode componentRenderMode) { }
}
}
namespace Microsoft.Extensions.DependencyInjection
{
public static class SupplyParameterFromPersistentComponentStateProviderServiceCollectionExtensions
{
public static IServiceCollection AddSupplyValueFromPersistentComponentStateProvider(
this IServiceCollection services) { }
}
public static class RazorComponentsRazorComponentBuilderExtensions
{
public static IRazorComponentsBuilder RegisterPersistentService<TPersistentService>(
this IRazorComponentsBuilder builder,
IComponentRenderMode renderMode) { }
}
}
Usage Examples
Component State Persistence
@page "/counter"
<h1>Counter</h1>
<p>Current count: @CurrentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
[SupplyParameterFromPersistentComponentState]
private int CurrentCount { get; set; }
private void IncrementCount()
{
CurrentCount++;
}
}
Multiple Components of the Same Type
<!-- ParentComponent.razor -->
@page "/parent"
@foreach (var element in elements)
{
<ChildComponent @key="element.Name" />
}
<!-- ChildComponent.razor -->
<div>
<p>Current count: @Element.CurrentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</div>
@code {
[SupplyParameterFromPersistentComponentState]
public State Element { get; set; }
private void IncrementCount()
{
Element.CurrentCount++;
}
protected override void OnInitialized()
{
Element ??= new State();
}
private class State
{
public int CurrentCount { get; set; }
}
}
Service State Persistence
public class CounterService
{
[SupplyParameterFromPersistentComponentState]
public int CurrentCount { get; set; }
public void IncrementCount()
{
CurrentCount++;
}
}
builder.Services.AddPersistentService<CounterService>(RenderMode.InteractiveAuto);
Alternative Designs
The existing imperative API for state persistence offers more flexibility for complex scenarios, but is more verbose and error-prone. The declarative model provides a simpler approach for common scenarios while still allowing fallback to the imperative API when needed.
Risk Considerations
-
Serialization Limitations: By default, System.Text.Json is used for serialization which is not trimmer-safe. Developers must ensure that types being serialized are preserved through other means.
-
Key Computation Constraints: The key computation algorithm only considers a subset of the component hierarchy, limiting its use in deeply nested or recursive component hierarchies.
-
Service Persistence Scope: Only scoped services are supported for persistence, and implementations must expose the same properties for consistent serialization.
-
Type Compatibility: Developers must ensure that types marked for persistence can be properly serialized and deserialized by the default serializer.