@@ -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