Skip to content

Commit a2c79c0

Browse files
committed
Cover exception and double async/sync disposal with unit tests.
1 parent b6959c6 commit a2c79c0

File tree

1 file changed

+69
-0
lines changed

1 file changed

+69
-0
lines changed

src/Components/Components/test/OwningComponentBaseTest.cs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,79 @@ public async Task DisposeAsync_CallsDispose_WithDisposingTrue()
127127
Assert.True(component.DisposingParameter);
128128
}
129129

130+
[Fact]
131+
public async Task DisposeAsync_ThenDispose_IsIdempotent()
132+
{
133+
var services = new ServiceCollection();
134+
services.AddSingleton<Counter>();
135+
services.AddTransient<MyService>();
136+
var serviceProvider = services.BuildServiceProvider();
137+
138+
var counter = serviceProvider.GetRequiredService<Counter>();
139+
var renderer = new TestRenderer(serviceProvider);
140+
var component = (ComponentWithDispose)renderer.InstantiateComponent<ComponentWithDispose>();
141+
142+
_ = component.MyService;
143+
144+
// First disposal via DisposeAsync
145+
await ((IAsyncDisposable)component).DisposeAsync();
146+
var firstCallCount = component.DisposeCallCount;
147+
Assert.Equal(1, counter.DisposedCount);
148+
149+
// Second disposal via Dispose - user override is called but base class prevents double-disposal
150+
((IDisposable)component).Dispose();
151+
// User override is called again, but base.Dispose() returns early due to IsDisposed check
152+
Assert.True(component.DisposeCallCount >= firstCallCount); // Override may be called, but...
153+
Assert.Equal(1, counter.DisposedCount); // ...service should only be disposed once
154+
}
155+
156+
[Fact]
157+
public async Task DisposeAsyncCore_Override_WithException_StillCallsDispose()
158+
{
159+
var services = new ServiceCollection();
160+
services.AddSingleton<Counter>();
161+
services.AddTransient<MyService>();
162+
var serviceProvider = services.BuildServiceProvider();
163+
164+
var renderer = new TestRenderer(serviceProvider);
165+
var component = (ComponentWithThrowingDisposeAsyncCore)renderer.InstantiateComponent<ComponentWithThrowingDisposeAsyncCore>();
166+
167+
_ = component.MyService;
168+
169+
// Even if DisposeAsyncCore throws, Dispose(true) should still be called
170+
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
171+
await ((IAsyncDisposable)component).DisposeAsync());
172+
173+
// Dispose should have been called due to try-finally
174+
Assert.True(component.DisposingParameter);
175+
Assert.True(component.IsDisposedPublic);
176+
}
177+
130178
private class ComponentWithDispose : OwningComponentBase<MyService>
131179
{
132180
public MyService MyService => Service;
133181
public bool? DisposingParameter { get; private set; }
182+
public int DisposeCallCount { get; private set; }
183+
184+
protected override void Dispose(bool disposing)
185+
{
186+
DisposingParameter = disposing;
187+
DisposeCallCount++;
188+
base.Dispose(disposing);
189+
}
190+
}
191+
192+
private class ComponentWithThrowingDisposeAsyncCore : OwningComponentBase<MyService>
193+
{
194+
public MyService MyService => Service;
195+
public bool? DisposingParameter { get; private set; }
196+
public bool IsDisposedPublic => IsDisposed;
197+
198+
protected override async ValueTask DisposeAsyncCore()
199+
{
200+
await base.DisposeAsyncCore();
201+
throw new InvalidOperationException("Something went wrong in async disposal");
202+
}
134203

135204
protected override void Dispose(bool disposing)
136205
{

0 commit comments

Comments
 (0)