diff --git a/activate.sh b/activate.sh
old mode 100644
new mode 100755
diff --git a/src/Components/Components/src/IPersistentComponentStateScenario.cs b/src/Components/Components/src/IPersistentComponentStateScenario.cs
new file mode 100644
index 000000000000..2d67c2a86a81
--- /dev/null
+++ b/src/Components/Components/src/IPersistentComponentStateScenario.cs
@@ -0,0 +1,16 @@
+// 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;
+
+///
+/// Represents a scenario for persistent component state restoration.
+///
+public interface IPersistentComponentStateScenario
+{
+ ///
+ /// Gets a value indicating whether callbacks for this scenario can be invoked multiple times.
+ /// If false, callbacks are automatically unregistered after first invocation.
+ ///
+ bool IsRecurring { get; }
+}
\ No newline at end of file
diff --git a/src/Components/Components/src/IPersistentStateFilter.cs b/src/Components/Components/src/IPersistentStateFilter.cs
new file mode 100644
index 000000000000..44c33faaad29
--- /dev/null
+++ b/src/Components/Components/src/IPersistentStateFilter.cs
@@ -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;
+
+///
+/// Defines filtering logic for persistent component state restoration.
+///
+public interface IPersistentStateFilter
+{
+ ///
+ /// Determines whether state should be restored for the given scenario.
+ ///
+ /// The restoration scenario.
+ /// True if state should be restored; otherwise false.
+ bool ShouldRestore(IPersistentComponentStateScenario scenario);
+}
\ No newline at end of file
diff --git a/src/Components/Components/src/PersistentComponentState.cs b/src/Components/Components/src/PersistentComponentState.cs
index a3dd2fdddc81..936f74cb7ec9 100644
--- a/src/Components/Components/src/PersistentComponentState.cs
+++ b/src/Components/Components/src/PersistentComponentState.cs
@@ -16,6 +16,7 @@ public class PersistentComponentState
private readonly IDictionary _currentState;
private readonly List _registeredCallbacks;
+ private readonly List _restoringCallbacks = new();
internal PersistentComponentState(
IDictionary currentState,
@@ -27,6 +28,11 @@ internal PersistentComponentState(
internal bool PersistingState { get; set; }
+ ///
+ /// Gets the current restoration scenario, if any.
+ ///
+ public IPersistentComponentStateScenario? CurrentScenario { get; internal set; }
+
internal void InitializeExistingState(IDictionary existingState)
{
if (_existingState != null)
@@ -155,6 +161,73 @@ internal bool TryTakeFromJson(string key, [DynamicallyAccessedMembers(JsonSerial
}
}
+ ///
+ /// Registers a callback to be invoked when state is restored and the filter allows the current scenario.
+ ///
+ /// The filter to determine if the callback should be invoked for a scenario.
+ /// The callback to invoke during restoration.
+ /// A subscription that can be disposed to unregister the callback.
+ public RestoringComponentStateSubscription RegisterOnRestoring(
+ IPersistentStateFilter filter,
+ Action callback)
+ {
+ ArgumentNullException.ThrowIfNull(filter);
+ ArgumentNullException.ThrowIfNull(callback);
+
+ var registration = new RestoreComponentStateRegistration(filter, callback);
+ _restoringCallbacks.Add(registration);
+
+ // If we already have a current scenario and the filter matches, invoke immediately
+ if (CurrentScenario != null && filter.ShouldRestore(CurrentScenario))
+ {
+ callback();
+ }
+
+ return new RestoringComponentStateSubscription(_restoringCallbacks, filter, callback);
+ }
+
+ ///
+ /// Updates the existing state with new state for subsequent restoration calls.
+ /// Only allowed when existing state is empty (fully consumed).
+ ///
+ /// New state dictionary to replace existing state.
+ /// The restoration scenario context.
+ internal void UpdateExistingState(IDictionary newState, IPersistentComponentStateScenario scenario)
+ {
+ ArgumentNullException.ThrowIfNull(newState);
+ ArgumentNullException.ThrowIfNull(scenario);
+
+ if (_existingState != null && _existingState.Count > 0)
+ {
+ throw new InvalidOperationException("Cannot update existing state when state dictionary is not empty. State must be fully consumed before updating.");
+ }
+
+ _existingState = newState;
+ CurrentScenario = scenario;
+
+ // Invoke matching restoration callbacks
+ InvokeRestoringCallbacks(scenario);
+ }
+
+ private void InvokeRestoringCallbacks(IPersistentComponentStateScenario scenario)
+ {
+ for (int i = _restoringCallbacks.Count - 1; i >= 0; i--)
+ {
+ var registration = _restoringCallbacks[i];
+
+ if (registration.Filter.ShouldRestore(scenario))
+ {
+ registration.Callback();
+
+ // Remove callback if scenario is not recurring (one-time scenarios)
+ if (!scenario.IsRecurring)
+ {
+ _restoringCallbacks.RemoveAt(i);
+ }
+ }
+ }
+ }
+
private bool TryTake(string key, out byte[]? value)
{
ArgumentNullException.ThrowIfNull(key);
diff --git a/src/Components/Components/src/PersistentState/ComponentStatePersistenceManager.cs b/src/Components/Components/src/PersistentState/ComponentStatePersistenceManager.cs
index 72c1ca666411..e9bba77856db 100644
--- a/src/Components/Components/src/PersistentState/ComponentStatePersistenceManager.cs
+++ b/src/Components/Components/src/PersistentState/ComponentStatePersistenceManager.cs
@@ -17,6 +17,7 @@ public class ComponentStatePersistenceManager
private bool _stateIsPersisted;
private readonly PersistentServicesRegistry? _servicesRegistry;
private readonly Dictionary _currentState = new(StringComparer.Ordinal);
+ private bool _isFirstRestore = true;
///
/// Initializes a new instance of .
@@ -55,9 +56,43 @@ public ComponentStatePersistenceManager(ILoggerThe to restore the application state from.
/// A that will complete when the state has been restored.
public async Task RestoreStateAsync(IPersistentComponentStateStore store)
+ {
+ await RestoreStateAsync(store, scenario: null);
+ }
+
+ ///
+ /// Restores component state from the given store with scenario context.
+ ///
+ /// The store to restore state from.
+ /// The restoration scenario context.
+ /// A task that completes when state restoration is finished.
+ public async Task RestoreStateAsync(
+ IPersistentComponentStateStore store,
+ IPersistentComponentStateScenario? scenario)
{
var data = await store.GetPersistedStateAsync();
- State.InitializeExistingState(data);
+
+ if (_isFirstRestore)
+ {
+ // First-time initialization
+ State.InitializeExistingState(data);
+ _isFirstRestore = false;
+ }
+ else
+ {
+ // Scenario-based update - only if we have a scenario
+ if (scenario != null)
+ {
+ State.UpdateExistingState(data, scenario);
+ }
+ else
+ {
+ // This is a second call without a scenario, which should fail
+ // (maintaining the original behavior for backward compatibility)
+ throw new InvalidOperationException("PersistentComponentState already initialized.");
+ }
+ }
+
_servicesRegistry?.Restore(State);
}
diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt
index 33bf7c236923..94c2438ca8a6 100644
--- a/src/Components/Components/src/PublicAPI.Unshipped.txt
+++ b/src/Components/Components/src/PublicAPI.Unshipped.txt
@@ -21,3 +21,14 @@ static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCo
static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsTracing(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
static Microsoft.Extensions.DependencyInjection.SupplyParameterFromPersistentComponentStateProviderServiceCollectionExtensions.AddSupplyValueFromPersistentComponentStateProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.GetComponentKey() -> object?
+Microsoft.AspNetCore.Components.IPersistentComponentStateScenario
+Microsoft.AspNetCore.Components.IPersistentComponentStateScenario.IsRecurring.get -> bool
+Microsoft.AspNetCore.Components.IPersistentStateFilter
+Microsoft.AspNetCore.Components.IPersistentStateFilter.ShouldRestore(Microsoft.AspNetCore.Components.IPersistentComponentStateScenario! scenario) -> bool
+Microsoft.AspNetCore.Components.PersistentComponentState.CurrentScenario.get -> Microsoft.AspNetCore.Components.IPersistentComponentStateScenario?
+*REMOVED*Microsoft.AspNetCore.Components.PersistentComponentState.RegisterOnRestoring(Microsoft.AspNetCore.Components.IPersistentComponentStateScenario! scenario, System.Action! callback) -> Microsoft.AspNetCore.Components.RestoringComponentStateSubscription
+Microsoft.AspNetCore.Components.PersistentComponentState.RegisterOnRestoring(Microsoft.AspNetCore.Components.IPersistentStateFilter! filter, System.Action! callback) -> Microsoft.AspNetCore.Components.RestoringComponentStateSubscription
+Microsoft.AspNetCore.Components.RestoringComponentStateSubscription
+Microsoft.AspNetCore.Components.RestoringComponentStateSubscription.RestoringComponentStateSubscription() -> void
+Microsoft.AspNetCore.Components.RestoringComponentStateSubscription.Dispose() -> void
+Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.RestoreStateAsync(Microsoft.AspNetCore.Components.IPersistentComponentStateStore! store, Microsoft.AspNetCore.Components.IPersistentComponentStateScenario? scenario) -> System.Threading.Tasks.Task!
diff --git a/src/Components/Components/src/Reflection/PropertyGetter.cs b/src/Components/Components/src/Reflection/PropertyGetter.cs
index 03fa596cbc5c..e4f2289f0dd4 100644
--- a/src/Components/Components/src/Reflection/PropertyGetter.cs
+++ b/src/Components/Components/src/Reflection/PropertyGetter.cs
@@ -14,12 +14,16 @@ internal sealed class PropertyGetter
private readonly Func