Skip to content

Commit 245624e

Browse files
committed
More tests
1 parent 76dd1f2 commit 245624e

5 files changed

+693
-37
lines changed

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

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -94,23 +94,33 @@ async Task PauseAndPersistState()
9494
// the next store can start with a clean slate.
9595
foreach (var store in compositeStore)
9696
{
97-
await PersistState(store);
97+
var result = await TryPersistState(store);
98+
if (!result)
99+
{
100+
break;
101+
}
98102
_currentState.Clear();
99103
}
100104
}
101105
else
102106
{
103-
await PersistState(store);
107+
await TryPersistState(store);
104108
}
105109

106110
State.PersistingState = false;
107111
_stateIsPersisted = true;
108112
}
109113

110-
async Task PersistState(IPersistentComponentStateStore store)
114+
async Task<bool> TryPersistState(IPersistentComponentStateStore store)
111115
{
112-
await PauseAsync(store);
116+
if (!await TryPauseAsync(store))
117+
{
118+
_currentState.Clear();
119+
return false;
120+
}
121+
113122
await store.PersistStateAsync(_currentState);
123+
return true;
114124
}
115125
}
116126

@@ -166,9 +176,9 @@ private void InferRenderModes(Renderer renderer)
166176
}
167177
}
168178

169-
internal Task PauseAsync(IPersistentComponentStateStore store)
179+
internal Task<bool> TryPauseAsync(IPersistentComponentStateStore store)
170180
{
171-
List<Task>? pendingCallbackTasks = null;
181+
List<Task<bool>>? pendingCallbackTasks = null;
172182

173183
// We are iterating backwards to allow the callbacks to remove themselves from the list.
174184
// Otherwise, we would have to make a copy of the list to avoid running into situations
@@ -189,31 +199,38 @@ internal Task PauseAsync(IPersistentComponentStateStore store)
189199
continue;
190200
}
191201

192-
var result = ExecuteCallback(registration.Callback, _logger);
202+
var result = TryExecuteCallback(registration.Callback, _logger);
193203
if (!result.IsCompletedSuccessfully)
194204
{
195-
pendingCallbackTasks ??= new();
205+
pendingCallbackTasks ??= [];
196206
pendingCallbackTasks.Add(result);
197207
}
208+
else
209+
{
210+
if (!result.Result)
211+
{
212+
return Task.FromResult(false);
213+
}
214+
}
198215
}
199216

200217
if (pendingCallbackTasks != null)
201218
{
202-
return Task.WhenAll(pendingCallbackTasks);
219+
return AnyTaskFailed(pendingCallbackTasks);
203220
}
204221
else
205222
{
206-
return Task.CompletedTask;
223+
return Task.FromResult(true);
207224
}
208225

209-
static Task ExecuteCallback(Func<Task> callback, ILogger<ComponentStatePersistenceManager> logger)
226+
static Task<bool> TryExecuteCallback(Func<Task> callback, ILogger<ComponentStatePersistenceManager> logger)
210227
{
211228
try
212229
{
213230
var current = callback();
214231
if (current.IsCompletedSuccessfully)
215232
{
216-
return current;
233+
return Task.FromResult(true);
217234
}
218235
else
219236
{
@@ -223,21 +240,35 @@ static Task ExecuteCallback(Func<Task> callback, ILogger<ComponentStatePersisten
223240
catch (Exception ex)
224241
{
225242
logger.LogError(new EventId(1000, "PersistenceCallbackError"), ex, "There was an error executing a callback while pausing the application.");
226-
return Task.CompletedTask;
243+
return Task.FromResult(false);
227244
}
228245

229-
static async Task Awaited(Task task, ILogger<ComponentStatePersistenceManager> logger)
246+
static async Task<bool> Awaited(Task task, ILogger<ComponentStatePersistenceManager> logger)
230247
{
231248
try
232249
{
233250
await task;
251+
return true;
234252
}
235253
catch (Exception ex)
236254
{
237255
logger.LogError(new EventId(1000, "PersistenceCallbackError"), ex, "There was an error executing a callback while pausing the application.");
238-
return;
256+
return false;
239257
}
240258
}
241259
}
260+
261+
static async Task<bool> AnyTaskFailed(List<Task<bool>> pendingCallbackTasks)
262+
{
263+
foreach (var result in await Task.WhenAll(pendingCallbackTasks))
264+
{
265+
if (!result)
266+
{
267+
return false;
268+
}
269+
}
270+
271+
return true;
272+
}
242273
}
243274
}

src/Components/Components/src/SupplyParameterFromPersistentComponentStateProviderServiceCollectionExtensions.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,7 @@ public static IServiceCollection AddSupplyValueFromPersistentComponentStateProvi
4545
// We look for the assembly in the current list of loaded assemblies.
4646
// We look for the type inside the assembly.
4747
// We resolve the service from the DI container.
48-
// TODO: We can support registering for a specific render mode at this level (that way no info gets sent to the client accidentally 4 example).
49-
// Even as far as defaulting to Server (to avoid disclosing anything confidential to the client, even though is the Developer responsibility).
50-
// We can choose to fail when the service is not registered on DI.
5148
// We loop through the properties in the type and try to restore the properties that have SupplyParameterFromPersistentComponentState on them.
52-
5349
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPersistentComponentRegistration>(new PersistentComponentRegistration<TService>(componentRenderMode)));
5450

5551
return services;

src/Components/Components/src/SupplyParameterFromPersistentComponentStateValueProvider.cs

Lines changed: 79 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ internal sealed class SupplyParameterFromPersistentComponentStateValueProvider(P
1818
private readonly Dictionary<ComponentState, PersistingComponentStateSubscription> _subscriptions = [];
1919

2020
public bool IsFixed => false;
21+
// For testing purposes only
22+
internal Dictionary<ComponentState, PersistingComponentStateSubscription> Subscriptions => _subscriptions;
2123

2224
public bool CanSupplyValue(in CascadingParameterInfo parameterInfo)
2325
=> parameterInfo.Attribute is SupplyParameterFromPersistentComponentStateAttribute;
@@ -31,7 +33,7 @@ public bool CanSupplyValue(in CascadingParameterInfo parameterInfo)
3133
"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.",
3234
Justification = "JSON serialization and deserialization might require types that cannot be statically analyzed.")]
3335
public object? GetCurrentValue(in CascadingParameterInfo parameterInfo) =>
34-
state.TryTakeFromJson(parameterInfo.PropertyName, parameterInfo.PropertyType, out var value) ? value : null;
36+
throw new InvalidOperationException("Using this provider requires a key.");
3537

3638
[UnconditionalSuppressMessage(
3739
"ReflectionAnalysis",
@@ -74,7 +76,8 @@ public void Unsubscribe(ComponentState subscriber, in CascadingParameterInfo par
7476
}
7577
}
7678

77-
private static string ComputeKey(ComponentState componentState, string propertyName)
79+
// Internal for testing only
80+
internal static string ComputeKey(ComponentState componentState, string propertyName)
7881
{
7982
// We need to come up with a pseudo-unique key for the storage key.
8083
// We need to consider the property name, the component type, and its position within the component tree.
@@ -110,20 +113,58 @@ private static string ComputeFinalKey(byte[] preKey, ComponentState componentSta
110113
{
111114
Span<byte> keyBuffer = stackalloc byte[1024];
112115
preKey.CopyTo(keyBuffer);
113-
if (key is IUtf8SpanFormattable formattable)
116+
if (key is IUtf8SpanFormattable spanFormattable)
114117
{
115-
while (!formattable.TryFormat(keyBuffer[preKey.Length..], out var written, "{0:G}", CultureInfo.InvariantCulture))
118+
var wroteKey = false;
119+
while (!wroteKey)
116120
{
117-
// It is really unlikely that we will enter here, but we need to handle this case
118-
Debug.Assert(written == 0);
119-
var newPool = pool == null ? ArrayPool<byte>.Shared.Rent(2048) : ArrayPool<byte>.Shared.Rent(pool.Length * 2);
120-
keyBuffer[0..preKey.Length].CopyTo(newPool);
121-
keyBuffer = newPool;
122-
if (pool != null)
121+
wroteKey = spanFormattable.TryFormat(keyBuffer[preKey.Length..], out var written, "", CultureInfo.InvariantCulture);
122+
if (!wroteKey)
123123
{
124-
ArrayPool<byte>.Shared.Return(pool, clearArray: true);
124+
// It is really unlikely that we will enter here, but we need to handle this case
125+
Debug.Assert(written == 0);
126+
GrowBuffer(preKey, ref pool, ref keyBuffer);
127+
}
128+
else
129+
{
130+
keyBuffer = keyBuffer[..(preKey.Length + written)];
131+
}
132+
}
133+
}
134+
else if (key is IFormattable formattable)
135+
{
136+
var keyString = formattable.ToString("", CultureInfo.InvariantCulture);
137+
var wroteKey = false;
138+
while (!wroteKey)
139+
{
140+
wroteKey = Encoding.UTF8.TryGetBytes(keyString, keyBuffer[preKey.Length..], out var written);
141+
if (!wroteKey)
142+
{
143+
Debug.Assert(written == 0);
144+
GrowBuffer(preKey, ref pool, ref keyBuffer);
145+
}
146+
else
147+
{
148+
keyBuffer = keyBuffer[..(preKey.Length + written)];
149+
}
150+
}
151+
}
152+
else if (key is IConvertible convertible)
153+
{
154+
var keyString = convertible.ToString(CultureInfo.InvariantCulture);
155+
var wroteKey = false;
156+
while (!wroteKey)
157+
{
158+
wroteKey = Encoding.UTF8.TryGetBytes(keyString, keyBuffer[preKey.Length..], out var written);
159+
if (!wroteKey)
160+
{
161+
Debug.Assert(written == 0);
162+
GrowBuffer(preKey, ref pool, ref keyBuffer);
163+
}
164+
else
165+
{
166+
keyBuffer = keyBuffer[..(preKey.Length + written)];
125167
}
126-
pool = newPool;
127168
}
128169
}
129170

@@ -139,6 +180,18 @@ private static string ComputeFinalKey(byte[] preKey, ComponentState componentSta
139180
}
140181
}
141182

183+
private static void GrowBuffer(byte[] preKey, ref byte[]? pool, ref Span<byte> keyBuffer)
184+
{
185+
var newPool = pool == null ? ArrayPool<byte>.Shared.Rent(2048) : ArrayPool<byte>.Shared.Rent(pool.Length * 2);
186+
keyBuffer[0..preKey.Length].CopyTo(newPool);
187+
keyBuffer = newPool;
188+
if (pool != null)
189+
{
190+
ArrayPool<byte>.Shared.Return(pool, clearArray: true);
191+
}
192+
pool = newPool;
193+
}
194+
142195
private static object? GetSerializableKey(ComponentState componentState)
143196
{
144197
if (componentState.LogicalParentComponentState is not { } parentComponentState)
@@ -173,10 +226,18 @@ private static string GetParentComponentType(ComponentState componentState) =>
173226
private static byte[] KeyFactory((string parentComponentType, string componentType, string propertyName) parts) =>
174227
Encoding.UTF8.GetBytes(string.Join(".", parts.parentComponentType, parts.componentType, parts.propertyName));
175228

176-
private static bool IsSerializableKey(object key) =>
177-
key is { } componentKey && componentKey.GetType() is Type type &&
178-
(Type.GetTypeCode(type) != TypeCode.Object
179-
|| type == typeof(Guid)
180-
|| type == typeof(DateOnly)
181-
|| type == typeof(TimeOnly));
229+
private static bool IsSerializableKey(object key)
230+
{
231+
if (key == null)
232+
{
233+
return false;
234+
}
235+
var keyType = key.GetType();
236+
var result = Type.GetTypeCode(keyType) != TypeCode.Object
237+
|| keyType == typeof(Guid)
238+
|| keyType == typeof(DateOnly)
239+
|| keyType == typeof(TimeOnly);
240+
241+
return result;
242+
}
182243
}

src/Components/Components/test/PersistentState/ComponentStatePersistenceManagerTest.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,55 @@ public async Task PersistStateAsync_ContinuesInvokingPauseCallbacksDuringPersist
302302
Assert.Equal(LogLevel.Error, log.LogLevel);
303303
}
304304

305+
[Fact]
306+
public async Task PersistStateAsync_InvokesAllCallbacksEvenIfACallbackIsRemovedAsPartOfRunningIt()
307+
{
308+
// Arrange
309+
var state = new Dictionary<string, byte[]>();
310+
var store = new TestStore(state);
311+
var persistenceManager = new ComponentStatePersistenceManager(
312+
NullLogger<ComponentStatePersistenceManager>.Instance,
313+
CreateServiceProvider());
314+
var renderer = new TestRenderer();
315+
316+
var executionSequence = new List<int>();
317+
318+
persistenceManager.State.RegisterOnPersisting(() =>
319+
{
320+
executionSequence.Add(1);
321+
return Task.CompletedTask;
322+
}, new TestRenderMode());
323+
324+
PersistingComponentStateSubscription subscription2 = default;
325+
subscription2 = persistenceManager.State.RegisterOnPersisting(() =>
326+
{
327+
executionSequence.Add(2);
328+
subscription2.Dispose();
329+
return Task.CompletedTask;
330+
}, new TestRenderMode());
331+
332+
var tcs = new TaskCompletionSource();
333+
persistenceManager.State.RegisterOnPersisting(async () =>
334+
{
335+
executionSequence.Add(3);
336+
await tcs.Task;
337+
executionSequence.Add(4);
338+
}, new TestRenderMode());
339+
340+
// Act
341+
var persistTask = persistenceManager.PersistStateAsync(store, renderer);
342+
tcs.SetResult(); // Allow the async callback to complete
343+
await persistTask;
344+
345+
// Assert
346+
Assert.Contains(3, executionSequence);
347+
Assert.Contains(2, executionSequence);
348+
Assert.Contains(1, executionSequence);
349+
Assert.Contains(4, executionSequence);
350+
351+
Assert.Equal(4, executionSequence.Count);
352+
}
353+
305354
private class TestRenderer : Renderer
306355
{
307356
public TestRenderer() : base(new ServiceCollection().BuildServiceProvider(), NullLoggerFactory.Instance)

0 commit comments

Comments
 (0)