-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Persist Prerendered State #50373
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 #50373
Changes from all commits
1c416bb
576529b
fb8ef7e
1d6a1e7
a84bd39
8d10aff
4ac7ec3
ecbad34
8d36369
b84048e
f002b24
5af5309
e48cd1c
8071ac2
4b71007
ddcabf0
f13b535
7bbfde0
414b461
87fee27
1e5de57
bbf34b0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
// 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; | ||
|
||
/// <summary> | ||
/// A service that can infer <see cref="IComponent"/>'s <see cref="PersistedStateSerializationMode"/>. | ||
/// </summary> | ||
public interface ISerializationModeHandler | ||
{ | ||
/// <summary> | ||
/// Infers <see cref="IComponent"/>'s <see cref="PersistedStateSerializationMode"/>. | ||
/// </summary> | ||
/// <param name="callbackTarget">The callback target</param> | ||
/// <returns>The <see cref="PersistedStateSerializationMode"/> for the component.</returns> | ||
public PersistedStateSerializationMode GetCallbackTargetSerializationMode(object? callbackTarget); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,25 +11,33 @@ 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<PersistenceCallback> _registeredCallbacks = new(); | ||
private readonly ILogger<ComponentStatePersistenceManager> _logger; | ||
|
||
/// <summary> | ||
/// Initializes a new instance of <see cref="ComponentStatePersistenceManager"/>. | ||
/// </summary> | ||
public ComponentStatePersistenceManager(ILogger<ComponentStatePersistenceManager> logger) | ||
public ComponentStatePersistenceManager(ILogger<ComponentStatePersistenceManager> logger, ISerializationModeHandler serializationModeHandler) | ||
{ | ||
State = new PersistentComponentState(_currentState, _pauseCallbacks); | ||
_logger = logger; | ||
|
||
State = new(_registeredCallbacks, serializationModeHandler); | ||
} | ||
|
||
/// <summary> | ||
/// Gets the <see cref="ComponentStatePersistenceManager"/> associated with the <see cref="ComponentStatePersistenceManager"/>. | ||
/// </summary> | ||
public PersistentComponentState State { get; } | ||
|
||
/// <summary> | ||
/// | ||
/// </summary> | ||
/// <param name="serializationModeHandler"></param> | ||
public void SetSerializationModeHandler(ISerializationModeHandler serializationModeHandler) | ||
{ | ||
State.SetSerializationModeHandler(serializationModeHandler); | ||
} | ||
|
||
/// <summary> | ||
/// Restores the component application state from the given <see cref="IPersistentComponentStateStore"/>. | ||
/// </summary> | ||
|
@@ -44,47 +52,48 @@ public async Task RestoreStateAsync(IPersistentComponentStateStore store) | |
/// <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="store">The <see cref="IPersistentComponentStateStore"/> to persist the application state into.</param> | ||
/// <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"/>. | ||
/// Persists the component application state into the given <see cref="IPersistentComponentStateStore"/> | ||
/// so that it could be restored on Server. | ||
/// </summary> | ||
/// <param name="store">The <see cref="IPersistentComponentStateStore"/> to restore the application state from.</param> | ||
/// <param name="store">The <see cref="IPersistentComponentStateStore"/> to persist the application state into.</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) | ||
{ | ||
throw new InvalidOperationException("State already persisted."); | ||
} | ||
|
||
_stateIsPersisted = true; | ||
|
||
return dispatcher.InvokeAsync(PauseAndPersistState); | ||
|
||
async Task PauseAndPersistState() | ||
{ | ||
State.PersistingState = true; | ||
await PauseAsync(); | ||
State.PersistingState = false; | ||
var currentState = new Dictionary<string, byte[]>(); | ||
|
||
await store.PersistStateAsync(_currentState); | ||
State.PersistenceContext = new(currentState); | ||
await PauseAsync(store); | ||
State.PersistenceContext = default; | ||
Comment on lines
+76
to
+78
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can avoid all this if we do things a bit differently.
|
||
|
||
await store.PersistStateAsync(currentState); | ||
} | ||
} | ||
|
||
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 callback = _registeredCallbacks[i]; | ||
if (!store.SupportsSerializationMode(callback.SerializationMode)) | ||
{ | ||
continue; | ||
} | ||
|
||
var result = ExecuteCallback(callback.Callback, _logger); | ||
if (!result.IsCompletedSuccessfully) | ||
{ | ||
pendingCallbackTasks ??= new(); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Components; | |
public enum PersistedStateSerializationMode | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Decided to use the existing enum |
||
{ | ||
/// <summary> | ||
/// Indicates that the serialization mode should be inferred from the current request context. | ||
/// Indicates that the serialization mode should be inferred. | ||
/// </summary> | ||
Infer = 1, | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can and should avoid baking in the "server vs webassembly" distinction at this level - see #50373 (comment) |
||
|
@@ -22,4 +22,9 @@ public enum PersistedStateSerializationMode | |
/// Indicates that the state should be persisted so that execution may resume on WebAssembly. | ||
/// </summary> | ||
WebAssembly = 3, | ||
|
||
/// <summary> | ||
/// Indicates that the state should be persisted so that execution may resume on both Server and WebAssembly. | ||
/// </summary> | ||
ServerAndWebAssembly = 4, | ||
} |
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 PersistenceCallback(Func<Task> Callback, PersistedStateSerializationMode SerializationMode); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is only used inside PersistentComponentState, it's better to nest it inside that type. Having it as a top-level type adds unnecessary noise to the Microsoft.AspNetCore.Components namespace. |
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment as above, these types that are used just for structured data should be inside the class that uses them. You can also dispense creating a separate type when all you are holding is a single piece of data. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,29 +13,29 @@ namespace Microsoft.AspNetCore.Components; | |
public class PersistentComponentState | ||
{ | ||
private IDictionary<string, byte[]>? _existingState; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need _existingState anymore? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need |
||
private readonly IDictionary<string, byte[]> _currentState; | ||
|
||
private readonly List<Func<Task>> _registeredCallbacks; | ||
private readonly List<PersistenceCallback> _registeredCallbacks; | ||
private ISerializationModeHandler _serializationModeHandler; | ||
|
||
internal PersistentComponentState( | ||
IDictionary<string, byte[]> currentState, | ||
List<Func<Task>> pauseCallbacks) | ||
internal PersistenceContext? PersistenceContext { get; set; } | ||
|
||
internal PersistentComponentState(List<PersistenceCallback> registeredCallbacks, ISerializationModeHandler serializationModeHandler) | ||
{ | ||
_currentState = currentState; | ||
_registeredCallbacks = pauseCallbacks; | ||
_registeredCallbacks = registeredCallbacks; | ||
_serializationModeHandler = serializationModeHandler; | ||
} | ||
|
||
internal bool PersistingState { get; set; } | ||
|
||
internal void InitializeExistingState(IDictionary<string, byte[]> existingState) | ||
{ | ||
if (_existingState != null) | ||
{ | ||
throw new InvalidOperationException("PersistentComponentState already initialized."); | ||
} | ||
// The existing state is either Server or WebAssembly | ||
_existingState = existingState ?? throw new ArgumentNullException(nameof(existingState)); | ||
} | ||
|
||
internal void SetSerializationModeHandler(ISerializationModeHandler serializationModeHandler) | ||
{ | ||
_serializationModeHandler = serializationModeHandler; | ||
} | ||
|
||
/// <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. | ||
|
@@ -46,9 +46,26 @@ public PersistingComponentStateSubscription RegisterOnPersisting(Func<Task> call | |
{ | ||
ArgumentNullException.ThrowIfNull(callback); | ||
|
||
_registeredCallbacks.Add(callback); | ||
var serializationMode = _serializationModeHandler.GetCallbackTargetSerializationMode(callback.Target); | ||
|
||
return new PersistingComponentStateSubscription(_registeredCallbacks, callback); | ||
return RegisterOnPersisting(callback, serializationMode); | ||
} | ||
|
||
/// <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="serializationMode">The <see cref="PersistedStateSerializationMode"/> to register the callback.</param> | ||
/// <returns>A subscription that can be used to unregister the callback when disposed.</returns> | ||
public PersistingComponentStateSubscription RegisterOnPersisting(Func<Task> callback, PersistedStateSerializationMode serializationMode) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should probably be |
||
{ | ||
ArgumentNullException.ThrowIfNull(callback); | ||
|
||
var persistenceCallback = new PersistenceCallback(callback, serializationMode); | ||
_registeredCallbacks.Add(persistenceCallback); | ||
|
||
return new PersistingComponentStateSubscription(_registeredCallbacks, persistenceCallback); | ||
} | ||
|
||
/// <summary> | ||
|
@@ -60,19 +77,17 @@ public PersistingComponentStateSubscription RegisterOnPersisting(Func<Task> call | |
[RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] | ||
public void PersistAsJson<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string key, TValue instance) | ||
{ | ||
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> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need this interface? Can this be done in a different way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason for creating this interface is that it was impossible to reference
EndpointHtmlRenderer
inPersistentComponentState
. Alternative solution was to add some render mode specific absctract methods to theRenderer
and then override them inEndpointHtmlRenderer
which didn't feel right.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you checked-out Steve's spike? It doesn't require those.
That sounds desirable over a separate interface that needs to be implemented in many places.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ISerializationModeHandler
also solves the issue when the code start to run on the circuit or webassembly runtime. When this happens render mode for the components is not available. The reason is that circuit/webassembly runtime uses it own scope and the Renderer's dictionary for component states is not populated. More about this in this comment