Skip to content

[Blazor] Provide the ability to filter persistent component state callbacks based on the reason for persistence #62393

Open
@javiercn

Description

@javiercn

Support for persisting and restoring component state in a subset of scenarios.

Right now, when a Blazor component supports PersistentComponentState, it unconditionally persists the state.

In .NET 10, we've added support for persisting the state on server disconnections in addition to persisting the state during prerendering.

We are also working to enable persisting state during enhanced navigations. As a result of this, we need to support the ability for the user to enable or disable persisting the state on a per scenario basis.

The scenarios that we have are:

  • Prerendering
  • Server disconnection
  • Enhanced navigation

Each of these scenarios has different requirements. For example, prerendering and disconnection should persist the state by default, while enhanced navigation should persist the state only when the user explicitly opts in.

In the current APIs, the user can use the [SupplyParameterFromPersistentComponentState] attribute to opt in to persisting the state. For example

@page "/example"
<p>My parameter: @MyParameter</p>

@code {
    [SupplyParameterFromPersistentComponentState]
    public string MyParameter { get; set; }

    protected override void OnInitialized()
    {
        MyParameter ??= "Default value";
    }
}

We want to have a way to specify the scenarios in which the state should be persisted. Given that [SupplyParameterFromPersistentComponentState] lives in the Microsoft.AspNetCore.Components dll, which doesn't know anything about the specific scenarios, we need to define an abstraction on this assembly that can be implemented in the Microsoft.AspNetCore.Components.Web dll.

We need to define the scenarios in the Microsoft.AspNetCore.Components.Web assembly, so that components authors can create class libraries that can be used in the different hosting models.

One example of the sample above would look like would be:

@page "/example"
<p>My parameter: @MyParameter</p>

@code {
    [SupplyParameterFromPersistentComponentState]
    [PersistStateOnPrerendering]
    [PersistStateOnServerDisconnection(false)]
    public string MyParameter { get; set; }

    protected override void OnInitialized()
    {
        MyParameter ??= "Default value";
    }
}

The above example would persist the state during prerendering, but not during server disconnection.

We also have an imperative API that can be used to persist component state. Here is an example of how its used:

@page "/example"
@inject PersistentComponentState PersistentComponentState
<p>My parameter: @MyParameter</p>

@code {
    public string MyParameter { get; set; }

    protected override void OnInitialized()
    {
        if(PersistentComponentState.TryGetValue("MyParameter", out var value))
        {
            MyParameter = value;
        }
        else
        {
            MyParameter = "Default value";
        }
        PersistentComponentState.Register(() =>
        {
            PersistentComponentState.PersistAsJson("MyParameter", MyParameter);
        });
    }
}

We also want to support the same scenarios for the imperative API. We need to have a way of passing some registration options when registering the state persistence callback. For example, we could have:

@page "/example"
@inject PersistentComponentState PersistentComponentState
<p>My parameter: @MyParameter</p>

@code {
    public string MyParameter { get; set; }

    protected override void OnInitialized()
    {
        if (PersistentComponentState.TryGetValue("MyParameter", out var value))
        {
            MyParameter = value;
        }
        else
        {
            MyParameter = "Default value";
        }

        PersistentComponentState.Register(() =>
        {
            PersistentComponentState.PersistAsJson("MyParameter", MyParameter);
        }, new PersistentComponentStateRegistrationOptions
        {
            PersistencePolicy = [
              PersistOnPrerenderingAttribute.Enabled,
              PersistOnServerDisconnectionAttribute.Disabled
            ]
        });
    }
}

Now, when it comes to the design. We need to define an IComponentStatePersistencePolicy and an IComponentStatePersistencePolicyEvaluator that gets to evaluate the current policy and determine if the state should be persisted or not.

Each IComponentStatePersistencePolicy has a default value that indicates whether persistence is enabled or disabled by default when there are no filters that apply.

IComponentStatePersistencePolicyEvaluator has a method to check if it supports the current policy and a method that evaluates the policy against the current scenario.

When we evaluate the policy, we iterate through the registered policy evaluators to check if they support the policy by calling SupportsPolicy. If they do, we call ShouldPersist to determine if the policy is enabled or disabled for the current scenario.

Whenever we find the first policy evaluator that supports the policy, we return the result of ShouldPersist. If no evaluators support the policy, we return the default value of the policy.

Implementation steps:

  • Define IComponentStatePersistencePolicy

  • Define IComponentStatePersistencePolicyEvaluator

  • Create concrete policy implementations

    • PrerenderingPersistencePolicy
    • ServerDisconnectionPolicy
    • EnhancedNavigationPolicy
  • Create concrete policy evaluators for the matching policies

    • PersistOnPrerenderingAttribute(bool persist = true)
    • PersistOnDisconnectionAttribute(bool persist = true)
    • PersistOnEnhancedNavigationAttribute(bool persist = true)
  • The interfaces live in Microsoft.AspNetCore.Components

  • The implementations live in Microsoft.AspNetCore.Components.Web

  • Extend the APIs in ComponentStatePersistenceManager to support receiving a policy in PersistAsync.

  • Extend the APIs in PersistentComponentState to support receiving a registration options in the Register method.

  • Update the PersistentComponentStateSubscription to store the policy evaluators.

  • Update ComponentStatePersistenceManager to evaluate the policies when persisting the state.

  • Update SupplyParameterFromPersistentComponentStateValueProvider to collect the policy evaluators from the PropertyInfo and pass them to the Register call when subscribing to the state persistence.

  • Do not modify public APIs, instead add new APIs when needed (for example, additional overloads).

  • Do not add overloads to non-public APIs, modify the existing APIs to support the new scenarios and adjust the existing code to use the new APIs.

  • All the policies should use the singleton pattern with a public static property to access the instance.

  • PolicyEvaluators should also use the singleton pattern with two public static properties to access the instances for the enabled and disabled policies.

Metadata

Metadata

Assignees

Labels

area-blazorIncludes: Blazor, Razor Components

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions