@@ -141,16 +141,13 @@ public async Task DisposeAsync_ThenDispose_IsIdempotent()
141141
142142 _ = component . MyService ;
143143
144- // First disposal via DisposeAsync
145144 await ( ( IAsyncDisposable ) component ) . DisposeAsync ( ) ;
146145 var firstCallCount = component . DisposeCallCount ;
147146 Assert . Equal ( 1 , counter . DisposedCount ) ;
148147
149- // Second disposal via Dispose - user override is called but base class prevents double-disposal
150148 ( ( 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
149+ Assert . True ( component . DisposeCallCount >= firstCallCount ) ;
150+ Assert . Equal ( 1 , counter . DisposedCount ) ;
154151 }
155152
156153 [ Fact ]
@@ -166,11 +163,8 @@ public async Task DisposeAsyncCore_Override_WithException_StillCallsDispose()
166163
167164 _ = component . MyService ;
168165
169- // Even if DisposeAsyncCore throws, Dispose(true) should still be called
170- await Assert . ThrowsAsync < InvalidOperationException > ( async ( ) =>
171166 await ( ( IAsyncDisposable ) component ) . DisposeAsync ( ) ) ;
172167
173- // Dispose should have been called due to try-finally
174168 Assert . True ( component . DisposingParameter ) ;
175169 Assert . True ( component . IsDisposedPublic ) ;
176170 }
@@ -215,4 +209,92 @@ private class MyOwningComponent : OwningComponentBase<MyService>
215209 // Expose IsDisposed for testing
216210 public bool IsDisposedPublic => IsDisposed ;
217211 }
212+
213+ [ Fact ]
214+ public async Task ComplexComponent_DisposesResourcesOnlyWhenDisposingIsTrue ( )
215+ {
216+ var services = new ServiceCollection ( ) ;
217+ services . AddSingleton < Counter > ( ) ;
218+ services . AddTransient < MyService > ( ) ;
219+ var serviceProvider = services . BuildServiceProvider ( ) ;
220+
221+ var renderer = new TestRenderer ( serviceProvider ) ;
222+ var component = ( ComplexComponent ) renderer . InstantiateComponent < ComplexComponent > ( ) ;
223+
224+ _ = component . MyService ;
225+
226+ await ( ( IAsyncDisposable ) component ) . DisposeAsync ( ) ;
227+
228+ // Verify all managed resources were disposed because disposing=true
229+ Assert . True ( component . TimerDisposed ) ;
230+ Assert . True ( component . CancellationTokenSourceDisposed ) ;
231+ Assert . True ( component . EventUnsubscribed ) ;
232+ Assert . Equal ( 1 , component . ManagedResourcesCleanedUpCount ) ;
233+ }
234+
235+ [ Fact ]
236+ public void ComplexComponent_WithDisposingFalse_SkipsManagedResourceCleanup ( )
237+ {
238+ var services = new ServiceCollection ( ) ;
239+ services . AddSingleton < Counter > ( ) ;
240+ services . AddTransient < MyService > ( ) ;
241+ var serviceProvider = services . BuildServiceProvider ( ) ;
242+
243+ var renderer = new TestRenderer ( serviceProvider ) ;
244+ var component = ( ComplexComponent ) renderer . InstantiateComponent < ComplexComponent > ( ) ;
245+
246+ _ = component . MyService ;
247+
248+ component . TestDisposeWithFalse ( ) ;
249+
250+ Assert . False ( component . TimerDisposed ) ;
251+ Assert . False ( component . CancellationTokenSourceDisposed ) ;
252+ Assert . False ( component . EventUnsubscribed ) ;
253+ Assert . Equal ( 0 , component . ManagedResourcesCleanedUpCount ) ;
254+ }
255+
256+ private class ComplexComponent : OwningComponentBase < MyService >
257+ {
258+ private readonly System . Threading . Timer _timer ;
259+ private readonly CancellationTokenSource _cts ;
260+ private bool _eventSubscribed ;
261+
262+ public MyService MyService => Service ;
263+ public bool TimerDisposed { get ; private set ; }
264+ public bool CancellationTokenSourceDisposed { get ; private set ; }
265+ public bool EventUnsubscribed { get ; private set ; }
266+ public int ManagedResourcesCleanedUpCount { get ; private set ; }
267+
268+ public ComplexComponent ( )
269+ {
270+ _timer = new System . Threading . Timer ( _ => { } , null , Timeout . Infinite , Timeout . Infinite ) ;
271+ _cts = new CancellationTokenSource ( ) ;
272+ _eventSubscribed = true ;
273+ }
274+
275+ public void TestDisposeWithFalse ( ) => Dispose ( disposing : false ) ;
276+
277+ protected override void Dispose ( bool disposing )
278+ {
279+ if ( disposing )
280+ {
281+ _timer ? . Dispose ( ) ;
282+ TimerDisposed = true ;
283+
284+ _cts ? . Cancel ( ) ;
285+ _cts ? . Dispose ( ) ;
286+ CancellationTokenSourceDisposed = true ;
287+
288+ if ( _eventSubscribed )
289+ {
290+ EventUnsubscribed = true ;
291+ _eventSubscribed = false ;
292+ }
293+
294+ ManagedResourcesCleanedUpCount ++ ;
295+ }
296+
297+ base . Dispose ( disposing ) ;
298+ }
299+ }
218300}
0 commit comments