Skip to content

Commit 5b06d1d

Browse files
ilonatommyCopilot
andauthored
[Blazor] OwningComponentBase Dispose method in .NET 10 gets back the original behavior (#64695)
* Unit test that reproduces the problem. * Feedback: Add more non-trivial cases tests. * Remove unit testing the buggy behavior. * Feedback: Trust framework to not to call dispose more than once. --------- Co-authored-by: Copilot <[email protected]>
1 parent fab9e0e commit 5b06d1d

File tree

2 files changed

+121
-5
lines changed

2 files changed

+121
-5
lines changed

src/Components/Components/src/OwningComponentBase.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ protected IServiceProvider ScopedServices
4444
}
4545
}
4646

47-
/// <inhertidoc />
47+
/// <inheritdoc />
4848
void IDisposable.Dispose()
4949
{
5050
Dispose(disposing: true);
@@ -69,12 +69,12 @@ protected virtual void Dispose(bool disposing)
6969
}
7070
}
7171

72-
/// <inhertidoc />
72+
/// <inheritdoc />
7373
async ValueTask IAsyncDisposable.DisposeAsync()
7474
{
7575
await DisposeAsyncCore().ConfigureAwait(false);
7676

77-
Dispose(disposing: false);
77+
Dispose(disposing: true);
7878
GC.SuppressFinalize(this);
7979
}
8080

@@ -89,8 +89,6 @@ protected virtual async ValueTask DisposeAsyncCore()
8989
await _scope.Value.DisposeAsync().ConfigureAwait(false);
9090
_scope = null;
9191
}
92-
93-
IsDisposed = true;
9492
}
9593
}
9694

src/Components/Components/test/OwningComponentBaseTest.cs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,129 @@ public MyService(Counter counter)
111111
void IDisposable.Dispose() => Counter.DisposedCount++;
112112
}
113113

114+
[Fact]
115+
public async Task DisposeAsync_CallsDispose_WithDisposingTrue()
116+
{
117+
var services = new ServiceCollection();
118+
services.AddSingleton<Counter>();
119+
services.AddTransient<MyService>();
120+
var serviceProvider = services.BuildServiceProvider();
121+
122+
var renderer = new TestRenderer(serviceProvider);
123+
var component = (ComponentWithDispose)renderer.InstantiateComponent<ComponentWithDispose>();
124+
125+
_ = component.MyService;
126+
await ((IAsyncDisposable)component).DisposeAsync();
127+
Assert.True(component.DisposingParameter);
128+
}
129+
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+
await ((IAsyncDisposable)component).DisposeAsync();
145+
var firstCallCount = component.DisposeCallCount;
146+
Assert.Equal(1, counter.DisposedCount);
147+
148+
((IDisposable)component).Dispose();
149+
Assert.True(component.DisposeCallCount >= firstCallCount);
150+
Assert.Equal(1, counter.DisposedCount);
151+
}
152+
153+
private class ComponentWithDispose : OwningComponentBase<MyService>
154+
{
155+
public MyService MyService => Service;
156+
public bool? DisposingParameter { get; private set; }
157+
public int DisposeCallCount { get; private set; }
158+
159+
protected override void Dispose(bool disposing)
160+
{
161+
DisposingParameter = disposing;
162+
DisposeCallCount++;
163+
base.Dispose(disposing);
164+
}
165+
}
166+
114167
private class MyOwningComponent : OwningComponentBase<MyService>
115168
{
116169
public MyService MyService => Service;
117170

118171
// Expose IsDisposed for testing
119172
public bool IsDisposedPublic => IsDisposed;
120173
}
174+
175+
[Fact]
176+
public async Task ComplexComponent_DisposesResourcesOnlyWhenDisposingIsTrue()
177+
{
178+
var services = new ServiceCollection();
179+
services.AddSingleton<Counter>();
180+
services.AddTransient<MyService>();
181+
var serviceProvider = services.BuildServiceProvider();
182+
183+
var renderer = new TestRenderer(serviceProvider);
184+
var component = (ComplexComponent)renderer.InstantiateComponent<ComplexComponent>();
185+
186+
_ = component.MyService;
187+
188+
await ((IAsyncDisposable)component).DisposeAsync();
189+
190+
// Verify all managed resources were disposed because disposing=true
191+
Assert.True(component.TimerDisposed);
192+
Assert.True(component.CancellationTokenSourceDisposed);
193+
Assert.True(component.EventUnsubscribed);
194+
Assert.Equal(1, component.ManagedResourcesCleanedUpCount);
195+
}
196+
197+
private class ComplexComponent : OwningComponentBase<MyService>
198+
{
199+
private readonly System.Threading.Timer _timer;
200+
private readonly CancellationTokenSource _cts;
201+
private bool _eventSubscribed;
202+
203+
public MyService MyService => Service;
204+
public bool TimerDisposed { get; private set; }
205+
public bool CancellationTokenSourceDisposed { get; private set; }
206+
public bool EventUnsubscribed { get; private set; }
207+
public int ManagedResourcesCleanedUpCount { get; private set; }
208+
209+
public ComplexComponent()
210+
{
211+
_timer = new System.Threading.Timer(_ => { }, null, Timeout.Infinite, Timeout.Infinite);
212+
_cts = new CancellationTokenSource();
213+
_eventSubscribed = true;
214+
}
215+
216+
protected override void Dispose(bool disposing)
217+
{
218+
if (disposing)
219+
{
220+
_timer?.Dispose();
221+
TimerDisposed = true;
222+
223+
_cts?.Cancel();
224+
_cts?.Dispose();
225+
CancellationTokenSourceDisposed = true;
226+
227+
if (_eventSubscribed)
228+
{
229+
EventUnsubscribed = true;
230+
_eventSubscribed = false;
231+
}
232+
233+
ManagedResourcesCleanedUpCount++;
234+
}
235+
236+
base.Dispose(disposing);
237+
}
238+
}
121239
}

0 commit comments

Comments
 (0)