diff --git a/src/Components/Components/src/OwningComponentBase.cs b/src/Components/Components/src/OwningComponentBase.cs
index 739f0614258d..b2e9d9ef554a 100644
--- a/src/Components/Components/src/OwningComponentBase.cs
+++ b/src/Components/Components/src/OwningComponentBase.cs
@@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Components;
/// requires disposal such as a repository or database abstraction. Using
/// as a base class ensures that the service provider scope is disposed with the component.
///
-public abstract class OwningComponentBase : ComponentBase, IDisposable
+public abstract class OwningComponentBase : ComponentBase, IDisposable, IAsyncDisposable
{
private AsyncServiceScope? _scope;
@@ -44,20 +44,53 @@ protected IServiceProvider ScopedServices
}
}
+ ///
void IDisposable.Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Releases the service scope used by the component.
+ ///
+ /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
+ protected virtual void Dispose(bool disposing)
{
if (!IsDisposed)
{
- _scope?.Dispose();
- _scope = null;
- Dispose(disposing: true);
+ if (disposing && _scope.HasValue && _scope.Value is IDisposable disposable)
+ {
+ disposable.Dispose();
+ _scope = null;
+ }
+
IsDisposed = true;
}
}
- ///
- protected virtual void Dispose(bool disposing)
+ ///
+ async ValueTask IAsyncDisposable.DisposeAsync()
{
+ await DisposeAsyncCore().ConfigureAwait(false);
+
+ Dispose(disposing: false);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Asynchronously releases the service scope used by the component.
+ ///
+ /// A task that represents the asynchronous dispose operation.
+ protected virtual async ValueTask DisposeAsyncCore()
+ {
+ if (!IsDisposed && _scope.HasValue)
+ {
+ await _scope.Value.DisposeAsync().ConfigureAwait(false);
+ _scope = null;
+ }
+
+ IsDisposed = true;
}
}
diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt
index f11613946794..76ae63ac245a 100644
--- a/src/Components/Components/src/PublicAPI.Unshipped.txt
+++ b/src/Components/Components/src/PublicAPI.Unshipped.txt
@@ -19,5 +19,6 @@ Microsoft.AspNetCore.Components.Infrastructure.PersistentStateProviderServiceCol
static Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.AspNetCore.Components.IComponentRenderMode! componentRenderMode) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsMetrics(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsTracing(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
+virtual Microsoft.AspNetCore.Components.OwningComponentBase.DisposeAsyncCore() -> System.Threading.Tasks.ValueTask
static Microsoft.AspNetCore.Components.Infrastructure.PersistentStateProviderServiceCollectionExtensions.AddSupplyValueFromPersistentComponentStateProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.GetComponentKey() -> object?
diff --git a/src/Components/Components/test/OwningComponentBaseTest.cs b/src/Components/Components/test/OwningComponentBaseTest.cs
index 7bd68ad750c7..ae9a2b4a67ac 100644
--- a/src/Components/Components/test/OwningComponentBaseTest.cs
+++ b/src/Components/Components/test/OwningComponentBaseTest.cs
@@ -29,6 +29,69 @@ public void CreatesScopeAndService()
Assert.Equal(1, counter.DisposedCount);
}
+ [Fact]
+ public async Task DisposeAsyncReleasesScopeAndService()
+ {
+ var services = new ServiceCollection();
+ services.AddSingleton();
+ services.AddTransient();
+ var serviceProvider = services.BuildServiceProvider();
+
+ var counter = serviceProvider.GetRequiredService();
+ var renderer = new TestRenderer(serviceProvider);
+ var component1 = (MyOwningComponent)renderer.InstantiateComponent();
+
+ Assert.NotNull(component1.MyService);
+ Assert.Equal(1, counter.CreatedCount);
+ Assert.Equal(0, counter.DisposedCount);
+ Assert.False(component1.IsDisposedPublic);
+
+ await ((IAsyncDisposable)component1).DisposeAsync();
+ Assert.Equal(1, counter.CreatedCount);
+ Assert.Equal(1, counter.DisposedCount);
+ Assert.True(component1.IsDisposedPublic);
+ }
+
+ [Fact]
+ public void ThrowsWhenAccessingScopedServicesAfterDispose()
+ {
+ var services = new ServiceCollection();
+ services.AddSingleton();
+ services.AddTransient();
+ var serviceProvider = services.BuildServiceProvider();
+
+ var renderer = new TestRenderer(serviceProvider);
+ var component1 = (MyOwningComponent)renderer.InstantiateComponent();
+
+ // Access service first to create scope
+ var service = component1.MyService;
+
+ ((IDisposable)component1).Dispose();
+
+ // Should throw when trying to access services after disposal
+ Assert.Throws(() => component1.MyService);
+ }
+
+ [Fact]
+ public async Task ThrowsWhenAccessingScopedServicesAfterDisposeAsync()
+ {
+ var services = new ServiceCollection();
+ services.AddSingleton();
+ services.AddTransient();
+ var serviceProvider = services.BuildServiceProvider();
+
+ var renderer = new TestRenderer(serviceProvider);
+ var component1 = (MyOwningComponent)renderer.InstantiateComponent();
+
+ // Access service first to create scope
+ var service = component1.MyService;
+
+ await ((IAsyncDisposable)component1).DisposeAsync();
+
+ // Should throw when trying to access services after disposal
+ Assert.Throws(() => component1.MyService);
+ }
+
private class Counter
{
public int CreatedCount { get; set; }
@@ -51,5 +114,8 @@ public MyService(Counter counter)
private class MyOwningComponent : OwningComponentBase
{
public MyService MyService => Service;
+
+ // Expose IsDisposed for testing
+ public bool IsDisposedPublic => IsDisposed;
}
}