Skip to content

Commit 263ee71

Browse files
authored
[Blazor] Prerendered state (#50742)
[Blazor] Adds support for persting prerendered state on Blazor Web applications. * Persists state both for server and webassembly as necessary. * Initializes the state when a given interactive runtime is initialized and renders the first set of components. * On WebAssembly, this is the first time the app starts. * On Server this happens every time a circuit starts. * The state is available during the first render, until the components reach quiescence. The approach we follow is different for server and webassembly: * On Server, we support initializing the circuit with an empty set of descriptors and in that case, we delay initialization until the first `UpdateRootComponents` call is issued. * This is because it's hard to deal with the security constraints imposed by starting a new circuit multiple times, and its easier to handle them within UpdateRootComponents. We might switch this approach in the future to go through `StartCircuit` too. * On WebAssembly, we query for the initial set of webassembly components when we are starting the runtime in a Blazor Web Scenario. * We do this because Blazor WebAssembly offers a programatic API to render root components at a given location defined by their selectors, so we need to make sure that those components can receive state at the same time the initial set of WebAssembly components added to the page. There are a set of tests validating different behaviors with regards to enhanced navigation and streaming rendering, as well as making sure that auto mode can access the state on Server and WebAssembly, and that Server gets new state every time a circuit is opened.
1 parent df8ca3e commit 263ee71

File tree

58 files changed

+2583
-641
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+2583
-641
lines changed

src/Components/Components/src/IPersistentComponentStateStore.cs

+7
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,11 @@ public interface IPersistentComponentStateStore
2020
/// <param name="state">The serialized state to persist.</param>
2121
/// <returns>A <see cref="Task" /> that completes when the state is persisted to disk.</returns>
2222
Task PersistStateAsync(IReadOnlyDictionary<string, byte[]> state);
23+
24+
/// <summary>
25+
/// Returns a value that indicates whether the store supports the given <see cref="IComponentRenderMode"/>.
26+
/// </summary>
27+
/// <param name="renderMode">The <see cref="IComponentRenderMode"/> in question.</param>
28+
/// <returns><c>true</c> if the render mode is supported by the store, otherwise <c>false</c>.</returns>
29+
bool SupportsRenderMode(IComponentRenderMode renderMode) => true;
2330
}

src/Components/Components/src/Infrastructure/ComponentStatePersistenceManager.cs

+78-20
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,18 @@ namespace Microsoft.AspNetCore.Components.Infrastructure;
1111
/// </summary>
1212
public class ComponentStatePersistenceManager
1313
{
14+
private readonly List<PersistComponentStateRegistration> _registeredCallbacks = new();
15+
private readonly ILogger<ComponentStatePersistenceManager> _logger;
16+
1417
private bool _stateIsPersisted;
15-
private readonly List<Func<Task>> _pauseCallbacks = new();
1618
private readonly Dictionary<string, byte[]> _currentState = new(StringComparer.Ordinal);
17-
private readonly ILogger<ComponentStatePersistenceManager> _logger;
1819

1920
/// <summary>
2021
/// Initializes a new instance of <see cref="ComponentStatePersistenceManager"/>.
2122
/// </summary>
2223
public ComponentStatePersistenceManager(ILogger<ComponentStatePersistenceManager> logger)
2324
{
24-
State = new PersistentComponentState(_currentState, _pauseCallbacks);
25+
State = new PersistentComponentState(_currentState, _registeredCallbacks);
2526
_logger = logger;
2627
}
2728

@@ -48,43 +49,100 @@ public async Task RestoreStateAsync(IPersistentComponentStateStore store)
4849
/// <param name="renderer">The <see cref="Renderer"/> that components are being rendered.</param>
4950
/// <returns>A <see cref="Task"/> that will complete when the state has been restored.</returns>
5051
public Task PersistStateAsync(IPersistentComponentStateStore store, Renderer renderer)
51-
=> PersistStateAsync(store, renderer.Dispatcher);
52-
53-
/// <summary>
54-
/// Persists the component application state into the given <see cref="IPersistentComponentStateStore"/>.
55-
/// </summary>
56-
/// <param name="store">The <see cref="IPersistentComponentStateStore"/> to restore the application state from.</param>
57-
/// <param name="dispatcher">The <see cref="Dispatcher"/> corresponding to the components' renderer.</param>
58-
/// <returns>A <see cref="Task"/> that will complete when the state has been restored.</returns>
59-
public Task PersistStateAsync(IPersistentComponentStateStore store, Dispatcher dispatcher)
6052
{
6153
if (_stateIsPersisted)
6254
{
6355
throw new InvalidOperationException("State already persisted.");
6456
}
6557

66-
_stateIsPersisted = true;
67-
68-
return dispatcher.InvokeAsync(PauseAndPersistState);
58+
return renderer.Dispatcher.InvokeAsync(PauseAndPersistState);
6959

7060
async Task PauseAndPersistState()
7161
{
7262
State.PersistingState = true;
73-
await PauseAsync();
63+
64+
if (store is IEnumerable<IPersistentComponentStateStore> compositeStore)
65+
{
66+
// We only need to do inference when there is more than one store. This is determined by
67+
// the set of rendered components.
68+
InferRenderModes(renderer);
69+
70+
// Iterate over each store and give it a chance to run against the existing declared
71+
// render modes. After we've run through a store, we clear the current state so that
72+
// the next store can start with a clean slate.
73+
foreach (var store in compositeStore)
74+
{
75+
await PersistState(store);
76+
_currentState.Clear();
77+
}
78+
}
79+
else
80+
{
81+
await PersistState(store);
82+
}
83+
7484
State.PersistingState = false;
85+
_stateIsPersisted = true;
86+
}
7587

88+
async Task PersistState(IPersistentComponentStateStore store)
89+
{
90+
await PauseAsync(store);
7691
await store.PersistStateAsync(_currentState);
7792
}
7893
}
7994

80-
internal Task PauseAsync()
95+
private void InferRenderModes(Renderer renderer)
96+
{
97+
for (var i = 0; i < _registeredCallbacks.Count; i++)
98+
{
99+
var registration = _registeredCallbacks[i];
100+
if (registration.RenderMode != null)
101+
{
102+
// Explicitly set render mode, so nothing to do.
103+
continue;
104+
}
105+
106+
if (registration.Callback.Target is IComponent component)
107+
{
108+
var componentRenderMode = renderer.GetComponentRenderMode(component);
109+
if (componentRenderMode != null)
110+
{
111+
_registeredCallbacks[i] = new PersistComponentStateRegistration(registration.Callback, componentRenderMode);
112+
}
113+
else
114+
{
115+
// If we can't find a render mode, it's an SSR only component and we don't need to
116+
// persist its state at all.
117+
_registeredCallbacks[i] = default;
118+
}
119+
continue;
120+
}
121+
122+
throw new InvalidOperationException(
123+
$"The registered callback {registration.Callback.Method.Name} must be associated with a component or define" +
124+
$" an explicit render mode type during registration.");
125+
}
126+
}
127+
128+
internal Task PauseAsync(IPersistentComponentStateStore store)
81129
{
82130
List<Task>? pendingCallbackTasks = null;
83131

84-
for (var i = 0; i < _pauseCallbacks.Count; i++)
132+
for (var i = 0; i < _registeredCallbacks.Count; i++)
85133
{
86-
var callback = _pauseCallbacks[i];
87-
var result = ExecuteCallback(callback, _logger);
134+
var registration = _registeredCallbacks[i];
135+
136+
if (!store.SupportsRenderMode(registration.RenderMode!))
137+
{
138+
// The callback does not have an associated render mode and we are in a multi-store scenario.
139+
// Otherwise, in a single store scenario, we just run the callback.
140+
// If the registration callback is null, it's because it was associated with a component and we couldn't infer
141+
// its render mode, which means is an SSR only component and we don't need to persist its state at all.
142+
continue;
143+
}
144+
145+
var result = ExecuteCallback(registration.Callback, _logger);
88146
if (!result.IsCompletedSuccessfully)
89147
{
90148
pendingCallbackTasks ??= new();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Components;
5+
6+
internal readonly struct PersistComponentStateRegistration(
7+
Func<Task> callback,
8+
IComponentRenderMode? renderMode)
9+
{
10+
public Func<Task> Callback { get; } = callback;
11+
12+
public IComponentRenderMode? RenderMode { get; } = renderMode;
13+
}

src/Components/Components/src/PersistentComponentState.cs

+17-5
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ public class PersistentComponentState
1515
private IDictionary<string, byte[]>? _existingState;
1616
private readonly IDictionary<string, byte[]> _currentState;
1717

18-
private readonly List<Func<Task>> _registeredCallbacks;
18+
private readonly List<PersistComponentStateRegistration> _registeredCallbacks;
1919

2020
internal PersistentComponentState(
21-
IDictionary<string, byte[]> currentState,
22-
List<Func<Task>> pauseCallbacks)
21+
IDictionary<string , byte[]> currentState,
22+
List<PersistComponentStateRegistration> pauseCallbacks)
2323
{
2424
_currentState = currentState;
2525
_registeredCallbacks = pauseCallbacks;
@@ -43,12 +43,24 @@ internal void InitializeExistingState(IDictionary<string, byte[]> existingState)
4343
/// <param name="callback">The callback to invoke when the application is being paused.</param>
4444
/// <returns>A subscription that can be used to unregister the callback when disposed.</returns>
4545
public PersistingComponentStateSubscription RegisterOnPersisting(Func<Task> callback)
46+
=> RegisterOnPersisting(callback, null);
47+
48+
/// <summary>
49+
/// Register a callback to persist the component state when the application is about to be paused.
50+
/// Registered callbacks can use this opportunity to persist their state so that it can be retrieved when the application resumes.
51+
/// </summary>
52+
/// <param name="callback">The callback to invoke when the application is being paused.</param>
53+
/// <param name="renderMode"></param>
54+
/// <returns>A subscription that can be used to unregister the callback when disposed.</returns>
55+
public PersistingComponentStateSubscription RegisterOnPersisting(Func<Task> callback, IComponentRenderMode? renderMode)
4656
{
4757
ArgumentNullException.ThrowIfNull(callback);
4858

49-
_registeredCallbacks.Add(callback);
59+
var persistenceCallback = new PersistComponentStateRegistration(callback, renderMode);
60+
61+
_registeredCallbacks.Add(persistenceCallback);
5062

51-
return new PersistingComponentStateSubscription(_registeredCallbacks, callback);
63+
return new PersistingComponentStateSubscription(_registeredCallbacks, persistenceCallback);
5264
}
5365

5466
/// <summary>

src/Components/Components/src/PersistingComponentStateSubscription.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ namespace Microsoft.AspNetCore.Components;
1111
/// </summary>
1212
public readonly struct PersistingComponentStateSubscription : IDisposable
1313
{
14-
private readonly List<Func<Task>>? _callbacks;
15-
private readonly Func<Task>? _callback;
14+
private readonly List<PersistComponentStateRegistration>? _callbacks;
15+
private readonly PersistComponentStateRegistration? _callback;
1616

17-
internal PersistingComponentStateSubscription(List<Func<Task>> callbacks, Func<Task> callback)
17+
internal PersistingComponentStateSubscription(List<PersistComponentStateRegistration> callbacks, PersistComponentStateRegistration callback)
1818
{
1919
_callbacks = callbacks;
2020
_callback = callback;
@@ -23,9 +23,9 @@ internal PersistingComponentStateSubscription(List<Func<Task>> callbacks, Func<T
2323
/// <inheritdoc />
2424
public void Dispose()
2525
{
26-
if (_callback != null)
26+
if (_callback.HasValue)
2727
{
28-
_callbacks?.Remove(_callback);
28+
_callbacks?.Remove(_callback.Value);
2929
}
3030
}
3131
}

src/Components/Components/src/PublicAPI.Unshipped.txt

+4-1
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ Microsoft.AspNetCore.Components.CascadingValueSource<TValue>.NotifyChangedAsync(
1616
Microsoft.AspNetCore.Components.CascadingValueSource<TValue>.NotifyChangedAsync(TValue newValue) -> System.Threading.Tasks.Task!
1717
Microsoft.AspNetCore.Components.ComponentBase.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task!
1818
Microsoft.AspNetCore.Components.IComponentRenderMode
19-
Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.PersistStateAsync(Microsoft.AspNetCore.Components.IPersistentComponentStateStore! store, Microsoft.AspNetCore.Components.Dispatcher! dispatcher) -> System.Threading.Tasks.Task!
2019
Microsoft.AspNetCore.Components.InjectAttribute.Key.get -> object?
2120
Microsoft.AspNetCore.Components.InjectAttribute.Key.init -> void
21+
Microsoft.AspNetCore.Components.IPersistentComponentStateStore.SupportsRenderMode(Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> bool
2222
Microsoft.AspNetCore.Components.ParameterView.ToDictionary() -> System.Collections.Generic.IReadOnlyDictionary<string!, object?>!
2323
*REMOVED*Microsoft.AspNetCore.Components.ParameterView.ToDictionary() -> System.Collections.Generic.IReadOnlyDictionary<string!, object!>!
24+
Microsoft.AspNetCore.Components.PersistentComponentState.RegisterOnPersisting(System.Func<System.Threading.Tasks.Task!>! callback, Microsoft.AspNetCore.Components.IComponentRenderMode? renderMode) -> Microsoft.AspNetCore.Components.PersistingComponentStateSubscription
2425
Microsoft.AspNetCore.Components.RenderHandle.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task!
2526
*REMOVED*Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string! relativeUri) -> System.Uri!
2627
Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string? relativeUri) -> System.Uri!
@@ -44,6 +45,7 @@ Microsoft.AspNetCore.Components.RenderTree.NamedEventChangeType
4445
Microsoft.AspNetCore.Components.RenderTree.NamedEventChangeType.Added = 0 -> Microsoft.AspNetCore.Components.RenderTree.NamedEventChangeType
4546
Microsoft.AspNetCore.Components.RenderTree.NamedEventChangeType.Removed = 1 -> Microsoft.AspNetCore.Components.RenderTree.NamedEventChangeType
4647
Microsoft.AspNetCore.Components.RenderTree.RenderBatch.NamedEventChanges.get -> Microsoft.AspNetCore.Components.RenderTree.ArrayRange<Microsoft.AspNetCore.Components.RenderTree.NamedEventChange>?
48+
Microsoft.AspNetCore.Components.RenderTree.Renderer.GetComponentState(Microsoft.AspNetCore.Components.IComponent! component) -> Microsoft.AspNetCore.Components.Rendering.ComponentState!
4749
Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame.ComponentFrameFlags.get -> Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags
4850
Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrameType.ComponentRenderMode = 9 -> Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrameType
4951
Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrameType.NamedEvent = 10 -> Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrameType
@@ -101,6 +103,7 @@ virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.DisposeAsync()
101103
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.AddPendingTask(Microsoft.AspNetCore.Components.Rendering.ComponentState? componentState, System.Threading.Tasks.Task! task) -> void
102104
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.CreateComponentState(int componentId, Microsoft.AspNetCore.Components.IComponent! component, Microsoft.AspNetCore.Components.Rendering.ComponentState? parentComponentState) -> Microsoft.AspNetCore.Components.Rendering.ComponentState!
103105
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.RenderTree.EventFieldInfo? fieldInfo, System.EventArgs! eventArgs, bool waitForQuiescence) -> System.Threading.Tasks.Task!
106+
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.GetComponentRenderMode(Microsoft.AspNetCore.Components.IComponent! component) -> Microsoft.AspNetCore.Components.IComponentRenderMode?
104107
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.ResolveComponentForRenderMode(System.Type! componentType, int? parentComponentId, Microsoft.AspNetCore.Components.IComponentActivator! componentActivator, Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> Microsoft.AspNetCore.Components.IComponent!
105108
~Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame.ComponentRenderMode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode
106109
~Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame.NamedEventAssignedName.get -> string

src/Components/Components/src/RenderTree/Renderer.cs

+14-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,20 @@ private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvid
133133
protected ComponentState GetComponentState(int componentId)
134134
=> GetRequiredComponentState(componentId);
135135

136-
internal ComponentState GetComponentState(IComponent component)
136+
/// <summary>
137+
/// Gets the <see cref="IComponentRenderMode"/> for a given component if available.
138+
/// </summary>
139+
/// <param name="component">The component type</param>
140+
/// <returns></returns>
141+
protected internal virtual IComponentRenderMode? GetComponentRenderMode(IComponent component)
142+
=> null;
143+
144+
/// <summary>
145+
/// Resolves the component state for a given <see cref="IComponent"/> instance.
146+
/// </summary>
147+
/// <param name="component">The <see cref="IComponent"/> instance</param>
148+
/// <returns></returns>
149+
protected internal ComponentState GetComponentState(IComponent component)
137150
=> _componentStateByComponent.GetValueOrDefault(component);
138151

139152
private async void RenderRootComponentsOnHotReload()

0 commit comments

Comments
 (0)