Repository: dotnet/runtime
File: src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs (C#)

Code before:

                ThrowHelper.ThrowObjectDisposedException();
            }

            return service;
        }

        public void Dispose()
        {
            List<object>? toDispose = BeginDispose();
            List<Exception>? exceptions = null;
            var index = (toDispose?.Count ?? 0) - 1;

            while (index >= 0)
            {
                try
                {
                    for (; index >= 0; index--)
                    {
                        if (toDispose![index] is IDisposable disposable)
                        {
                            disposable.Dispose();
                        }
                        else
                        {
                            throw new InvalidOperationException(SR.Format(SR.AsyncDisposableServiceDispose, TypeNameHelper.GetTypeDisplayName(toDispose[index])));
                        }
                    }
                }
                catch (Exception ex)
                {
                    exceptions ??= new List<Exception>();
                    exceptions.Add(ex);
                    index--;
                }
            }

            if (exceptions is null)
            {
                return;
            }

            if (exceptions.Count == 1)
            {
                throw exceptions[0];
            }

            throw new AggregateException(exceptions);
        }

        public ValueTask DisposeAsync()

Review comment (security):
Do I understand correctly that before, if any of the services happened to be IAsyncDisposable-only, it literally was breaking all the further disposals? 😅

Should we add a test for this, and also for the mix of IDisposable-only and IAsyncDisposable, to verify it works?

---

Also, I found that we actually state in the [docs](https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-disposeasync) regarding IAsyncDisposables in DI:

> Specifically, implementations of [IDisposable](https://learn.microsoft.com/en-us/dotnet/api/system.idisposable) and [IAsyncDisposable](https://learn.microsoft.com/en-us/dotnet/api/system.iasyncdisposable) are properly disposed at the end of their specified lifetime.

The way I'd interpret it, we promise here to clean up IAsyncDisposables regardless, but the implementation doesn't follow on this promise? 🤔

There is no warnings about this exception in the [`AsyncServiceScope.Dispose()`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.asyncservicescope.dispose?view=net-10.0-pp#microsoft-extensions-dependencyinjection-asyncservicescope-dispose) API docs either... Is it documented somewhere?..

It might be smth out of scope of this PR, but it feels like something we might potentially want to fix? Especially given we already handle this case above in the `CaptureDisposable` method.

---

Re: "`catch` outside of `for`" optimization -- I'd vote for keeping it simple, i.e. ordinary `for`/`foreach`, and `catch` inside it, unless are we actually sure this will give any kind of perf gain (i.e. a microbenchmark). Am I missing smth? I don't think I've seen this pattern in the libraries, e.g. here is a straightforward foreach:

https://github.com/dotnet/runtime/blob/531dd278ada113a634e5bd451ee0838c28924bc9/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/DefaultPartitionedRateLimiter.cs#L128-L139


Code after:
                }

                ThrowHelper.ThrowObjectDisposedException();
            }

            return service;
        }

        public void Dispose()
        {
            List<object>? toDispose = BeginDispose();
            if (toDispose is null)
            {
                return;
            }

            object? exceptionsCache = null;
            for (var i = toDispose.Count - 1; i >= 0; i--)
            {
                try
                {
                    if (toDispose[i] is IDisposable disposable)
                    {
                        disposable.Dispose();
                    }
                    else
                    {
                        throw new InvalidOperationException(SR.Format(SR.AsyncDisposableServiceDispose, TypeNameHelper.GetTypeDisplayName(toDispose[i])));
                    }
                }
                catch (Exception exception)
                {
                    AddExceptionToCache(ref exceptionsCache, exception);
                }
            }

            CheckExceptionCache(exceptionsCache);
        }

        public ValueTask DisposeAsync()
        {
            List<object>? toDispose = BeginDispose();
            if (toDispose is null)
            {
                return default;
            }

            object? exceptionsCache = null;
            for (var i = toDispose.Count - 1; i >= 0; i--)
            {
                try