-
Notifications
You must be signed in to change notification settings - Fork 10.4k
[Blazor] Support for declaratively persisting component and services state #60634
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
Changes from all commits
b956382
f1f5cd1
0377a29
8736766
16b72f5
ce89edd
9e64c8a
29dcd96
e14ea2b
24502b5
05ac066
92bdc15
cd3a946
1e78121
5e0215d
5e60fee
f36c7fa
16fe34f
8705221
5ed7280
6ba2eb4
9fc4138
94c6412
c64f605
ef6b70b
d6d276f
2e91b14
1784ad7
1444089
23e00e3
854370a
afabecd
2ef98cd
3717242
44d92fc
adc065e
3478be9
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 |
---|---|---|
|
@@ -13,17 +13,16 @@ | |
namespace Microsoft.AspNetCore.Components; | ||
|
||
internal readonly struct CascadingParameterState | ||
(in CascadingParameterInfo parameterInfo, ICascadingValueSupplier valueSupplier, object? key) | ||
{ | ||
private static readonly ConcurrentDictionary<Type, CascadingParameterInfo[]> _cachedInfos = new(); | ||
|
||
public CascadingParameterInfo ParameterInfo { get; } | ||
public ICascadingValueSupplier ValueSupplier { get; } | ||
public CascadingParameterInfo ParameterInfo { get; } = parameterInfo; | ||
public ICascadingValueSupplier ValueSupplier { get; } = valueSupplier; | ||
public object? Key { get; } = key; | ||
|
||
public CascadingParameterState(in CascadingParameterInfo parameterInfo, ICascadingValueSupplier valueSupplier) | ||
{ | ||
ParameterInfo = parameterInfo; | ||
ValueSupplier = valueSupplier; | ||
} | ||
: this(parameterInfo, valueSupplier, key: null) { } | ||
|
||
public static IReadOnlyList<CascadingParameterState> FindCascadingParameters(ComponentState componentState, out bool hasSingleDeliveryParameters) | ||
{ | ||
|
@@ -55,7 +54,7 @@ public static IReadOnlyList<CascadingParameterState> FindCascadingParameters(Com | |
{ | ||
// Although not all parameters might be matched, we know the maximum number | ||
resultStates ??= new List<CascadingParameterState>(infos.Length - infoIndex); | ||
resultStates.Add(new CascadingParameterState(info, supplier)); | ||
resultStates.Add(new CascadingParameterState(info, supplier, componentState)); | ||
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'm a bit lost here - why would Maybe some other naming would clarify this. 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. Yeah, naming is a bit odd here. I chose that in the beginning because I didn't know what I was going to use, but I think using |
||
|
||
if (info.Attribute.SingleDelivery) | ||
{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,17 +15,35 @@ public class ComponentStatePersistenceManager | |
private readonly ILogger<ComponentStatePersistenceManager> _logger; | ||
|
||
private bool _stateIsPersisted; | ||
private readonly PersistentServicesRegistry? _servicesRegistry; | ||
private readonly Dictionary<string, byte[]> _currentState = new(StringComparer.Ordinal); | ||
|
||
/// <summary> | ||
/// Initializes a new instance of <see cref="ComponentStatePersistenceManager"/>. | ||
/// </summary> | ||
/// <param name="logger"></param> | ||
public ComponentStatePersistenceManager(ILogger<ComponentStatePersistenceManager> logger) | ||
{ | ||
State = new PersistentComponentState(_currentState, _registeredCallbacks); | ||
_logger = logger; | ||
} | ||
|
||
/// <summary> | ||
/// Initializes a new instance of <see cref="ComponentStatePersistenceManager"/>. | ||
/// </summary> | ||
/// <param name="logger"></param> | ||
/// <param name="serviceProvider"></param> | ||
public ComponentStatePersistenceManager(ILogger<ComponentStatePersistenceManager> logger, IServiceProvider serviceProvider) : this(logger) | ||
{ | ||
_servicesRegistry = new PersistentServicesRegistry(serviceProvider); | ||
} | ||
|
||
// For testing purposes only | ||
internal PersistentServicesRegistry? ServicesRegistry => _servicesRegistry; | ||
|
||
// For testing purposes only | ||
internal List<PersistComponentStateRegistration> RegisteredCallbacks => _registeredCallbacks; | ||
|
||
/// <summary> | ||
/// Gets the <see cref="ComponentStatePersistenceManager"/> associated with the <see cref="ComponentStatePersistenceManager"/>. | ||
/// </summary> | ||
|
@@ -40,6 +58,7 @@ public async Task RestoreStateAsync(IPersistentComponentStateStore store) | |
{ | ||
var data = await store.GetPersistedStateAsync(); | ||
State.InitializeExistingState(data); | ||
_servicesRegistry?.Restore(State); | ||
} | ||
|
||
/// <summary> | ||
|
@@ -59,6 +78,9 @@ public Task PersistStateAsync(IPersistentComponentStateStore store, Renderer ren | |
|
||
async Task PauseAndPersistState() | ||
{ | ||
// Ensure that we register the services before we start persisting the state. | ||
_servicesRegistry?.RegisterForPersistence(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. The naming of this makes me suspicious. It sounds like it would create duplicate callbacks every time the circuit is paused and persisted. Do we somehow know this can only get called once? Or do we know that 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 only gets called once at the end of the lifetime of the host (during prerendering in this case, in the future when we "hibernate" the circuit. If in the future there's a situation where this gets called more than once, we can adjust |
||
|
||
State.PersistingState = true; | ||
|
||
if (store is IEnumerable<IPersistentComponentStateStore> compositeStore) | ||
|
@@ -72,24 +94,53 @@ async Task PauseAndPersistState() | |
// the next store can start with a clean slate. | ||
foreach (var store in compositeStore) | ||
{ | ||
await PersistState(store); | ||
var result = await TryPersistState(store); | ||
if (!result) | ||
{ | ||
break; | ||
} | ||
_currentState.Clear(); | ||
} | ||
} | ||
else | ||
{ | ||
await PersistState(store); | ||
await TryPersistState(store); | ||
} | ||
|
||
State.PersistingState = false; | ||
_stateIsPersisted = true; | ||
} | ||
|
||
async Task PersistState(IPersistentComponentStateStore store) | ||
async Task<bool> TryPersistState(IPersistentComponentStateStore store) | ||
{ | ||
await PauseAsync(store); | ||
if (!await TryPauseAsync(store)) | ||
{ | ||
_currentState.Clear(); | ||
return false; | ||
} | ||
|
||
await store.PersistStateAsync(_currentState); | ||
ilonatommy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return true; | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Initializes the render mode for state persisted by the platform. | ||
/// </summary> | ||
/// <param name="renderMode">The render mode to use for state persisted by the platform.</param> | ||
/// <exception cref="InvalidOperationException">when the render mode is already set.</exception> | ||
public void SetPlatformRenderMode(IComponentRenderMode renderMode) | ||
{ | ||
if (_servicesRegistry == null) | ||
{ | ||
return; | ||
} | ||
else if (_servicesRegistry?.RenderMode != null) | ||
{ | ||
throw new InvalidOperationException("Render mode already set."); | ||
} | ||
|
||
_servicesRegistry!.RenderMode = renderMode; | ||
} | ||
|
||
private void InferRenderModes(Renderer renderer) | ||
|
@@ -125,11 +176,17 @@ private void InferRenderModes(Renderer renderer) | |
} | ||
} | ||
|
||
internal Task PauseAsync(IPersistentComponentStateStore store) | ||
internal Task<bool> TryPauseAsync(IPersistentComponentStateStore store) | ||
{ | ||
List<Task>? pendingCallbackTasks = null; | ||
List<Task<bool>>? pendingCallbackTasks = null; | ||
|
||
for (var i = 0; i < _registeredCallbacks.Count; i++) | ||
// We are iterating backwards to allow the callbacks to remove themselves from the list. | ||
// Otherwise, we would have to make a copy of the list to avoid running into situations | ||
// where we don't run all the callbacks because the count of the list changed while we | ||
// were iterating over it. | ||
// It is not allowed to register a callback while we are persisting the state, so we don't | ||
// need to worry about new callbacks being added to the list. | ||
for (var i = _registeredCallbacks.Count - 1; i >= 0; i--) | ||
javiercn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
var registration = _registeredCallbacks[i]; | ||
|
||
|
@@ -142,31 +199,38 @@ internal Task PauseAsync(IPersistentComponentStateStore store) | |
continue; | ||
} | ||
|
||
var result = ExecuteCallback(registration.Callback, _logger); | ||
var result = TryExecuteCallback(registration.Callback, _logger); | ||
if (!result.IsCompletedSuccessfully) | ||
{ | ||
pendingCallbackTasks ??= new(); | ||
pendingCallbackTasks ??= []; | ||
pendingCallbackTasks.Add(result); | ||
} | ||
else | ||
{ | ||
if (!result.Result) | ||
{ | ||
return Task.FromResult(false); | ||
} | ||
} | ||
} | ||
|
||
if (pendingCallbackTasks != null) | ||
{ | ||
return Task.WhenAll(pendingCallbackTasks); | ||
return AnyTaskFailed(pendingCallbackTasks); | ||
} | ||
else | ||
{ | ||
return Task.CompletedTask; | ||
return Task.FromResult(true); | ||
} | ||
|
||
static Task ExecuteCallback(Func<Task> callback, ILogger<ComponentStatePersistenceManager> logger) | ||
static Task<bool> TryExecuteCallback(Func<Task> callback, ILogger<ComponentStatePersistenceManager> logger) | ||
{ | ||
try | ||
{ | ||
var current = callback(); | ||
if (current.IsCompletedSuccessfully) | ||
{ | ||
return current; | ||
return Task.FromResult(true); | ||
} | ||
else | ||
{ | ||
|
@@ -176,21 +240,35 @@ static Task ExecuteCallback(Func<Task> callback, ILogger<ComponentStatePersisten | |
catch (Exception ex) | ||
{ | ||
logger.LogError(new EventId(1000, "PersistenceCallbackError"), ex, "There was an error executing a callback while pausing the application."); | ||
return Task.CompletedTask; | ||
return Task.FromResult(false); | ||
} | ||
|
||
static async Task Awaited(Task task, ILogger<ComponentStatePersistenceManager> logger) | ||
static async Task<bool> Awaited(Task task, ILogger<ComponentStatePersistenceManager> logger) | ||
{ | ||
try | ||
{ | ||
await task; | ||
return true; | ||
} | ||
catch (Exception ex) | ||
{ | ||
logger.LogError(new EventId(1000, "PersistenceCallbackError"), ex, "There was an error executing a callback while pausing the application."); | ||
return; | ||
return false; | ||
} | ||
} | ||
} | ||
|
||
static async Task<bool> AnyTaskFailed(List<Task<bool>> pendingCallbackTasks) | ||
{ | ||
foreach (var result in await Task.WhenAll(pendingCallbackTasks)) | ||
{ | ||
if (!result) | ||
{ | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
} | ||
} |
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.
This is just fixing the solution file that was broken.