Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Blazor] Initial support for persisting component state #60634

Open
wants to merge 19 commits into
base: main
Choose a base branch
from

Conversation

javiercn
Copy link
Member

@javiercn javiercn commented Feb 26, 2025

Adds a declarative model for persistent component and services state

This PR augments the persistent component state feature with a declarative model that allows the developer to place an attribute on components and services properties to indicate that they should be persisted during prerendering so that it is accessible when the application becomes interactive.

Scenarios

Serializing state for a component

@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++;
    }
}
  • Properties annotated with [SupplyParameterFromPersistentComponentState] will be serialized and deserialized during prerendering.

Serializing state for 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; }
    }
}
  • Properties annotated with [SupplyParameterFromPersistentComponentState] will be serialized and deserialized during prerendering.
  • The @key directive is used to ensure that the state is correctly associated with the component instance.
  • The Element property is initialized in the OnInitialized method to avoid null reference exceptions similarly to how we do it for
    query parameters and form data.

Serializing state for a service

CounterService.cs

public class CounterService
{
    [SupplyParameterFromPersistentComponentState]
    public int CurrentCount { get; set; }

    public void IncrementCount()
    {
        CurrentCount++;
    }
}

Program.cs

builder.Services.AddPersistentService<CounterService>(RenderMode.InteractiveAuto);
  • Properties annotated with [SupplyParameterFromPersistentComponentState] will be serialized during prerendering and deserialized when the application becomes interactive.
  • The AddPersistentService method is used to register the service for persistence.
  • The render mode is required as can't be inferred from the service type.
    • RenderMode.Server - The service will be available for interactive server mode.
    • RenderMode.Webassembly - The service will be available for interactive webassembly mode.
    • RenderMode.InteractiveAuto - The service will be available for both interactive server and webassembly modes if a component renders in any of those modes.
  • The service will be resolved during interactive mode initialization and the properties annotated with [SupplyParameterFromPersistentComponentState] will be deserialized.

Implementation details

Key Computation

For components

We need to generate a unique key for each property that needs to be persisted. For components, this key is computed based on:

  • The parent component type
  • The component type
  • The property name
  • The @key directive if present and serializable (e.g., Guid, DateOnly, TimeOnly, and primitive types)

The key computation ensures that even if multiple instances of the same component are present on the page (for example, in a loop), each instance's state can be uniquely identified and persisted.

The key computation algorithm only takes into account a small subset of a component hierarchy for performance reasons. This limits the ability to persist state on recursive component hierarchies. Our recommendation for those scenarios is to persist the state at the top level of the hierarchy.

It's also important to indicate that the imperative API is still available for more advanced scenarios, which offers more flexibility on how to handle the more complex cases.

For services

Only persisting scoped services is supported. We need to generate a unique key for each property that needs to be persisted. The key for services is derived from:

  • The type used to register the persistent service
    • Assembly
    • Full type name
    • Property name

Properties to be serialized are identified from the actual service instance.

  • This approach allows marking an abstraction as a persistent service.
  • Enables actual implementations to be internal or different types.
  • Supports shared code in different assemblies.
  • Each instance must expose the same properties.

Serialization and Deserialization

By default properties are serialized using the System.Text.Json serializer with default settings. Note that this method is not trimmer safe and requires the user to ensure that the types used are preserved through some other means.

This is consistent with our usage of System.Text.Json across other areas of the product, like root component parameters or JSInterop.

We plan to add an extensibility point to control the serialization mechanism used in this scenario in a future change.

@@ -11777,6 +11790,8 @@ Global
{01A75167-DF5A-AF38-8700-C3FBB2C2CFF5} = {225AEDCF-7162-4A86-AC74-06B84660B379}
{E6D564C0-4CA5-411C-BF40-9802AF7900CB} = {01A75167-DF5A-AF38-8700-C3FBB2C2CFF5}
{7899F5DD-AA7C-4561-BAC4-E2EC78B7D157} = {01A75167-DF5A-AF38-8700-C3FBB2C2CFF5}
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {60D51C98-2CC0-40DF-B338-44154EFEE2FF}
{E22DD5A6-06E2-490E-BD32-88D629FD6668} = {60D51C98-2CC0-40DF-B338-44154EFEE2FF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}
Copy link
Member Author

Choose a reason for hiding this comment

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

This is just fixing the solution file that was broken.

Comment on lines 114 to 144
public void SetPlatformRenderMode(IComponentRenderMode renderMode)
{
if (_servicesRegistry == null)
{
return;
}
else if (_servicesRegistry?.RenderMode != null)
{
throw new InvalidOperationException("Render mode already set.");
}

_servicesRegistry!.RenderMode = renderMode;
}
Copy link
Member Author

Choose a reason for hiding this comment

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

Microsoft.AspNetCore.Components doesn't have access to the common render modes (Server,Webassembly,Auto) so we need to give hosts a way to configure the render mode that is used for persisting the PersistentServicesRegistry


namespace Microsoft.AspNetCore.Components.Reflection;

internal sealed class PropertyGetter
Copy link
Member Author

Choose a reason for hiding this comment

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

This is a copy and modification of PropertySetter

Copy link
Member Author

Choose a reason for hiding this comment

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

Comment on lines -374 to -375
_componentStateById.Add(componentId, componentState);
_componentStateByComponent.Add(component, componentState);
Copy link
Member Author

Choose a reason for hiding this comment

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

Moved this into a method that ComponentState calls in its constructor to set this up, so that cascading value providers can access the RenderMode during the initial setup.

@javiercn javiercn marked this pull request as ready for review March 3, 2025 16:51
@javiercn javiercn requested a review from a team as a code owner March 3, 2025 16:51
@javiercn javiercn requested a review from Copilot March 3, 2025 16:51
@javiercn
Copy link
Member Author

javiercn commented Mar 3, 2025

I'm adding more tests, but the current implementation should be working for all mainline scenarios

@javiercn
Copy link
Member Author

javiercn commented Mar 3, 2025

This pull request includes several changes aimed at enhancing the functionality and robustness of the ASP.NET Core components. The most significant changes involve the addition of new projects to the solution, modifications to cascading parameter state management, and improvements to persistent component state handling.

Solution Structure Updates:

  • Added new projects Endpoints and CustomElements to the solution file AspNetCore.sln.
  • Updated the Global section in the solution file to include new project GUID mappings. [1] [2]

Cascading Parameter State Enhancements:

  • Introduced a new constructor for CascadingParameterState to include an optional key parameter.
  • Updated FindCascadingParameters method to use the new constructor with the key parameter.
  • Modified ICascadingValueSupplier to include an overloaded method GetCurrentValue that accepts a key parameter.
  • Adjusted ParameterView to use the updated GetCurrentValue method with the key parameter.

Persistent Component State Improvements:

  • Added new methods to PersistentComponentState for JSON serialization and deserialization with type safety and error handling. [1] [2]
  • Enhanced ComponentStatePersistenceManager to support service registration and state restoration. [1] [2] [3] [4]
  • Introduced new interfaces and classes for persistent component registration and service type caching. [1] [2] [3]

These changes collectively improve the modularity, state management, and persistence capabilities of the ASP.NET Core components, making the framework more robust and easier to extend.

Copy link
Contributor

@Copilot 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.

PR Overview

This PR introduces initial support for persisting component state in Blazor by augmenting the existing persistence mechanism with a declarative model. Key changes include:

  • New infrastructure for generating and caching unique keys for both component and service state.
  • Extension methods and service registrations that integrate persistent state functionality for both components and services.
  • Modifications to persistence state registration, restoration, and callback execution across the rendering and DI pipelines.

Reviewed Changes

File Description
src/Components/Components/src/Reflection/PropertyGetter.cs Introduces a generic property getter using dynamic code support.
src/Components/Components/src/PersistentState/PersistentServiceTypeCache.cs Adds a cache for resolving persistent service types.
src/Components/Components/src/PersistentState/PersistentServicesRegistry.cs Implements service registration/restoration for persistent state.
src/Components/Components/src/SupplyParameterFromPersistentComponentStateProviderServiceCollectionExtensions.cs Provides extension methods for persistent state-related service registration.
src/Components/Components/src/SupplyParameterFromPersistentComponentStateValueProvider.cs Adds a value provider to supply component parameters from persistent state.
src/Components/Components/src/PersistentComponentState.cs Updates the component state management to include persistence callbacks.
src/Components/Components/src/RenderTree/Renderer.cs and others Integrates persistent state behavior with component registration and cascading parameters.
src/Components/Components/test/Lifetime/ComponentStatePersistenceManagerTest.cs Updates tests to reflect the new persistent state service registration.
src/Components/Components/src/ParameterView.cs Adjusts cascading parameter lookup to incorporate a derived key.

Copilot reviewed 30 out of 30 changed files in this pull request and generated 4 comments.

@javiercn javiercn force-pushed the javiercn/declarative-persistent-component-state branch from 1c54a24 to 995d550 Compare March 5, 2025 16:32

private static bool IsSerializableKey(object key) =>
key is { } componentKey && componentKey.GetType() is Type type &&
(Type.GetTypeCode(type) != TypeCode.Object
Copy link
Member

Choose a reason for hiding this comment

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

this pattern is showing up again.

// were iterating over it.
// It is not allowed to register a callback while we are persisting the state, so we don't
// need to worry about new callbacks being added to the list.
for (var i = _registeredCallbacks.Count - 1; i >= 0; i--)
Copy link
Member Author

Choose a reason for hiding this comment

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

Added a test for this

@javiercn javiercn force-pushed the javiercn/declarative-persistent-component-state branch from 995d550 to 997105b Compare March 6, 2025 19:41
@javiercn javiercn force-pushed the javiercn/declarative-persistent-component-state branch from 997105b to 245624e Compare March 7, 2025 09:43
_subscriptions[subscriber] = state.RegisterOnPersisting(() =>
{
var storageKey = ComputeKey(subscriber, propertyName);
var property = subscriber.Component.GetType().GetProperty(propertyName)!.GetValue(subscriber.Component)!;
Copy link
Member Author

Choose a reason for hiding this comment

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

This needs to use PropertyGetter

Copy link
Member Author

Choose a reason for hiding this comment

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

We need a separate cache for these properties too

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.

2 participants