Closed
Description
Blazor doesn't call DisposeAsync
on the DI scope, which leads to the following exception being logged during Circuit disposal:
fail: Microsoft.AspNetCore.Components.Server.Circuits.CircuitRegistry[100]
Unhandled exception disposing circuit host: 'blazor_async_dispose.MyScopedDisposableService' type only implements IAsyncDisposable. Use DisposeAsync to dispose the container.
System.InvalidOperationException: 'blazor_async_dispose.MyScopedDisposableService' type only implements IAsyncDisposable. Use DisposeAsync to dispose the container.
at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.Dispose()
at Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost.<DisposeAsync>b__51_0()
at Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContext.<>c.<<InvokeAsync>b__9_0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost.DisposeAsync()
at Microsoft.AspNetCore.Components.Server.Circuits.CircuitRegistry.DisposeCircuitEntry(DisconnectedCircuitEntry entry)
The DI scope/container is rigid about which kind of disposal you use depending on the services you have. This matrix lays out what happens. Across the top are the kinds of services you have, down the left is how you dispose the scope:
How you dispose the scope | Implement IDisposable only |
Implement both IAsyncDisposable and IDisposable |
Implement IAsyncDisposable only |
---|---|---|---|
DisposeAsync |
OK (calls Dispose ) |
OK (calls DisposeAsync ) |
OK (calls DisposeAsync ) |
Dispose |
OK (calls Dispose ) |
OK (calls Dispose ) |
❌Throws❌ |
It's definitely arguable that throwing this exception in DI isn't appropriate. I just want to open the discussion somewhere.
Repro Steps
dotnet new blazorserver
- Add file
MyScopedDisposableService.cs
:
public class MyScopedDisposableService : IAsyncDisposable
{
public MyScopedDisposableService()
{
Console.WriteLine("Scoped Service Constructed");
}
public ValueTask DisposeAsync()
{
Console.WriteLine("Scoped Service Disposed async!");
return default;
}
}
- Register in
ConfigureServices
:
services.AddScoped<MyScopedDisposableService>();
- Inject into
Pages/Index.razor
:
@inject MyScopedDisposableService Service
- Launch the app.
- Navigate to the index page.
- Close the tab.
- Wait about ~5 minutes for the circuit to get disposed.
Expected Behavior
No error is logged
Actual Behavior
The following error is logged:
fail: Microsoft.AspNetCore.Components.Server.Circuits.CircuitRegistry[100]
Unhandled exception disposing circuit host: 'blazor_async_dispose.MyScopedDisposableService' type only implements IAsyncDisposable. Use DisposeAsync to dispose the container.
System.InvalidOperationException: 'blazor_async_dispose.MyScopedDisposableService' type only implements IAsyncDisposable. Use DisposeAsync to dispose the container.
at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.Dispose()
at Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost.<DisposeAsync>b__51_0()
at Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContext.<>c.<<InvokeAsync>b__9_0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost.DisposeAsync()
at Microsoft.AspNetCore.Components.Server.Circuits.CircuitRegistry.DisposeCircuitEntry(DisconnectedCircuitEntry entry)