Skip to content

Commit 6caf183

Browse files
committed
Refactor
1 parent cd10568 commit 6caf183

19 files changed

+850
-865
lines changed
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
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.Buffers;
5+
using System.Collections.Concurrent;
6+
using System.Diagnostics.CodeAnalysis;
7+
using System.Reflection;
8+
using Microsoft.AspNetCore.Components.Reflection;
9+
using Microsoft.AspNetCore.Components.Rendering;
10+
using Microsoft.AspNetCore.Internal;
11+
using Microsoft.Extensions.Logging;
12+
13+
namespace Microsoft.AspNetCore.Components.Infrastructure;
14+
15+
internal partial class PersistentValueProviderComponentSubscription
16+
{
17+
private static readonly ConcurrentDictionary<(Type, string), PropertyGetter> _propertyGetterCache = new();
18+
private static readonly ConcurrentDictionary<Type, IPersistentComponentStateSerializer?> _serializerCache = new();
19+
private static readonly object _uninitializedValue = new();
20+
21+
private readonly PersistentComponentState _state;
22+
private readonly ComponentState _subscriber;
23+
private readonly string _propertyName;
24+
private readonly Type _propertyType;
25+
private readonly PropertyGetter _propertyGetter;
26+
private readonly IPersistentComponentStateSerializer? _customSerializer;
27+
private readonly ILogger _logger;
28+
29+
private readonly PersistingComponentStateSubscription? _persistingSubscription;
30+
private readonly RestoringComponentStateSubscription? _restoringSubscription;
31+
private object? _lastValue = _uninitializedValue;
32+
private bool _hasPendingInitialValue;
33+
private bool _ignoreUpdatedValues;
34+
private string? _storageKey;
35+
36+
public PersistentValueProviderComponentSubscription(
37+
PersistentComponentState state,
38+
ComponentState subscriber,
39+
CascadingParameterInfo parameterInfo,
40+
IServiceProvider serviceProvider,
41+
ILogger logger)
42+
{
43+
_state = state;
44+
_subscriber = subscriber;
45+
_propertyName = parameterInfo.PropertyName;
46+
_propertyType = parameterInfo.PropertyType;
47+
_logger = logger;
48+
var attribute = (PersistentStateAttribute)parameterInfo.Attribute;
49+
50+
_customSerializer = _serializerCache.GetOrAdd(_propertyType, SerializerFactory, serviceProvider);
51+
_propertyGetter = _propertyGetterCache.GetOrAdd((subscriber.Component.GetType(), _propertyName), PropertyGetterFactory);
52+
53+
_persistingSubscription = state.RegisterOnPersisting(
54+
PersistProperty,
55+
subscriber.Renderer.GetComponentRenderMode(subscriber.Component));
56+
57+
_restoringSubscription = state.RegisterOnRestoring(
58+
RestoreProperty,
59+
new RestoreOptions { RestoreBehavior = attribute.RestoreBehavior, AllowUpdates = attribute.AllowUpdates });
60+
}
61+
62+
internal object? GetOrComputeLastValue()
63+
{
64+
var isInitialized = !ReferenceEquals(_lastValue, _uninitializedValue);
65+
if (!isInitialized)
66+
{
67+
// Remove the uninitialized sentinel.
68+
_lastValue = null;
69+
if (_hasPendingInitialValue)
70+
{
71+
RestoreProperty();
72+
_hasPendingInitialValue = false;
73+
}
74+
}
75+
else
76+
{
77+
if (_ignoreUpdatedValues)
78+
{
79+
// At this point, we just received a value update from `RestoreProperty`.
80+
// The property value might have been modified by the component and in this
81+
// case we want to overwrite it with the value we just restored.
82+
_ignoreUpdatedValues = false;
83+
return _lastValue;
84+
}
85+
else
86+
{
87+
// In this case, the component might have modified the property value after
88+
// we restored it from the persistent state. We don't want to overwrite it
89+
// with a previously restored value.
90+
var currentPropertyValue = _propertyGetter.GetValue(_subscriber.Component);
91+
return currentPropertyValue;
92+
}
93+
}
94+
95+
return _lastValue;
96+
}
97+
98+
[UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "OpenComponent already has the right set of attributes")]
99+
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "OpenComponent already has the right set of attributes")]
100+
[UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "OpenComponent already has the right set of attributes")]
101+
[UnconditionalSuppressMessage("Trimming", "IL2077:'type' argument does not satisfy 'DynamicallyAccessedMemberTypes' in call to target method. The source field does not have matching annotations.", Justification = "Property types on components are preserved through other means.")]
102+
private void RestoreProperty()
103+
{
104+
var skipNotifications = _hasPendingInitialValue;
105+
if (ReferenceEquals(_lastValue, _uninitializedValue) && !_hasPendingInitialValue)
106+
{
107+
// Upon subscribing, the callback might be invoked right away,
108+
// but this is too early to restore the first value since the component state
109+
// hasn't been fully initialized yet.
110+
// For that reason, we make a mark to restore the state on GetOrComputeLastValue.
111+
_hasPendingInitialValue = true;
112+
return;
113+
}
114+
115+
// The key needs to be computed here, do not move this outside of the lambda.
116+
_storageKey ??= PersistentStateKeyResolver.ComputeKey(_subscriber, _propertyName);
117+
118+
if (_customSerializer != null)
119+
{
120+
if (_state.TryTakeBytes(_storageKey, out var data))
121+
{
122+
Log.RestoringValueFromState(_logger, _storageKey, _propertyType.Name, _propertyName);
123+
var sequence = new ReadOnlySequence<byte>(data!);
124+
_lastValue = _customSerializer.Restore(_propertyType, sequence);
125+
if (!skipNotifications)
126+
{
127+
_ignoreUpdatedValues = true;
128+
_subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound);
129+
}
130+
}
131+
else
132+
{
133+
Log.ValueNotFoundInPersistentState(_logger, _storageKey, _propertyType.Name, "null", _propertyName);
134+
}
135+
}
136+
else
137+
{
138+
if (_state.TryTakeFromJson(_storageKey, _propertyType, out var value))
139+
{
140+
Log.RestoredValueFromPersistentState(_logger, _storageKey, _propertyType.Name, "null", _propertyName);
141+
_lastValue = value;
142+
if (!skipNotifications)
143+
{
144+
_ignoreUpdatedValues = true;
145+
_subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound);
146+
}
147+
}
148+
else
149+
{
150+
Log.NoValueToRestoreFromState(_logger, _storageKey, _propertyType.Name, _propertyName);
151+
}
152+
}
153+
}
154+
155+
[UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "OpenComponent already has the right set of attributes")]
156+
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "OpenComponent already has the right set of attributes")]
157+
[UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "OpenComponent already has the right set of attributes")]
158+
[UnconditionalSuppressMessage("Trimming", "IL2077:'type' argument does not satisfy 'DynamicallyAccessedMemberTypes' in call to target method. The source field does not have matching annotations.", Justification = "Property types on components are preserved through other means.")]
159+
private Task PersistProperty()
160+
{
161+
// The key needs to be computed here, do not move this outside of the lambda.
162+
_storageKey ??= PersistentStateKeyResolver.ComputeKey(_subscriber, _propertyName);
163+
164+
var property = _propertyGetter.GetValue(_subscriber.Component);
165+
if (property == null)
166+
{
167+
Log.SkippedPersistingNullValue(_logger, _storageKey, _propertyType.Name, _subscriber.Component.GetType().Name, _propertyName);
168+
return Task.CompletedTask;
169+
}
170+
171+
if (_customSerializer != null)
172+
{
173+
Log.PersistingValueToState(_logger, _storageKey, _propertyType.Name, _subscriber.Component.GetType().Name, _propertyName);
174+
175+
using var writer = new PooledArrayBufferWriter<byte>();
176+
_customSerializer.Persist(_propertyType, property, writer);
177+
_state.PersistAsBytes(_storageKey, writer.WrittenMemory.ToArray());
178+
return Task.CompletedTask;
179+
}
180+
181+
// Fallback to JSON serialization
182+
Log.PersistingValueToState(_logger, _storageKey, _propertyType.Name, _subscriber.Component.GetType().Name, _propertyName);
183+
_state.PersistAsJson(_storageKey, property, _propertyType);
184+
return Task.CompletedTask;
185+
}
186+
187+
public void Dispose()
188+
{
189+
_persistingSubscription?.Dispose();
190+
_restoringSubscription?.Dispose();
191+
}
192+
193+
private IPersistentComponentStateSerializer? SerializerFactory(Type type, IServiceProvider serviceProvider)
194+
{
195+
var serializerType = typeof(PersistentComponentStateSerializer<>).MakeGenericType(type);
196+
var serializer = serviceProvider.GetService(serializerType);
197+
198+
// The generic class now inherits from the internal interface, so we can cast directly
199+
return serializer as IPersistentComponentStateSerializer;
200+
}
201+
202+
[UnconditionalSuppressMessage(
203+
"Trimming",
204+
"IL2077:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The source field does not have matching annotations.",
205+
Justification = "Properties of rendered components are preserved through other means and won't get trimmed.")]
206+
207+
private static PropertyGetter PropertyGetterFactory((Type type, string propertyName) key)
208+
{
209+
var (type, propertyName) = key;
210+
var propertyInfo = GetPropertyInfo(type, propertyName);
211+
if (propertyInfo == null)
212+
{
213+
throw new InvalidOperationException($"Property {propertyName} not found on type {type.FullName}");
214+
}
215+
return new PropertyGetter(type, propertyInfo);
216+
217+
static PropertyInfo? GetPropertyInfo([DynamicallyAccessedMembers(LinkerFlags.Component)] Type type, string propertyName)
218+
=> type.GetProperty(propertyName);
219+
}
220+
221+
private static partial class Log
222+
{
223+
[LoggerMessage(1, LogLevel.Debug, "Persisting value for storage key '{StorageKey}' of type '{PropertyType}' from component '{ComponentType}' for property '{PropertyName}'", EventName = "PersistingValueToState")]
224+
public static partial void PersistingValueToState(ILogger logger, string storageKey, string propertyType, string componentType, string propertyName);
225+
226+
[LoggerMessage(2, LogLevel.Debug, "Skipped persisting null value for storage key '{StorageKey}' of type '{PropertyType}' from component '{ComponentType}' for property '{PropertyName}'", EventName = "SkippedPersistingNullValue")]
227+
public static partial void SkippedPersistingNullValue(ILogger logger, string storageKey, string propertyType, string componentType, string propertyName);
228+
229+
[LoggerMessage(3, LogLevel.Debug, "Restoring value for storage key '{StorageKey}' of type '{PropertyType}' for property '{PropertyName}'", EventName = "RestoringValueFromState")]
230+
public static partial void RestoringValueFromState(ILogger logger, string storageKey, string propertyType, string propertyName);
231+
232+
[LoggerMessage(4, LogLevel.Debug, "No value to restore for storage key '{StorageKey}' of type '{PropertyType}' for property '{PropertyName}'", EventName = "NoValueToRestoreFromState")]
233+
public static partial void NoValueToRestoreFromState(ILogger logger, string storageKey, string propertyType, string propertyName);
234+
235+
[LoggerMessage(5, LogLevel.Debug, "Restored value from persistent state for storage key '{StorageKey}' of type '{PropertyType}' for component '{ComponentType}' for property '{PropertyName}'", EventName = "RestoredValueFromPersistentState")]
236+
public static partial void RestoredValueFromPersistentState(ILogger logger, string storageKey, string propertyType, string componentType, string propertyName);
237+
238+
[LoggerMessage(6, LogLevel.Debug, "Value not found in persistent state for storage key '{StorageKey}' of type '{PropertyType}' for component '{ComponentType}' for property '{PropertyName}'", EventName = "ValueNotFoundInPersistentState")]
239+
public static partial void ValueNotFoundInPersistentState(ILogger logger, string storageKey, string propertyType, string componentType, string propertyName);
240+
}
241+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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 Microsoft.AspNetCore.Components.Rendering;
5+
6+
namespace Microsoft.AspNetCore.Components.Infrastructure;
7+
8+
internal struct ComponentSubscriptionKey(ComponentState subscriber, string propertyName) : IEquatable<ComponentSubscriptionKey>
9+
{
10+
public ComponentState Subscriber { get; } = subscriber;
11+
12+
public string PropertyName { get; } = propertyName;
13+
14+
public bool Equals(ComponentSubscriptionKey other)
15+
=> Subscriber == other.Subscriber && PropertyName == other.PropertyName;
16+
17+
public override bool Equals(object? obj)
18+
=> obj is ComponentSubscriptionKey other && Equals(other);
19+
20+
public override int GetHashCode()
21+
=> HashCode.Combine(Subscriber, PropertyName);
22+
}

src/Components/Components/src/PersistentComponentState.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public class PersistentComponentState
1818
private readonly List<PersistComponentStateRegistration> _registeredCallbacks;
1919
private readonly List<RestoreComponentStateRegistration> _registeredRestoringCallbacks;
2020
private IPersistentComponentStateScenario? _currentScenario;
21+
private RestoreContext? _currentContext = null;
2122

2223
internal PersistentComponentState(
2324
IDictionary<string, byte[]> currentState,
@@ -31,6 +32,7 @@ internal PersistentComponentState(
3132

3233
internal bool PersistingState { get; set; }
3334

35+
// TODO: Replace IPersistentComponentStateScenario with RestoreContext
3436
internal void InitializeExistingState(IDictionary<string, byte[]> existingState, IPersistentComponentStateScenario? scenario = null)
3537
{
3638
if (_existingState != null)
@@ -39,6 +41,7 @@ internal void InitializeExistingState(IDictionary<string, byte[]> existingState,
3941
}
4042
_existingState = existingState ?? throw new ArgumentNullException(nameof(existingState));
4143
_currentScenario = scenario;
44+
_currentContext = null!;
4245
}
4346

4447
/// <summary>
@@ -73,13 +76,30 @@ public PersistingComponentStateSubscription RegisterOnPersisting(Func<Task> call
7376
return new PersistingComponentStateSubscription(_registeredCallbacks, persistenceCallback);
7477
}
7578

79+
public RestoringComponentStateSubscription RegisterOnRestoring(Action callback, RestoreOptions options)
80+
{
81+
if (_currentContext!.ShouldRestore(options))
82+
{
83+
callback();
84+
}
85+
86+
if (options.AllowUpdates)
87+
{
88+
// TODO: Remove the filter from registration
89+
var registration = new RestoreComponentStateRegistration(null, callback);
90+
_registeredRestoringCallbacks.Add(registration);
91+
return new RestoringComponentStateSubscription(_registeredRestoringCallbacks, registration);
92+
}
93+
}
94+
7695
/// <summary>
7796
/// Register a callback to restore the component state during specific scenarios.
7897
/// The callback will only be invoked if the filter supports the current restoration scenario.
7998
/// </summary>
8099
/// <param name="filter">The filter that determines when this callback should be invoked.</param>
81100
/// <param name="callback">The callback to invoke during state restoration.</param>
82101
/// <returns>A subscription that can be used to unregister the callback when disposed.</returns>
102+
// TODO: Remove this method.
83103
public RestoringComponentStateSubscription RegisterOnRestoring(IPersistentStateFilter? filter, Action callback)
84104
{
85105
ArgumentNullException.ThrowIfNull(callback);
@@ -248,6 +268,7 @@ private bool TryTake(string key, out byte[]? value)
248268
/// </summary>
249269
/// <param name="state">The new state to apply.</param>
250270
/// <param name="scenario">The scenario for which the state is being updated.</param>
271+
// TODO: Replace scenario with context.
251272
internal void UpdateExistingState(IDictionary<string, byte[]> state, IPersistentComponentStateScenario? scenario)
252273
{
253274
ArgumentNullException.ThrowIfNull(state);

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

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,20 +78,19 @@ public async Task RestoreStateAsync(IPersistentComponentStateStore store, IPersi
7878
else
7979
{
8080
State.InitializeExistingState(data, scenario);
81+
_servicesRegistry?.Restore(State);
8182
_stateIsInitialized = true;
8283
}
8384

8485
foreach (var registration in _registeredRestoringCallbacks)
8586
{
86-
if (scenario == null || registration.Filter == null ||
87-
(registration.Filter.SupportsScenario(scenario) && registration.Filter.ShouldRestore(scenario)) ||
88-
(!registration.Filter.SupportsScenario(scenario) && !scenario.IsRecurring))
89-
{
90-
registration.Callback();
91-
}
87+
//if (scenario == null || registration.Filter == null ||
88+
// (registration.Filter.SupportsScenario(scenario) && registration.Filter.ShouldRestore(scenario)) ||
89+
// (!registration.Filter.SupportsScenario(scenario) && !scenario.IsRecurring))
90+
//{
91+
registration.Callback();
92+
//}
9293
}
93-
94-
_servicesRegistry?.Restore(State);
9594
}
9695

9796
/// <summary>

src/Components/Components/src/PersistentState/IPersistentComponentStateScenario.cs

Lines changed: 0 additions & 19 deletions
This file was deleted.

0 commit comments

Comments
 (0)