Skip to content

Commit 959c8e5

Browse files
Copilotjaviercn
andauthored
[Blazor] Fix ComponentStatePersistenceManager iteration to prevent AntiforgeryValidationException in Blazor WASM (#63674)
Reverses the for-loop iteration to ensure that if a callback unregisters itself, it doesn't affect other callbacks in subsequent operations. --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: javiercn <[email protected]>
1 parent 7dbebe9 commit 959c8e5

File tree

2 files changed

+54
-1
lines changed

2 files changed

+54
-1
lines changed

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,13 @@ public void SetPlatformRenderMode(IComponentRenderMode renderMode)
172172

173173
private void InferRenderModes(Renderer renderer)
174174
{
175-
for (var i = 0; i < _registeredCallbacks.Count; i++)
175+
// We are iterating backwards to allow the callbacks to remove themselves from the list.
176+
// Otherwise, we would have to make a copy of the list to avoid running into situations
177+
// where we don't run all the callbacks because the count of the list changed while we
178+
// were iterating over it.
179+
// It is not allowed to register a callback while we are persisting the state, so we don't
180+
// need to worry about new callbacks being added to the list.
181+
for (var i = _registeredCallbacks.Count - 1; i >= 0; i--)
176182
{
177183
var registration = _registeredCallbacks[i];
178184
if (registration.RenderMode != null)

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,53 @@ public async Task PersistStateAsync_InvokesAllCallbacksEvenIfACallbackIsRemovedA
353353
Assert.Equal(4, executionSequence.Count);
354354
}
355355

356+
[Fact]
357+
public async Task PersistStateAsync_InvokesAllCallbacksWhenFirstCallbackUnregistersItself()
358+
{
359+
// Arrange
360+
var state = new Dictionary<string, byte[]>();
361+
var store = new TestStore(state);
362+
var persistenceManager = new ComponentStatePersistenceManager(
363+
NullLogger<ComponentStatePersistenceManager>.Instance,
364+
CreateServiceProvider());
365+
var renderer = new TestRenderer();
366+
367+
var executionSequence = new List<int>();
368+
369+
// Register the first callback that will unregister itself - this is the key test case
370+
// where the first callback removes itself from the collection during iteration
371+
PersistingComponentStateSubscription firstSubscription = default;
372+
firstSubscription = persistenceManager.State.RegisterOnPersisting(() =>
373+
{
374+
executionSequence.Add(1);
375+
firstSubscription.Dispose(); // Remove itself from the collection
376+
return Task.CompletedTask;
377+
}, new TestRenderMode());
378+
379+
// Register additional callbacks to ensure they still get executed
380+
persistenceManager.State.RegisterOnPersisting(() =>
381+
{
382+
executionSequence.Add(2);
383+
return Task.CompletedTask;
384+
}, new TestRenderMode());
385+
386+
persistenceManager.State.RegisterOnPersisting(() =>
387+
{
388+
executionSequence.Add(3);
389+
return Task.CompletedTask;
390+
}, new TestRenderMode());
391+
392+
// Act
393+
await persistenceManager.PersistStateAsync(store, renderer);
394+
395+
// Assert
396+
// All callbacks should be executed even though the first one removed itself
397+
Assert.Contains(1, executionSequence);
398+
Assert.Contains(2, executionSequence);
399+
Assert.Contains(3, executionSequence);
400+
Assert.Equal(3, executionSequence.Count);
401+
}
402+
356403
[Fact]
357404
public async Task RestoreStateAsync_ValidatesOnlySupportUpdatesWhenRestoreContextValueUpdate()
358405
{

0 commit comments

Comments
 (0)