-
Notifications
You must be signed in to change notification settings - Fork 10.4k
[Blazor] Add ability to filter persistent component state callbacks based on persistence reason #62394
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
base: main
Are you sure you want to change the base?
[Blazor] Add ability to filter persistent component state callbacks based on persistence reason #62394
Changes from 4 commits
bae3489
2519ee8
0fddad7
f938ea4
4439a8c
a2c9f1c
52e8488
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> | ||
/// Default persistence reason used when no specific reason is provided. | ||
/// </summary> | ||
internal sealed class DefaultPersistenceReason : IPersistenceReason | ||
{ | ||
public static readonly DefaultPersistenceReason Instance = new(); | ||
|
||
private DefaultPersistenceReason() { } | ||
|
||
/// <inheritdoc /> | ||
public bool PersistByDefault => true; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
// 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> | ||
/// Represents a reason for persisting component state. | ||
/// </summary> | ||
public interface IPersistenceReason | ||
{ | ||
/// <summary> | ||
/// Gets a value indicating whether state should be persisted by default for this reason. | ||
/// </summary> | ||
bool PersistByDefault { get; } | ||
} |
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> | ||
/// Filters component state persistence based on the reason for persistence. | ||
/// </summary> | ||
public interface IPersistenceReasonFilter | ||
{ | ||
/// <summary> | ||
/// Determines whether state should be persisted for the given reason. | ||
/// </summary> | ||
/// <param name="reason">The reason for persistence.</param> | ||
/// <returns><c>true</c> to persist state, <c>false</c> to skip persistence, or <c>null</c> to defer to other filters or default behavior.</returns> | ||
bool? ShouldPersist(IPersistenceReason reason); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,9 +5,12 @@ namespace Microsoft.AspNetCore.Components; | |
|
||
internal readonly struct PersistComponentStateRegistration( | ||
Func<Task> callback, | ||
IComponentRenderMode? renderMode) | ||
IComponentRenderMode? renderMode, | ||
IReadOnlyList<IPersistenceReasonFilter>? reasonFilters = null) | ||
{ | ||
public Func<Task> Callback { get; } = callback; | ||
|
||
public IComponentRenderMode? RenderMode { get; } = renderMode; | ||
|
||
public IReadOnlyList<IPersistenceReasonFilter>? ReasonFilters { get; } = reasonFilters; | ||
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.
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. Updated PersistComponentStateRegistration.ReasonFilters property initialization to use the suggested pattern: |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
// 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> | ||
/// Base class for filtering component state persistence based on specific persistence reasons. | ||
/// </summary> | ||
/// <typeparam name="TReason">The type of persistence reason this filter handles.</typeparam> | ||
public abstract class PersistReasonFilter<TReason> : Attribute, IPersistenceReasonFilter | ||
where TReason : IPersistenceReason | ||
{ | ||
private readonly bool _persist; | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="PersistReasonFilter{TReason}"/> class. | ||
/// </summary> | ||
/// <param name="persist">Whether to persist state for the specified reason type.</param> | ||
protected PersistReasonFilter(bool persist) | ||
{ | ||
_persist = persist; | ||
} | ||
|
||
/// <inheritdoc /> | ||
public bool? ShouldPersist(IPersistenceReason reason) | ||
{ | ||
if (reason is TReason) | ||
{ | ||
return _persist; | ||
} | ||
|
||
return null; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,6 +45,30 @@ internal void InitializeExistingState(IDictionary<string, byte[]> existingState) | |
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> | ||
/// <param name="reasonFilters">Filters to control when the callback should be invoked based on the persistence reason.</param> | ||
/// <returns>A subscription that can be used to unregister the callback when disposed.</returns> | ||
public PersistingComponentStateSubscription RegisterOnPersisting(Func<Task> callback, IComponentRenderMode? renderMode, IReadOnlyList<IPersistenceReasonFilter>? reasonFilters) | ||
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. IReadOnlyList? reasonFilters <= Make this mandatory 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. Made the reasonFilters parameter mandatory in the RegisterOnPersisting overload that accepts filters. Updated the legacy overloads to pass Array.Empty<IPersistenceReasonFilter>() when no filters are provided. (a2c9f1c) |
||
{ | ||
ArgumentNullException.ThrowIfNull(callback); | ||
|
||
if (PersistingState) | ||
{ | ||
throw new InvalidOperationException("Registering a callback while persisting state is not allowed."); | ||
} | ||
|
||
var persistenceCallback = new PersistComponentStateRegistration(callback, renderMode, reasonFilters); | ||
|
||
_registeredCallbacks.Add(persistenceCallback); | ||
|
||
return new PersistingComponentStateSubscription(_registeredCallbacks, persistenceCallback); | ||
} | ||
|
||
/// <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. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -68,6 +68,16 @@ 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, DefaultPersistenceReason.Instance); | ||
|
||
/// <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="renderer">The <see cref="Renderer"/> that components are being rendered.</param> | ||
/// <param name="persistenceReason">The reason for persisting the state.</param> | ||
/// <returns>A <see cref="Task"/> that will complete when the state has been restored.</returns> | ||
public Task PersistStateAsync(IPersistentComponentStateStore store, Renderer renderer, IPersistenceReason persistenceReason) | ||
{ | ||
if (_stateIsPersisted) | ||
{ | ||
|
@@ -113,7 +123,7 @@ async Task PauseAndPersistState() | |
|
||
async Task<bool> TryPersistState(IPersistentComponentStateStore store) | ||
{ | ||
if (!await TryPauseAsync(store)) | ||
if (!await TryPauseAsync(store, persistenceReason)) | ||
{ | ||
_currentState.Clear(); | ||
return false; | ||
|
@@ -159,7 +169,7 @@ private void InferRenderModes(Renderer renderer) | |
var componentRenderMode = renderer.GetComponentRenderMode(component); | ||
if (componentRenderMode != null) | ||
{ | ||
_registeredCallbacks[i] = new PersistComponentStateRegistration(registration.Callback, componentRenderMode); | ||
_registeredCallbacks[i] = new PersistComponentStateRegistration(registration.Callback, componentRenderMode, registration.ReasonFilters); | ||
} | ||
else | ||
{ | ||
|
@@ -177,6 +187,9 @@ private void InferRenderModes(Renderer renderer) | |
} | ||
|
||
internal Task<bool> TryPauseAsync(IPersistentComponentStateStore store) | ||
=> TryPauseAsync(store, DefaultPersistenceReason.Instance); | ||
|
||
internal Task<bool> TryPauseAsync(IPersistentComponentStateStore store, IPersistenceReason persistenceReason) | ||
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. Do not create and overload. Simply change this method and adjust all the places it's used 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. Removed the overload from ComponentStatePersistenceManager.PersistStateAsync and modified the existing method to accept an optional nullable IPersistenceReason parameter. Updated all calling sites to work with the new signature. (a2c9f1c) |
||
{ | ||
List<Task<bool>>? pendingCallbackTasks = null; | ||
|
||
|
@@ -199,6 +212,27 @@ internal Task<bool> TryPauseAsync(IPersistentComponentStateStore store) | |
continue; | ||
} | ||
|
||
// Evaluate reason filters to determine if the callback should be executed for this persistence reason | ||
if (registration.ReasonFilters != null) | ||
{ | ||
var shouldPersist = EvaluateReasonFilters(registration.ReasonFilters, persistenceReason); | ||
if (shouldPersist.HasValue && !shouldPersist.Value) | ||
{ | ||
// Filters explicitly indicate not to persist for this reason | ||
continue; | ||
} | ||
else if (!shouldPersist.HasValue && !persistenceReason.PersistByDefault) | ||
{ | ||
// No filter matched and default is not to persist | ||
continue; | ||
} | ||
} | ||
else if (!persistenceReason.PersistByDefault) | ||
{ | ||
// No filters defined and default is not to persist | ||
continue; | ||
} | ||
|
||
var result = TryExecuteCallback(registration.Callback, _logger); | ||
if (!result.IsCompletedSuccessfully) | ||
{ | ||
|
@@ -271,4 +305,19 @@ static async Task<bool> AnyTaskFailed(List<Task<bool>> pendingCallbackTasks) | |
return true; | ||
} | ||
} | ||
|
||
private static bool? EvaluateReasonFilters(IReadOnlyList<IPersistenceReasonFilter> reasonFilters, IPersistenceReason persistenceReason) | ||
{ | ||
foreach (var reasonFilter in reasonFilters) | ||
{ | ||
var shouldPersist = reasonFilter.ShouldPersist(persistenceReason); | ||
if (shouldPersist.HasValue) | ||
{ | ||
return shouldPersist.Value; | ||
} | ||
} | ||
|
||
// No filter matched | ||
return null; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ | |
|
||
<ItemGroup> | ||
<Reference Include="Microsoft.AspNetCore.Components" /> | ||
<Reference Include="Microsoft.AspNetCore.Components.Web" /> | ||
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. Do not add this reference here 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. Removed the Microsoft.AspNetCore.Components.Web reference from the Components.Tests project. Created test-specific implementations of persistence reasons within the test file to maintain test functionality without the dependency. (4439a8c) |
||
<Reference Include="Microsoft.Extensions.DependencyInjection" /> | ||
<Reference Include="Microsoft.Extensions.Diagnostics.Testing" /> | ||
</ItemGroup> | ||
|
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.
Use
null
as a value in legacy overloads (and handle that within PauseAsync) instead of a "default" reasonThere 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.
Replaced DefaultPersistenceReason with null handling in legacy overloads. The ComponentStatePersistenceManager now handles null persistence reasons with default behavior (persist by default). Removed the DefaultPersistenceReason.cs file entirely. (a2c9f1c)