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

Persist Prerendered State #50625

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,11 @@ public interface IPersistentComponentStateStore
/// <param name="state">The serialized state to persist.</param>
/// <returns>A <see cref="Task" /> that completes when the state is persisted to disk.</returns>
Task PersistStateAsync(IReadOnlyDictionary<string, byte[]> state);

/// <summary>
/// Returns a value that indicates whether the store supports the given <see cref="IComponentRenderMode"/>.
/// </summary>
/// <param name="renderMode">The <see cref="IComponentRenderMode"/> in question.</param>
/// <returns><c>true</c> if the render mode is supported by the store, otherwise <c>false</c>.</returns>
bool SupportsRenderMode(IComponentRenderMode renderMode) => true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,15 @@ namespace Microsoft.AspNetCore.Components.Infrastructure;
/// </summary>
public class ComponentStatePersistenceManager
{
private bool _stateIsPersisted;
private readonly List<Func<Task>> _pauseCallbacks = new();
private readonly Dictionary<string, byte[]> _currentState = new(StringComparer.Ordinal);
private readonly List<RegistrationContext> _registeredCallbacks = new();
private readonly ILogger<ComponentStatePersistenceManager> _logger;

/// <summary>
/// Initializes a new instance of <see cref="ComponentStatePersistenceManager"/>.
/// </summary>
public ComponentStatePersistenceManager(ILogger<ComponentStatePersistenceManager> logger)
{
State = new PersistentComponentState(_currentState, _pauseCallbacks);
State = new PersistentComponentState(_registeredCallbacks);
_logger = logger;
}

Expand All @@ -48,43 +46,74 @@ public async Task RestoreStateAsync(IPersistentComponentStateStore store)
/// <param name="renderer">The <see cref="Renderer"/> that components are being rendered.</param>
/// <returns>A <see cref="Task"/> that will complete when the state has been restored.</returns>
public Task PersistStateAsync(IPersistentComponentStateStore store, Renderer renderer)
=> PersistStateAsync(store, renderer.Dispatcher);

/// <summary>
/// Persists the component application state into the given <see cref="IPersistentComponentStateStore"/>.
/// </summary>
/// <param name="store">The <see cref="IPersistentComponentStateStore"/> to restore the application state from.</param>
/// <param name="dispatcher">The <see cref="Dispatcher"/> corresponding to the components' renderer.</param>
/// <returns>A <see cref="Task"/> that will complete when the state has been restored.</returns>
public Task PersistStateAsync(IPersistentComponentStateStore store, Dispatcher dispatcher)
{
if (_stateIsPersisted)
return renderer.Dispatcher.InvokeAsync(PauseAndPersistState);

async Task PauseAndPersistState()
{
throw new InvalidOperationException("State already persisted.");
InferRenderModes(renderer);

if (store is IEnumerable<IPersistentComponentStateStore> compositeStore)
{
foreach (var store in compositeStore)
{
await PersistState(store);
}
}
else
{
await PersistState(store);
}
}

_stateIsPersisted = true;
async Task PersistState(IPersistentComponentStateStore store)
{
var currentState = new Dictionary<string, byte[]>();
State.PersistenceContext = new(currentState);

return dispatcher.InvokeAsync(PauseAndPersistState);
await PauseAsync(store);
await store.PersistStateAsync(currentState);
}
}

async Task PauseAndPersistState()
private void InferRenderModes(Renderer renderer)
{
for (var i = 0; i < _registeredCallbacks.Count; i++)
{
State.PersistingState = true;
await PauseAsync();
State.PersistingState = false;
var registration = _registeredCallbacks[i];
if (registration.RenderMode != null)
{
// Explicitly set render mode, so nothing to do.
continue;
}

await store.PersistStateAsync(_currentState);
if (registration.Callback.Target is IComponent component)
{
var componentRenderMode = renderer.GetComponentRenderMode(component);
_registeredCallbacks[i] = new RegistrationContext(registration.Callback, componentRenderMode);
continue;
}

throw new InvalidOperationException(
$"The registered callback {registration.Callback.Method.Name} must be associated with a component or define" +
$" an explicit render mode type during registration.");
}
}

internal Task PauseAsync()
internal Task PauseAsync(IPersistentComponentStateStore store)
{
List<Task>? pendingCallbackTasks = null;

for (var i = 0; i < _pauseCallbacks.Count; i++)
for (var i = 0; i < _registeredCallbacks.Count; i++)
{
var callback = _pauseCallbacks[i];
var result = ExecuteCallback(callback, _logger);
var registration = _registeredCallbacks[i];

if (!store.SupportsRenderMode(registration.RenderMode!))
{
continue;
}

var result = ExecuteCallback(registration.Callback, _logger);
if (!result.IsCompletedSuccessfully)
{
pendingCallbackTasks ??= new();
Expand Down
6 changes: 6 additions & 0 deletions src/Components/Components/src/PersistenceContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Components;

internal readonly record struct PersistenceContext(IDictionary<string, byte[]> State);
33 changes: 21 additions & 12 deletions src/Components/Components/src/PersistentComponentState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,17 @@ namespace Microsoft.AspNetCore.Components;
public class PersistentComponentState
{
private IDictionary<string, byte[]>? _existingState;
private readonly IDictionary<string, byte[]> _currentState;

private readonly List<Func<Task>> _registeredCallbacks;
internal PersistenceContext? PersistenceContext { get; set; }

private readonly List<RegistrationContext> _registeredCallbacks;

internal PersistentComponentState(
IDictionary<string, byte[]> currentState,
List<Func<Task>> pauseCallbacks)
List<RegistrationContext> pauseCallbacks)
{
_currentState = currentState;
_registeredCallbacks = pauseCallbacks;
}

internal bool PersistingState { get; set; }

internal void InitializeExistingState(IDictionary<string, byte[]> existingState)
{
if (_existingState != null)
Expand All @@ -43,12 +40,24 @@ internal void InitializeExistingState(IDictionary<string, byte[]> existingState)
/// <param name="callback">The callback to invoke when the application is being paused.</param>
/// <returns>A subscription that can be used to unregister the callback when disposed.</returns>
public PersistingComponentStateSubscription RegisterOnPersisting(Func<Task> callback)
=> RegisterOnPersisting(callback, null);

/// <summary>
/// Register a callback to persist the component state when the application is about to be paused.
/// Registered callbacks can use this opportunity to persist their state so that it can be retrieved when the application resumes.
/// </summary>
/// <param name="callback">The callback to invoke when the application is being paused.</param>
/// <param name="renderMode"></param>
/// <returns>A subscription that can be used to unregister the callback when disposed.</returns>
public PersistingComponentStateSubscription RegisterOnPersisting(Func<Task> callback, IComponentRenderMode? renderMode)
{
ArgumentNullException.ThrowIfNull(callback);

_registeredCallbacks.Add(callback);
var persistenceCallback = new RegistrationContext(callback, renderMode);

_registeredCallbacks.Add(persistenceCallback);

return new PersistingComponentStateSubscription(_registeredCallbacks, callback);
return new PersistingComponentStateSubscription(_registeredCallbacks, persistenceCallback);
}

/// <summary>
Expand All @@ -62,17 +71,17 @@ public PersistingComponentStateSubscription RegisterOnPersisting(Func<Task> call
{
ArgumentNullException.ThrowIfNull(key);

if (!PersistingState)
if (PersistenceContext is not { State: var currentState })
{
throw new InvalidOperationException("Persisting state is only allowed during an OnPersisting callback.");
}

if (_currentState.ContainsKey(key))
if (currentState.ContainsKey(key))
{
throw new ArgumentException($"There is already a persisted object under the same key '{key}'");
}

_currentState.Add(key, JsonSerializer.SerializeToUtf8Bytes(instance, JsonSerializerOptionsProvider.Options));
currentState.Add(key, JsonSerializer.SerializeToUtf8Bytes(instance, JsonSerializerOptionsProvider.Options));
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ namespace Microsoft.AspNetCore.Components;
/// </summary>
public readonly struct PersistingComponentStateSubscription : IDisposable
{
private readonly List<Func<Task>>? _callbacks;
private readonly Func<Task>? _callback;
private readonly List<RegistrationContext>? _callbacks;
private readonly RegistrationContext? _callback;

internal PersistingComponentStateSubscription(List<Func<Task>> callbacks, Func<Task> callback)
internal PersistingComponentStateSubscription(List<RegistrationContext> callbacks, RegistrationContext callback)
{
_callbacks = callbacks;
_callback = callback;
Expand All @@ -23,9 +23,9 @@ internal PersistingComponentStateSubscription(List<Func<Task>> callbacks, Func<T
/// <inheritdoc />
public void Dispose()
{
if (_callback != null)
if (_callback.HasValue)
{
_callbacks?.Remove(_callback);
_callbacks?.Remove(_callback.Value);
}
}
}
5 changes: 4 additions & 1 deletion src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ Microsoft.AspNetCore.Components.CascadingValueSource<TValue>.NotifyChangedAsync(
Microsoft.AspNetCore.Components.CascadingValueSource<TValue>.NotifyChangedAsync(TValue newValue) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Components.ComponentBase.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Components.IComponentRenderMode
Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.PersistStateAsync(Microsoft.AspNetCore.Components.IPersistentComponentStateStore! store, Microsoft.AspNetCore.Components.Dispatcher! dispatcher) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Components.InjectAttribute.Key.get -> object?
Microsoft.AspNetCore.Components.InjectAttribute.Key.init -> void
Microsoft.AspNetCore.Components.IPersistentComponentStateStore.SupportsRenderMode(Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> bool
Microsoft.AspNetCore.Components.ParameterView.ToDictionary() -> System.Collections.Generic.IReadOnlyDictionary<string!, object?>!
*REMOVED*Microsoft.AspNetCore.Components.ParameterView.ToDictionary() -> System.Collections.Generic.IReadOnlyDictionary<string!, object!>!
Microsoft.AspNetCore.Components.PersistentComponentState.RegisterOnPersisting(System.Func<System.Threading.Tasks.Task!>! callback, Microsoft.AspNetCore.Components.IComponentRenderMode? renderMode) -> Microsoft.AspNetCore.Components.PersistingComponentStateSubscription
Microsoft.AspNetCore.Components.RenderHandle.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task!
*REMOVED*Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string! relativeUri) -> System.Uri!
Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string? relativeUri) -> System.Uri!
Expand All @@ -44,6 +45,7 @@ Microsoft.AspNetCore.Components.RenderTree.NamedEventChangeType
Microsoft.AspNetCore.Components.RenderTree.NamedEventChangeType.Added = 0 -> Microsoft.AspNetCore.Components.RenderTree.NamedEventChangeType
Microsoft.AspNetCore.Components.RenderTree.NamedEventChangeType.Removed = 1 -> Microsoft.AspNetCore.Components.RenderTree.NamedEventChangeType
Microsoft.AspNetCore.Components.RenderTree.RenderBatch.NamedEventChanges.get -> Microsoft.AspNetCore.Components.RenderTree.ArrayRange<Microsoft.AspNetCore.Components.RenderTree.NamedEventChange>?
Microsoft.AspNetCore.Components.RenderTree.Renderer.GetComponentState(Microsoft.AspNetCore.Components.IComponent! component) -> Microsoft.AspNetCore.Components.Rendering.ComponentState!
Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame.ComponentFrameFlags.get -> Microsoft.AspNetCore.Components.RenderTree.ComponentFrameFlags
Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrameType.ComponentRenderMode = 9 -> Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrameType
Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrameType.NamedEvent = 10 -> Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrameType
Expand Down Expand Up @@ -101,6 +103,7 @@ virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.DisposeAsync()
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.AddPendingTask(Microsoft.AspNetCore.Components.Rendering.ComponentState? componentState, System.Threading.Tasks.Task! task) -> void
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!
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.RenderTree.EventFieldInfo? fieldInfo, System.EventArgs! eventArgs, bool waitForQuiescence) -> System.Threading.Tasks.Task!
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.GetComponentRenderMode(Microsoft.AspNetCore.Components.IComponent! component) -> Microsoft.AspNetCore.Components.IComponentRenderMode?
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!
~Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame.ComponentRenderMode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode
~Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame.NamedEventAssignedName.get -> string
Expand Down
6 changes: 6 additions & 0 deletions src/Components/Components/src/RegistrationContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Components;

internal readonly record struct RegistrationContext(Func<Task> Callback, IComponentRenderMode? RenderMode);
15 changes: 14 additions & 1 deletion src/Components/Components/src/RenderTree/Renderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,20 @@ private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvid
protected ComponentState GetComponentState(int componentId)
=> GetRequiredComponentState(componentId);

internal ComponentState GetComponentState(IComponent component)
/// <summary>
/// Gets the <see cref="IComponentRenderMode"/> for a given component if available.
/// </summary>
/// <param name="component">The component type</param>
/// <returns></returns>
protected internal virtual IComponentRenderMode? GetComponentRenderMode(IComponent component)
=> null;

/// <summary>
///
/// </summary>
/// <param name="component"></param>
/// <returns></returns>
protected internal ComponentState GetComponentState(IComponent component)
=> _componentStateByComponent.GetValueOrDefault(component);

private async void RenderRootComponentsOnHotReload()
Expand Down
Loading