8000 [Blazor] Support for declaratively persisting component and services state by javiercn · Pull Request #60634 · dotnet/aspnetcore · GitHub
[go: up one dir, main page]

Skip to content

[Blazor] Support for declaratively persisting component and services state #60634

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

Merged
merged 37 commits into from
Mar 13, 2025

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.

< 8000 input type="hidden" name="_method" value="put" autocomplete="off" />

@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
@javiercn javiercn force-pushed the javiercn/declarative-persistent-component-state branch 2 times, most recently from 997105b to 245624e Compare March 7, 2025 09:43
@javiercn javiercn force-pushed the javiercn/declarative-persistent-component-state branch from 3c4cc13 to bedbdf0 Compare March 10, 2025 14:05
@javiercn javiercn requested review from wtgodbe and a team as code owners March 10, 2025 14:05
@javiercn javiercn force-pushed the javiercn/declarative-persistent-component-state branch from 1cdfc6c to 3478be9 Compare March 13, 2025 17:33
@javiercn javiercn enabled auto-merge (squash) March 13, 2025 17:49
@javiercn javiercn merged commit 90cc8cd into main Mar 13, 2025
27 checks passed
@javiercn javiercn deleted the javiercn/declarative-persistent-component-state branch March 13, 2025 22:34
@dotnet-policy-service dotnet-policy-service bot added this to the 10.0-preview3 milestone Mar 13, 2025
@BrennanConroy
Copy link
Member

I believe this PR caused a perf regression:
{95C0F0B5-5934-4086-B8EC-C422EEBD2968}
Narrowing the aspnetcore runtime version we used got down to these commits causing the regression e88f8d6...f355564 and this PR is the obvious culprit from that list.

It looks like lock contention potentially increased as well which may be causing the regression:
{9C840F57-49F7-449B-A7A0-BCCA2C6E0A3C}

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.

9 participants
0