Skip to content

Commit e05a98d

Browse files
committed
fix: DisposeComponentsAsync disposes components while the dispatcher is busy (fixes #1939)
1 parent 2abc4d4 commit e05a98d

3 files changed

Lines changed: 38 additions & 29 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ All notable changes to **bUnit** will be documented in this file. The project ad
66

77
## [Unreleased]
88

9+
### Fixed
10+
11+
- `DisposeComponentsAsync()` could dispose nothing when the renderer's dispatcher was busy, e.g. with a timer calling `InvokeAsync`. Reported by [@xfunc-tim-schneider](https://github.com/xfunc-tim-schneider) in #1939. Fixed by [@linkdotnet](https://github.com/linkdotnet).
12+
913
## [2.11.3] - 2026-09-13
1014

1115
### Fixed

‎src/bunit/Rendering/BunitRenderer.cs‎

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -281,27 +281,21 @@ public IReadOnlyList<IRenderedComponent<TComponent>> FindComponents<TComponent>(
281281
public Task DisposeComponents()
282282
{
283283
ObjectDisposedException.ThrowIf(disposed, this);
284-
Task? returnTask;
285284

286-
lock (renderTreeUpdateLock)
285+
return Dispatcher.InvokeAsync(async () =>
287286
{
288-
returnTask = Dispatcher.InvokeAsync(async () =>
289-
{
290-
ResetUnhandledException();
291-
292-
foreach (var root in rootComponents)
293-
{
294-
root.Detach();
295-
}
296-
297-
await Task.WhenAll(disposalTasks).ConfigureAwait(false);
298-
disposalTasks.Clear();
299-
});
287+
ResetUnhandledException();
300288

289+
var roots = rootComponents.ToArray();
301290
rootComponents.Clear();
302-
}
303291

304-
return returnTask;
292+
foreach (var root in roots)
293+
{
294+
root.Detach();
295+
}
296+
297+
await Task.WhenAll(disposalTasks.ToArray()).ConfigureAwait(false);
298+
});
305299
}
306300

307301
/// <inheritdoc/>
@@ -425,21 +419,11 @@ protected override void AddPendingTask(ComponentState? componentState, Task task
425419
if (componentState is null)
426420
{
427421
ArgumentNullException.ThrowIfNull(task);
428-
AddDisposalTaskToQueue();
422+
disposalTasks.RemoveAll(static t => t.IsCompleted);
423+
disposalTasks.Add(task);
429424
}
430425

431426
base.AddPendingTask(componentState, task);
432-
433-
void AddDisposalTaskToQueue()
434-
{
435-
var t = task;
436-
t = task.ContinueWith(_ =>
437-
{
438-
disposalTasks.Remove(t);
439-
}, TaskScheduler.Current);
440-
441-
disposalTasks.Add(t);
442-
}
443427
}
444428

445429
internal Task SetDirectParametersAsync<TComponent>(IRenderedComponent<TComponent> renderedComponent, ParameterView parameters)
@@ -602,7 +586,6 @@ protected override void Dispose(bool disposing)
602586
if (disposing)
603587
{
604588
returnedRenderedComponentIds.Clear();
605-
disposalTasks.Clear();
606589
unhandledExceptionTsc.TrySetCanceled();
607590
}
608591

‎tests/bunit.tests/BunitContextTest.cs‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,28 @@ public async Task Test104()
5252
callStack.Count.ShouldBe(1);
5353
}
5454

55+
[Fact(DisplayName = "DisposeComponents disposes components while the dispatcher is busy")]
56+
public async Task Test105()
57+
{
58+
var callStack = new List<string>();
59+
Render<ChildDispose>(ps => ps.Add(p => p.CallStack, callStack));
60+
using var entered = new ManualResetEventSlim();
61+
using var release = new ManualResetEventSlim();
62+
var blocker = Task.Run(() => Renderer.Dispatcher.InvokeAsync(() =>
63+
{
64+
entered.Set();
65+
release.Wait();
66+
}));
67+
entered.Wait();
68+
69+
var disposeTask = DisposeComponentsAsync();
70+
release.Set();
71+
await disposeTask;
72+
await blocker;
73+
74+
callStack.Count.ShouldBe(1);
75+
}
76+
5577
[Fact(DisplayName = "The test service provider should register a placeholder HttpClient which throws exceptions")]
5678
public void Test024()
5779
{

0 commit comments

Comments
 (0)