Description
Starting in .NET 11 Preview 2, the built-in dependency injection container continues disposing services after a service's Dispose() or DisposeAsync() method throws. The change applies when disposing service scopes and root service providers, both synchronously and asynchronously.
After all captured disposable services have been processed, a single disposal exception is rethrown unchanged. If multiple disposal operations fail, an AggregateException containing all the exceptions is thrown.
This behaviour was introduced by dotnet/runtime#123342.
Version
Other (please put exact version in description textbox)
Previous behavior
The built-in dependency injection container disposed captured services in reverse order. If a service's Dispose() or DisposeAsync() method threw an exception, disposal stopped immediately. The exception was propagated unchanged, and services earlier in the disposal sequence were not disposed.
For example, suppose both services in this scope throw from Dispose():
_ = scope.ServiceProvider.GetRequiredService<FirstDisposable>();
_ = scope.ServiceProvider.GetRequiredService<SecondDisposable>();
scope.Dispose();
SecondDisposable was disposed first. When it threw, FirstDisposable.Dispose() was not called, and the exception from SecondDisposable was propagated.
New behavior
The built-in dependency injection container attempts to dispose every captured service, even when one or more disposal operations throw. After disposal finishes:
- If exactly one disposal operation failed, its original exception is rethrown.
- If two or more disposal operations failed, an
AggregateException containing each failure is thrown.
In the preceding example, both SecondDisposable.Dispose() and FirstDisposable.Dispose() are called. Because both throw, scope.Dispose() throws an AggregateException containing both exceptions.
The same behaviour applies to asynchronous disposal through DisposeAsync().
Type of breaking change
Reason for change
Although disposal methods are discouraged from throwing exceptions, application and third-party services can still do so. Stopping at the first exception prevented other services from releasing resources such as timers, event handlers, and unmanaged handles. Continuing disposal gives every captured service an opportunity to clean up while still reporting all disposal failures to the caller.
Recommended action
No action is required if disposal methods do not throw and application code does not depend on disposal stopping after the first failure.
Where possible, update services so that their Dispose() and DisposeAsync() implementations do not throw. If application code handles exceptions from scope or service-provider disposal, account for both possible exception shapes:
try
{
scope.Dispose();
}
catch (AggregateException exception)
{
foreach (Exception innerException in exception.InnerExceptions)
{
// Handle or log each disposal failure.
}
}
catch (Exception exception)
{
// Exactly one disposal operation failed.
}
Do not rely on a disposal exception to prevent other services from being disposed. There is no compatibility switch to restore the previous behaviour.
Feature area
Extensions
Affected APIs
Microsoft.Extensions.DependencyInjection.ServiceProvider.Dispose()
Microsoft.Extensions.DependencyInjection.ServiceProvider.DisposeAsync()
Microsoft.Extensions.DependencyInjection.AsyncServiceScope.Dispose()
Microsoft.Extensions.DependencyInjection.AsyncServiceScope.DisposeAsync()
System.IDisposable.Dispose() on Microsoft.Extensions.DependencyInjection.IServiceScope instances created by the built-in dependency injection container
Description
Starting in .NET 11 Preview 2, the built-in dependency injection container continues disposing services after a service's
Dispose()orDisposeAsync()method throws. The change applies when disposing service scopes and root service providers, both synchronously and asynchronously.After all captured disposable services have been processed, a single disposal exception is rethrown unchanged. If multiple disposal operations fail, an
AggregateExceptioncontaining all the exceptions is thrown.This behaviour was introduced by dotnet/runtime#123342.
Version
Other (please put exact version in description textbox)
Previous behavior
The built-in dependency injection container disposed captured services in reverse order. If a service's
Dispose()orDisposeAsync()method threw an exception, disposal stopped immediately. The exception was propagated unchanged, and services earlier in the disposal sequence were not disposed.For example, suppose both services in this scope throw from
Dispose():SecondDisposablewas disposed first. When it threw,FirstDisposable.Dispose()was not called, and the exception fromSecondDisposablewas propagated.New behavior
The built-in dependency injection container attempts to dispose every captured service, even when one or more disposal operations throw. After disposal finishes:
AggregateExceptioncontaining each failure is thrown.In the preceding example, both
SecondDisposable.Dispose()andFirstDisposable.Dispose()are called. Because both throw,scope.Dispose()throws anAggregateExceptioncontaining both exceptions.The same behaviour applies to asynchronous disposal through
DisposeAsync().Type of breaking change
Reason for change
Although disposal methods are discouraged from throwing exceptions, application and third-party services can still do so. Stopping at the first exception prevented other services from releasing resources such as timers, event handlers, and unmanaged handles. Continuing disposal gives every captured service an opportunity to clean up while still reporting all disposal failures to the caller.
Recommended action
No action is required if disposal methods do not throw and application code does not depend on disposal stopping after the first failure.
Where possible, update services so that their
Dispose()andDisposeAsync()implementations do not throw. If application code handles exceptions from scope or service-provider disposal, account for both possible exception shapes:Do not rely on a disposal exception to prevent other services from being disposed. There is no compatibility switch to restore the previous behaviour.
Feature area
Extensions
Affected APIs
Microsoft.Extensions.DependencyInjection.ServiceProvider.Dispose()Microsoft.Extensions.DependencyInjection.ServiceProvider.DisposeAsync()Microsoft.Extensions.DependencyInjection.AsyncServiceScope.Dispose()Microsoft.Extensions.DependencyInjection.AsyncServiceScope.DisposeAsync()System.IDisposable.Dispose()onMicrosoft.Extensions.DependencyInjection.IServiceScopeinstances created by the built-in dependency injection container