Skip to content

ServiceProviderEngineScope should aggregate exceptions in Dispose rather than throwing on the first. #86426

Description

@swsyn

I understand that there is CA1065 rule

but

We run into errors during disposing some services in our Blazor Server App. We can wrap all our code in Dispose with try/catch but there can be third party services. It comes quite difficult to find the reason of some services were not disposed (also because Blazor's CircuitHost logs _disposeFailed with Debug level).

It seems difficult to assume that Dispose() will never throw an exception somewhere.

Activity

  1. ghost added
    untriagedNew issue has not been triaged by the area owner
    on May 18, 2023
  2. ericstj commented on May 23, 2023

    @ericstj
    Member

    I don't think we'll want framework components to swallow exceptions here. Is this an ask to try to capture an exception and rethrow as AggregateException when disposing all services in

    public void Dispose()
    {
    List<object>? toDispose = BeginDispose();
    if (toDispose != null)
    {
    for (int i = toDispose.Count - 1; i >= 0; i--)
    {
    if (toDispose[i] is IDisposable disposable)
    {
    disposable.Dispose();
    }
    else
    {
    throw new InvalidOperationException(SR.Format(SR.AsyncDisposableServiceDispose, TypeNameHelper.GetTypeDisplayName(toDispose[i])));
    }
    }
    }
    }
    public ValueTask DisposeAsync()
    {
    List<object>? toDispose = BeginDispose();
    if (toDispose != null)
    {
    try
    {
    for (int i = toDispose.Count - 1; i >= 0; i--)
    {
    object disposable = toDispose[i];
    if (disposable is IAsyncDisposable asyncDisposable)
    {
    ValueTask vt = asyncDisposable.DisposeAsync();
    if (!vt.IsCompletedSuccessfully)
    {
    return Await(i, vt, toDispose);
    }
    // If its a IValueTaskSource backed ValueTask,
    // inform it its result has been read so it can reset
    vt.GetAwaiter().GetResult();
    }
    else
    {
    ((IDisposable)disposable).Dispose();
    }
    }
    }
    catch (Exception ex)
    {
    return new ValueTask(Task.FromException(ex));
    }
    }
    return default;
    static async ValueTask Await(int i, ValueTask vt, List<object> toDispose)
    {
    await vt.ConfigureAwait(false);
    // vt is acting on the disposable at index i,
    // decrement it and move to the next iteration
    i--;
    for (; i >= 0; i--)
    {
    object disposable = toDispose[i];
    if (disposable is IAsyncDisposable asyncDisposable)
    {
    await asyncDisposable.DisposeAsync().ConfigureAwait(false);
    }
    else
    {
    ((IDisposable)disposable).Dispose();
    }
    }
    }
    }
    ?

    Of is this an ask to make the Blazor CircuitHost to make the dispose exception more visible: https://github.com/dotnet/aspnetcore/blob/39564d529f84f7a3bbac5b28ba11060e8ac30375/src/Components/Server/src/Circuits/CircuitHost.cs#L199

  3. swsyn commented on May 24, 2023

    @swsyn
    Author

    Yes, it was about both aforementioned variants:

    1. We expected that the scope would try to dispose all disposable services and then throw an exception. But the scope just stop disposing.

    It is important for us because we have some services which start timers and we need to stop them when the services are disposed. But they are not disposed because of broken service.

    public class Service: IDisposable
    {
    	private readonly ITimerService timerService;
    	private ITimer timer;
    	public TimeSpan UpdateInterval { get; set; } = TimeSpan.FromMinutes(2);
    
    	public Task SetupAsync()
    	{
    		timer = timerService.Create();
    		timer.Interval = UpdateInterval;
    		timer.Tick += TimerOnTick;
    		timer.IsEnabled = true;
    
    		return Task.CompletedTask;
    	}
    	
    	private void TimerOnTick(object sender, EventArgs e) { }
    
    	public void Dispose()
    	{
    		if (timer!=null) {
    			timer.IsEnabled = false;
    			timer.Tick -= TimerOnTick;
    			timer = null;
    		}
    	}
    }
    

    We have modular app structure and some services can be written by other developers.

    1. Yes, we also expected that Blazor CircuitHost dispose exception would be more visible.
  4. added and removed
    untriagedNew issue has not been triaged by the area owner
    on May 24, 2023
  5. ericstj commented on May 24, 2023

    @ericstj
    Member

    @swsyn can you open a separate issue in https://github.com/dotnet/aspnetcore with an ask for Blazor CircuitHost to stop hiding these exceptions?

    Let this issue track a request to change the behavior of ServiceProviderEngineScope to not halt on the first Dispose exception, but to instead proceed and aggregate exceptions that might occur.

  6. changed the title [-]Exception in Dispose() prevents disposing of all other services[/-] [+]ServiceProviderEngineScope should aggregate exceptions in Dispose rather than throwing on the first.[/+] on May 24, 2023
  7. added
    needs-further-triageIssue has been initially triaged, but needs deeper consideration or reconsideration
    on May 24, 2023
  8. swsyn commented on May 25, 2023

    @swsyn
    Author
  9. steveharter commented on Aug 7, 2023

    @steveharter
    Contributor

    Moving to 9.0; ServiceProviderEngineScope should try to run Dispose() on all services.

  10. added this to the 9.0.0 milestone on Aug 7, 2023
  11. modified the milestones: 9.0.0, 10.0.0 on Jul 25, 2024
  12. noxe commented on Sep 10, 2024

    @noxe

    we had the same problem today - if one service throws an exception - all other services do not call Dispose any more!

  13. modified the milestones: 10.0.0, Future on Jul 26, 2025
  14. added
    in-prThere is an active PR which will close this issue when it is merged
    on Jan 19, 2026
  15. self-assigned this
    on Jan 19, 2026
  16. modified the milestones: Future, 11.0.0 on Feb 25, 2026
  17. locked and limited conversation to collaborators on Mar 28, 2026
  18. rosebyte commented on Aug 26, 2026

    @rosebyte
    Member

    Created a breaking change issue dotnet/docs#55654.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area-Extensions-DependencyInjectionfeature-requestin-prThere is an active PR which will close this issue when it is mergedneeds-further-triageIssue has been initially triaged, but needs deeper consideration or reconsideration

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions