Skip to content

Commit e19a454

Browse files
committed
[Blazor] Implement scenario-based persistent state filtering for Blazor
* Adds support for ignoring persisted values during prerendering * Adds support for ignoring persisted values during resume * Adds support for receiving value updates during enhanced navigation Fixes #51584
1 parent 71edbbc commit e19a454

File tree

60 files changed

+3078
-1082
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+3078
-1082
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
using Microsoft.AspNetCore.Components.Rendering;
6+
7+
namespace Microsoft.AspNetCore.Components.Infrastructure;
8+
9+
[DebuggerDisplay("{GetDebuggerDisplay(),nq}")]
10+
internal readonly struct ComponentSubscriptionKey(ComponentState subscriber, string propertyName) : IEquatable<ComponentSubscriptionKey>
11+
{
12+
public ComponentState Subscriber { get; } = subscriber;
13+
14+
public string PropertyName { get; } = propertyName;
15+
16+
public bool Equals(ComponentSubscriptionKey other)
17+
=> Subscriber == other.Subscriber && PropertyName == other.PropertyName;
18+
19+
public override bool Equals(object? obj)
20+
=> obj is ComponentSubscriptionKey other && Equals(other);
21+
22+
public override int GetHashCode()
23+
=> HashCode.Combine(Subscriber, PropertyName);
24+
25+
private string GetDebuggerDisplay()
26+
=> $"{Subscriber.Component.GetType().Name}.{PropertyName}";
27+
}

src/Components/Components/src/PersistentComponentState.cs

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics;
45
using System.Diagnostics.CodeAnalysis;
56
using System.Text.Json;
67
using static Microsoft.AspNetCore.Internal.LinkerFlags;
@@ -16,24 +17,30 @@ public class PersistentComponentState
1617
private readonly IDictionary<string, byte[]> _currentState;
1718

1819
private readonly List<PersistComponentStateRegistration> _registeredCallbacks;
20+
private readonly List<RestoreComponentStateRegistration> _registeredRestoringCallbacks;
1921

2022
internal PersistentComponentState(
21-
IDictionary<string , byte[]> currentState,
22-
List<PersistComponentStateRegistration> pauseCallbacks)
23+
IDictionary<string, byte[]> currentState,
24+
List<PersistComponentStateRegistration> pauseCallbacks,
25+
List<RestoreComponentStateRegistration> restoringCallbacks)
2326
{
2427
_currentState = currentState;
2528
_registeredCallbacks = pauseCallbacks;
29+
_registeredRestoringCallbacks = restoringCallbacks;
2630
}
2731

2832
internal bool PersistingState { get; set; }
2933

30-
internal void InitializeExistingState(IDictionary<string, byte[]> existingState)
34+
internal RestoreContext CurrentContext { get; private set; } = RestoreContext.InitialValue;
35+
36+
internal void InitializeExistingState(IDictionary<string, byte[]> existingState, RestoreContext context)
3137
{
3238
if (_existingState != null)
3339
{
3440
throw new InvalidOperationException("PersistentComponentState already initialized.");
3541
}
3642
_existingState = existingState ?? throw new ArgumentNullException(nameof(existingState));
43+
CurrentContext = context;
3744
}
3845

3946
/// <summary>
@@ -68,6 +75,30 @@ public PersistingComponentStateSubscription RegisterOnPersisting(Func<Task> call
6875
return new PersistingComponentStateSubscription(_registeredCallbacks, persistenceCallback);
6976
}
7077

78+
/// <summary>
79+
/// Register a callback to restore the state when the application state is being restored.
80+
/// </summary>
81+
/// <param name="callback"> The callback to invoke when the application state is being restored.</param>
82+
/// <param name="options">Options that control the restoration behavior.</param>
83+
/// <returns>A subscription that can be used to unregister the callback when disposed.</returns>
84+
public RestoringComponentStateSubscription RegisterOnRestoring(Action callback, RestoreOptions options)
85+
{
86+
Debug.Assert(CurrentContext != null);
87+
if (CurrentContext.ShouldRestore(options))
88+
{
89+
callback();
90+
}
91+
92+
if (options.AllowUpdates)
93+
{
94+
var registration = new RestoreComponentStateRegistration(callback);
95+
_registeredRestoringCallbacks.Add(registration);
96+
return new RestoringComponentStateSubscription(_registeredRestoringCallbacks, registration);
97+
}
98+
99+
return default;
100+
}
101+
71102
/// <summary>
72103
/// Serializes <paramref name="instance"/> as JSON and persists it under the given <paramref name="key"/>.
73104
/// </summary>
@@ -214,4 +245,17 @@ private bool TryTake(string key, out byte[]? value)
214245
return false;
215246
}
216247
}
248+
249+
internal void UpdateExistingState(IDictionary<string, byte[]> state, RestoreContext context)
250+
{
251+
ArgumentNullException.ThrowIfNull(state);
252+
253+
if (_existingState == null || _existingState.Count > 0)
254+
{
255+
throw new InvalidOperationException("Cannot update existing state: previous state has not been cleared or state is not initialized.");
256+
}
257+
258+
_existingState = state;
259+
CurrentContext = context;
260+
}
217261
}

src/Components/Components/src/PersistentState/ComponentStatePersistenceManager.cs

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ namespace Microsoft.AspNetCore.Components.Infrastructure;
1212
public class ComponentStatePersistenceManager
1313
{
1414
private readonly List<PersistComponentStateRegistration> _registeredCallbacks = new();
15+
private readonly List<RestoreComponentStateRegistration> _registeredRestoringCallbacks = new();
1516
private readonly ILogger<ComponentStatePersistenceManager> _logger;
1617

1718
private bool _stateIsPersisted;
19+
private bool _stateIsInitialized;
1820
private readonly PersistentServicesRegistry? _servicesRegistry;
1921
private readonly Dictionary<string, byte[]> _currentState = new(StringComparer.Ordinal);
2022

@@ -24,7 +26,7 @@ public class ComponentStatePersistenceManager
2426
/// <param name="logger"></param>
2527
public ComponentStatePersistenceManager(ILogger<ComponentStatePersistenceManager> logger)
2628
{
27-
State = new PersistentComponentState(_currentState, _registeredCallbacks);
29+
State = new PersistentComponentState(_currentState, _registeredCallbacks, _registeredRestoringCallbacks);
2830
_logger = logger;
2931
}
3032

@@ -55,10 +57,38 @@ public ComponentStatePersistenceManager(ILogger<ComponentStatePersistenceManager
5557
/// <param name="store">The <see cref="IPersistentComponentStateStore"/> to restore the application state from.</param>
5658
/// <returns>A <see cref="Task"/> that will complete when the state has been restored.</returns>
5759
public async Task RestoreStateAsync(IPersistentComponentStateStore store)
60+
{
61+
await RestoreStateAsync(store, RestoreContext.InitialValue);
62+
}
63+
64+
/// <summary>
65+
/// Restores the application state.
66+
/// </summary>
67+
/// <param name="store"> The <see cref="IPersistentComponentStateStore"/> to restore the application state from.</param>
68+
/// <param name="context">The <see cref="RestoreContext"/> that provides additional context for the restoration.</param>
69+
/// <returns>A <see cref="Task"/> that will complete when the state has been restored.</returns>
70+
public async Task RestoreStateAsync(IPersistentComponentStateStore store, RestoreContext context)
5871
{
5972
var data = await store.GetPersistedStateAsync();
60-
State.InitializeExistingState(data);
61-
_servicesRegistry?.Restore(State);
73+
74+
if (_stateIsInitialized)
75+
{
76+
if (context != RestoreContext.ValueUpdate)
77+
{
78+
throw new InvalidOperationException("State already initialized.");
79+
}
80+
State.UpdateExistingState(data, context);
81+
foreach (var registration in _registeredRestoringCallbacks)
82+
{
83+
registration.Callback();
84+
}
85+
}
86+
else
87+
{
88+
State.InitializeExistingState(data, context);
89+
_servicesRegistry?.RegisterForPersistence(State);
90+
_stateIsInitialized = true;
91+
}
6292
}
6393

6494
/// <summary>
@@ -78,9 +108,6 @@ public Task PersistStateAsync(IPersistentComponentStateStore store, Renderer ren
78108

79109
async Task PauseAndPersistState()
80110
{
81-
// Ensure that we register the services before we start persisting the state.
82-
_servicesRegistry?.RegisterForPersistence(State);
83-
84111
State.PersistingState = true;
85112

86113
if (store is IEnumerable<IPersistentComponentStateStore> compositeStore)
@@ -271,4 +298,5 @@ static async Task<bool> AnyTaskFailed(List<Task<bool>> pendingCallbackTasks)
271298
return true;
272299
}
273300
}
301+
274302
}

src/Components/Components/src/PersistentState/PersistentServicesRegistry.cs

Lines changed: 41 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ namespace Microsoft.AspNetCore.Components.Infrastructure;
1717
internal sealed class PersistentServicesRegistry
1818
{
1919
private static readonly string _registryKey = typeof(PersistentServicesRegistry).FullName!;
20-
private static readonly RootTypeCache _persistentServiceTypeCache = new RootTypeCache();
20+
private static readonly RootTypeCache _persistentServiceTypeCache = new();
2121

2222
private readonly IServiceProvider _serviceProvider;
2323
private IPersistentServiceRegistration[] _registrations;
24-
private List<PersistingComponentStateSubscription> _subscriptions = [];
24+
private List<(PersistingComponentStateSubscription, RestoringComponentStateSubscription)> _subscriptions = [];
2525
private static readonly ConcurrentDictionary<Type, PropertiesAccessor> _cachedAccessorsByType = new();
2626

2727
public PersistentServicesRegistry(IServiceProvider serviceProvider)
@@ -45,7 +45,9 @@ internal void RegisterForPersistence(PersistentComponentState state)
4545
return;
4646
}
4747

48-
var subscriptions = new List<PersistingComponentStateSubscription>(_registrations.Length + 1);
48+
UpdateRegistrations(state);
49+
var subscriptions = new List<(PersistingComponentStateSubscription, RestoringComponentStateSubscription)>(
50+
_registrations.Length + 1);
4951
for (var i = 0; i < _registrations.Length; i++)
5052
{
5153
var registration = _registrations[i];
@@ -58,20 +60,29 @@ internal void RegisterForPersistence(PersistentComponentState state)
5860
var renderMode = registration.GetRenderModeOrDefault();
5961

6062
var instance = _serviceProvider.GetRequiredService(type);
61-
subscriptions.Add(state.RegisterOnPersisting(() =>
62-
{
63-
PersistInstanceState(instance, type, state);
64-
return Task.CompletedTask;
65-
}, renderMode));
63+
subscriptions.Add((
64+
state.RegisterOnPersisting(() =>
65+
{
66+
PersistInstanceState(instance, type, state);
67+
return Task.CompletedTask;
68+
}, renderMode),
69+
// In order to avoid registering one callback per property, we register a single callback with the most
70+
// permissive options and perform the filtering inside of it.
71+
state.RegisterOnRestoring(() =>
72+
{
73+
RestoreInstanceState(instance, type, state);
74+
}, new RestoreOptions { AllowUpdates = true })));
6675
}
6776

6877
if (RenderMode != null)
6978
{
70-
subscriptions.Add(state.RegisterOnPersisting(() =>
71-
{
72-
state.PersistAsJson(_registryKey, _registrations);
73-
return Task.CompletedTask;
74-
}, RenderMode));
79+
subscriptions.Add((
80+
state.RegisterOnPersisting(() =>
81+
{
82+
state.PersistAsJson(_registryKey, _registrations);
83+
return Task.CompletedTask;
84+
}, RenderMode),
85+
default));
7586
}
7687

7788
_subscriptions = subscriptions;
@@ -83,7 +94,7 @@ private static void PersistInstanceState(object instance, Type type, PersistentC
8394
var accessors = _cachedAccessorsByType.GetOrAdd(instance.GetType(), static (runtimeType, declaredType) => new PropertiesAccessor(runtimeType, declaredType), type);
8495
foreach (var (key, propertyType) in accessors.KeyTypePairs)
8596
{
86-
var (setter, getter) = accessors.GetAccessor(key);
97+
var (setter, getter, options) = accessors.GetAccessor(key);
8798
var value = getter.GetValue(instance);
8899
if (value != null)
89100
{
@@ -96,33 +107,12 @@ private static void PersistInstanceState(object instance, Type type, PersistentC
96107
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
97108
Justification = "Types registered for persistence are preserved in the API call to register them and typically live in assemblies that aren't trimmed.")]
98109
[DynamicDependency(LinkerFlags.JsonSerialized, typeof(PersistentServiceRegistration))]
99-
internal void Restore(PersistentComponentState state)
110+
private void UpdateRegistrations(PersistentComponentState state)
100111
{
101112
if (state.TryTakeFromJson<PersistentServiceRegistration[]>(_registryKey, out var registry) && registry != null)
102113
{
103114
_registrations = ResolveRegistrations(_registrations.Concat(registry));
104115
}
105-
106-
RestoreRegistrationsIfAvailable(state);
107-
}
108-
109-
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Types registered for persistence are preserved in the API call to register them and typically live in assemblies that aren't trimmed.")]
110-
private void RestoreRegistrationsIfAvailable(PersistentComponentState state)
111-
{
112-
foreach (var registration in _registrations)
113-
{
114-
var type = ResolveType(registration);
115-
if (type == null)
116-
{
117-
continue;
118-
}
119-
120-
var instance = _serviceProvider.GetService(type);
121-
if (instance != null)
122-
{
123-
RestoreInstanceState(instance, type, state);
124-
}
125-
}
126116
}
127117

128118
[RequiresUnreferencedCode("Calls Microsoft.AspNetCore.Components.PersistentComponentState.TryTakeFromJson(String, Type, out Object)")]
@@ -131,9 +121,13 @@ private static void RestoreInstanceState(object instance, Type type, PersistentC
131121
var accessors = _cachedAccessorsByType.GetOrAdd(instance.GetType(), static (runtimeType, declaredType) => new PropertiesAccessor(runtimeType, declaredType), type);
132122
foreach (var (key, propertyType) in accessors.KeyTypePairs)
133123
{
124+
var (setter, getter, options) = accessors.GetAccessor(key);
125+
if (!state.CurrentContext.ShouldRestore(options))
126+
{
127+
continue;
128+
}
134129
if (state.TryTakeFromJson(key, propertyType, out var result))
135130
{
136-
var (setter, getter) = accessors.GetAccessor(key);
137131
setter.SetValue(instance, result!);
138132
}
139133
}
@@ -156,12 +150,12 @@ private sealed class PropertiesAccessor
156150
{
157151
internal const BindingFlags BindablePropertyFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase;
158152

159-
private readonly Dictionary<string, (PropertySetter, PropertyGetter)> _underlyingAccessors;
153+
private readonly Dictionary<string, (PropertySetter, PropertyGetter, RestoreOptions)> _underlyingAccessors;
160154
private readonly (string, Type)[] _cachedKeysForService;
161155

162156
public PropertiesAccessor([DynamicallyAccessedMembers(LinkerFlags.Component)] Type targetType, Type keyType)
163157
{
164-
_underlyingAccessors = new Dictionary<string, (PropertySetter, PropertyGetter)>(StringComparer.OrdinalIgnoreCase);
158+
_underlyingAccessors = new Dictionary<string, (PropertySetter, PropertyGetter, RestoreOptions)>(StringComparer.OrdinalIgnoreCase);
165159

166160
var keys = new List<(string, Type)>();
167161
foreach (var propertyInfo in GetCandidateBindableProperties(targetType))
@@ -195,10 +189,16 @@ public PropertiesAccessor([DynamicallyAccessedMembers(LinkerFlags.Component)] Ty
195189
$"The type '{targetType.FullName}' declares a property matching the name '{propertyName}' that is not public. Persistent service properties must be public.");
196190
}
197191

192+
var restoreOptions = new RestoreOptions
193+
{
194+
RestoreBehavior = parameterAttribute.RestoreBehavior,
195+
AllowUpdates = parameterAttribute.AllowUpdates,
196+
};
197+
198198
var propertySetter = new PropertySetter(targetType, propertyInfo);
199199
var propertyGetter = new PropertyGetter(targetType, propertyInfo);
200200

201-
_underlyingAccessors.Add(key, (propertySetter, propertyGetter));
201+
_underlyingAccessors.Add(key, (propertySetter, propertyGetter, restoreOptions));
202202
}
203203

204204
_cachedKeysForService = [.. keys];
@@ -227,7 +227,7 @@ internal static IEnumerable<PropertyInfo> GetCandidateBindableProperties(
227227
[DynamicallyAccessedMembers(LinkerFlags.Component)] Type targetType)
228228
=> MemberAssignment.GetPropertiesIncludingInherited(targetType, BindablePropertyFlags);
229229

230-
internal (PropertySetter setter, PropertyGetter getter) GetAccessor(string key) =>
230+
internal (PropertySetter setter, PropertyGetter getter, RestoreOptions options) GetAccessor(string key) =>
231231
_underlyingAccessors.TryGetValue(key, out var result) ? result : default;
232232
}
233233

0 commit comments

Comments
 (0)