Skip to content

API review for support persistent component state on enhanced navigation #62773

Open
@javiercn

Description

@javiercn

Background and Motivation

We want to support choosing when properties annotated with PersistentComponentState should be restored as this is required for supporting PCS in enhanced navigation scenarios.

  • Prerendering and Resume work by default.

    • Apps might want to restore state only during prerendering or only during resume.
  • Enhanced navigation updates are off by default

    • They can override existing user state, which is not good.
    • The app needs to opt-in to receive updates for individual values.
  • There is a new RestoreContext and IRestoreContextFilter pair of types that is used to support choosing when we should skip restoring a value.

  • The logic is that if a subscription contains a filter that applies to a context the value is filtered according to the filter, otherwise the value is restored.

    • Disabling restore on circuit resume should not interfere with restore on prerendering or enhanced navigation.

Proposed API

Microsoft.AspNetCore.Components

namespace Microsoft.AspNetCore.Components.Infrastructure
{
    public class ComponentStatePersistenceManager
    {
+        public Task RestoreStateAsync(IPersistentComponentStateStore store, RestoreContext restoreContext);
    }
}
namespace Microsoft.AspNetCore.Components
{
    public class PersistentComponentState
    {
+        public RestoringComponentStateSubscription RegisterOnRestoring(Action callback, params Span<IRestoreContextFilter> filters);
    }

+    public abstract class RestoreContext
+    {
+        bool IsRecurring { get; }
+    }

+    public interface IRestoreContextFilter
+    {
+        bool ShouldRestore(IRestoreContext scenario);
+        bool SupportsScenario(IRestoreContext scenario);
+    }

+    public class RestoringComponentStateSubscription : IDisposable
+    {
+        public RestoringComponentStateSubscription();
+        public void Dispose();
+    }
}

Microsoft.AspNetCore.Components.Web

+ namespace Microsoft.AspNetCore.Components
+ {
+    public class WebRestoreContextFilter : IRestoreContextFilter
+    {
+        public static WebRestoreContextFilter Prerendering { get; }
+        public static WebRestoreContextFilter Reconnection { get; }
+        public static WebRestoreContextFilter EnhancedNavigation { get; }
+        public bool ShouldRestore(IRestoreContext scenario);
+        public bool SupportsScenario(IRestoreContext scenario);
+    }

+    public class WebRestoreContext : IRestoreContext
+    {
+        public static WebRestoreContext Prerendering { get; }
+        public static WebRestoreContext Reconnection { get; }
+        public static WebRestoreContext EnhancedNavigation { get; }
+        public bool IsRecurring { get; }
+    }

+    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
+    public sealed class RestoreStateOnPrerenderingAttribute : Attribute
+    {
+        public RestoreStateOnPrerenderingAttribute(bool enabled = true);
+    }

+    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
+    public sealed class RestoreStateOnReconnectionAttribute : Attribute
+    {
+        public RestoreStateOnReconnectionAttribute(bool enabled = true);
+    }

+    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
+    public sealed class UpdateStateOnEnhancedNavigation : Attribute
+    {
+        public UpdateStateOnEnhancedNavigation(bool enabled = false);
+    }

Usage Examples

@page "/clientfetchdata/"
@using BlazorUnitedApp.Client.Data
@using Microsoft.AspNetCore.Components.Web
@inject ClientWeatherForecastService ForecastService

<PageTitle>Weather forecast</PageTitle>

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from a service.</p>

@if (Forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in Forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    [PersistentState]
    [UpdateStateOnEnhancedNavigation(true)]
    public ClientWeatherForecast[]? Forecasts { get; set; }

    public string Page { get; set; } = "";

    protected override async Task OnInitializedAsync()
    {
        Forecasts ??= await ForecastService.GetForecastAsync(DateOnly.FromDateTime(DateTime.Now));
    }

Alternative Designs

Fold enhanced navigation into RestoreStateOnPrerendering and do not have enabled/disabled ctor, and instead only provide an attribute for non-defaults.

  • public sealed class DisableRestoreStateOnResumeAttribute
  • public sealed class DisableRestoreStateOnPrerenderingAttribute
  • public sealed class RestoreStateOnPrerendering(UpdateOnEnhancedNavigation = true)

Open questions:

  • Make some names more "agnostic"?
    • HostStart vs "Prerendering".
    • HostRestart vs "Resume".
    • AllowUpdates/ReceiveUpdates vs "UpdateOnEnhancedNavigation".

WHY MAKE NAMES MORE AGNOSTIC?

  • We don't want references to circuit, prerendering, enhanced navigation and so on to pollute the Microsoft.AspNetCore.Components assembly. Choosing more agnostic terms allows us to avoid those problematic terms and potentially simplify the design and implementation in the following ways:
    • Instead of attributes, we can have properties on PersistentState. HostStart, HostResume, AllowUpdates.
    • All the types in Components.Web can live on Components.
    • We can think of a day where we "resume" the host on other context. For example, if we also start supporting tearing down and restarting wasm apps on demand, or if we support an auto policy where we tear down the server components and restart them on wasm without reload.

Risks

N/A

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-needs-workAPI needs work before it is approved, it is NOT ready for implementationapi-ready-for-reviewAPI is ready for formal API review - https://github.com/dotnet/apireviewsarea-blazorIncludes: Blazor, Razor Components

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions