diff --git a/src/Components/Components/src/OwningComponentBase.cs b/src/Components/Components/src/OwningComponentBase.cs
index b2e9d9ef554a..a60f396e89ec 100644
--- a/src/Components/Components/src/OwningComponentBase.cs
+++ b/src/Components/Components/src/OwningComponentBase.cs
@@ -44,7 +44,7 @@ protected IServiceProvider ScopedServices
}
}
- ///
+ ///
void IDisposable.Dispose()
{
Dispose(disposing: true);
@@ -69,12 +69,20 @@ protected virtual void Dispose(bool disposing)
}
}
- ///
+ ///
async ValueTask IAsyncDisposable.DisposeAsync()
{
- await DisposeAsyncCore().ConfigureAwait(false);
-
- Dispose(disposing: false);
+ if (!IsDisposed)
+ {
+ try
+ {
+ await DisposeAsyncCore().ConfigureAwait(false);
+ }
+ finally
+ {
+ Dispose(disposing: true);
+ }
+ }
GC.SuppressFinalize(this);
}
@@ -89,8 +97,6 @@ protected virtual async ValueTask DisposeAsyncCore()
await _scope.Value.DisposeAsync().ConfigureAwait(false);
_scope = null;
}
-
- IsDisposed = true;
}
}
diff --git a/src/Components/Components/test/OwningComponentBaseTest.cs b/src/Components/Components/test/OwningComponentBaseTest.cs
index ae9a2b4a67ac..8064f75cb2c3 100644
--- a/src/Components/Components/test/OwningComponentBaseTest.cs
+++ b/src/Components/Components/test/OwningComponentBaseTest.cs
@@ -111,6 +111,98 @@ public MyService(Counter counter)
void IDisposable.Dispose() => Counter.DisposedCount++;
}
+ [Fact]
+ public async Task DisposeAsync_CallsDispose_WithDisposingTrue()
+ {
+ var services = new ServiceCollection();
+ services.AddSingleton();
+ services.AddTransient();
+ var serviceProvider = services.BuildServiceProvider();
+
+ var renderer = new TestRenderer(serviceProvider);
+ var component = (ComponentWithDispose)renderer.InstantiateComponent();
+
+ _ = component.MyService;
+ await ((IAsyncDisposable)component).DisposeAsync();
+ Assert.True(component.DisposingParameter);
+ }
+
+ [Fact]
+ public async Task DisposeAsync_ThenDispose_IsIdempotent()
+ {
+ var services = new ServiceCollection();
+ services.AddSingleton();
+ services.AddTransient();
+ var serviceProvider = services.BuildServiceProvider();
+
+ var counter = serviceProvider.GetRequiredService();
+ var renderer = new TestRenderer(serviceProvider);
+ var component = (ComponentWithDispose)renderer.InstantiateComponent();
+
+ _ = component.MyService;
+
+ await ((IAsyncDisposable)component).DisposeAsync();
+ var firstCallCount = component.DisposeCallCount;
+ Assert.Equal(1, counter.DisposedCount);
+
+ ((IDisposable)component).Dispose();
+ Assert.True(component.DisposeCallCount >= firstCallCount);
+ Assert.Equal(1, counter.DisposedCount);
+ }
+
+ [Fact]
+ public async Task DisposeAsyncCore_Override_WithException_StillCallsDispose()
+ {
+ var services = new ServiceCollection();
+ services.AddSingleton();
+ services.AddTransient();
+ var serviceProvider = services.BuildServiceProvider();
+
+ var renderer = new TestRenderer(serviceProvider);
+ var component = (ComponentWithThrowingDisposeAsyncCore)renderer.InstantiateComponent();
+
+ _ = component.MyService;
+
+ await Assert.ThrowsAsync(async () =>
+ await ((IAsyncDisposable)component).DisposeAsync());
+
+ Assert.True(component.DisposingParameter);
+ Assert.True(component.IsDisposedPublic);
+ }
+
+ private class ComponentWithDispose : OwningComponentBase
+ {
+ public MyService MyService => Service;
+ public bool? DisposingParameter { get; private set; }
+ public int DisposeCallCount { get; private set; }
+
+ protected override void Dispose(bool disposing)
+ {
+ DisposingParameter = disposing;
+ DisposeCallCount++;
+ base.Dispose(disposing);
+ }
+ }
+
+ private class ComponentWithThrowingDisposeAsyncCore : OwningComponentBase
+ {
+ public MyService MyService => Service;
+ public bool? DisposingParameter { get; private set; }
+ public bool IsDisposedPublic => IsDisposed;
+
+ protected override async ValueTask DisposeAsyncCore()
+ {
+ await base.DisposeAsyncCore();
+ throw new InvalidOperationException("Something went wrong in async disposal");
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ DisposingParameter = disposing;
+ base.Dispose(disposing);
+ }
+ }
+
private class MyOwningComponent : OwningComponentBase
{
public MyService MyService => Service;
@@ -118,4 +210,69 @@ private class MyOwningComponent : OwningComponentBase
// Expose IsDisposed for testing
public bool IsDisposedPublic => IsDisposed;
}
+
+ [Fact]
+ public async Task ComplexComponent_DisposesResourcesOnlyWhenDisposingIsTrue()
+ {
+ var services = new ServiceCollection();
+ services.AddSingleton();
+ services.AddTransient();
+ var serviceProvider = services.BuildServiceProvider();
+
+ var renderer = new TestRenderer(serviceProvider);
+ var component = (ComplexComponent)renderer.InstantiateComponent();
+
+ _ = component.MyService;
+
+ await ((IAsyncDisposable)component).DisposeAsync();
+
+ // Verify all managed resources were disposed because disposing=true
+ Assert.True(component.TimerDisposed);
+ Assert.True(component.CancellationTokenSourceDisposed);
+ Assert.True(component.EventUnsubscribed);
+ Assert.Equal(1, component.ManagedResourcesCleanedUpCount);
+ }
+
+ private class ComplexComponent : OwningComponentBase
+ {
+ private readonly System.Threading.Timer _timer;
+ private readonly CancellationTokenSource _cts;
+ private bool _eventSubscribed;
+
+ public MyService MyService => Service;
+ public bool TimerDisposed { get; private set; }
+ public bool CancellationTokenSourceDisposed { get; private set; }
+ public bool EventUnsubscribed { get; private set; }
+ public int ManagedResourcesCleanedUpCount { get; private set; }
+
+ public ComplexComponent()
+ {
+ _timer = new System.Threading.Timer(_ => { }, null, Timeout.Infinite, Timeout.Infinite);
+ _cts = new CancellationTokenSource();
+ _eventSubscribed = true;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _timer?.Dispose();
+ TimerDisposed = true;
+
+ _cts?.Cancel();
+ _cts?.Dispose();
+ CancellationTokenSourceDisposed = true;
+
+ if (_eventSubscribed)
+ {
+ EventUnsubscribed = true;
+ _eventSubscribed = false;
+ }
+
+ ManagedResourcesCleanedUpCount++;
+ }
+
+ base.Dispose(disposing);
+ }
+ }
}