Repository: dotnet/runtime
File: src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderEngineScopeTests.cs (C#)

Code before:
            Assert.Equal(TestDisposable.ErrorMessage, exception.Message);
            Assert.All(disposables, disposable => Assert.True(disposable.IsDisposed));
        }

        [Fact]
        public async Task DisposeAsync_TwoServicesThrows_DisposesAllAndThrowsAggregateException()
        {
            var services = new ServiceCollection();
            services.AddKeyedTransient("throws", (_, _) => new TestDisposable(true));
            services.AddKeyedTransient("doesnotthrow", (_, _) => new TestDisposable(false));

            var scope = services.BuildServiceProvider().GetRequiredService<IServiceScopeFactory>().CreateScope().ServiceProvider;

            var disposables = new TestDisposable[]
            {
                scope.GetRequiredKeyedService<TestDisposable>("throws"),
                scope.GetRequiredKeyedService<TestDisposable>("doesnotthrow"),
                scope.GetRequiredKeyedService<TestDisposable>("throws"),
                scope.GetRequiredKeyedService<TestDisposable>("doesnotthrow"),
            };

            var exception = await Assert.ThrowsAsync<AggregateException>(async () => await ((IAsyncDisposable)scope).DisposeAsync());
            Assert.Equal(2, exception.InnerExceptions.Count);
            Assert.All(exception.InnerExceptions, ex => Assert.IsType<InvalidOperationException>(ex));
            Assert.All(disposables, disposable => Assert.True(disposable.IsDisposed));
        }

        private class TestDisposable : IDisposable, IAsyncDisposable
        {
            public const string ErrorMessage = "Dispose failed.";

            private readonly bool _throwsOnDispose;

            public bool IsDisposed { get; private set; }

            public TestDisposable(bool throwsOnDispose)
            {
                _throwsOnDispose = throwsOnDispose;
            }

            public void Dispose()
            {
                IsDisposed = true;

                if (_throwsOnDispose)
                {
                    throw new InvalidOperationException(ErrorMessage);
                }
            }

            public ValueTask DisposeAsync()

Review comment (refactor):
The new `DisposeAsync_*` tests only exercise the path where `DisposeAsync` completes synchronously (the returned `ValueTask` is already completed), so they never hit the asynchronous branch where `IAsyncDisposable.DisposeAsync` returns an incomplete `ValueTask` and disposal continues in the `Await` helper. To ensure the new "dispose all and aggregate exceptions" behavior also holds for truly asynchronous disposals, consider adding a test with a custom `IAsyncDisposable` whose `DisposeAsync` returns an incomplete `ValueTask` and later throws, and verify that all disposables are disposed and their exceptions are aggregated rather than short-circuited.

Code after:

            var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () => await ((IAsyncDisposable)scope).DisposeAsync());
            Assert.Equal(TestDisposable.ErrorMessage, exception.Message);
            Assert.All(disposables, disposable => Assert.True(disposable.IsDisposed));
        }

        [Theory]
        [InlineData(true)]
        [InlineData(false)]
        public async Task DisposeAsync_TwoServicesThrows_DisposesAllAndThrowsAggregateException(bool synchronous)
        {
            var services = new ServiceCollection();
            services.AddKeyedTransient("throws", (_, _) => new TestDisposable(true, synchronous));
            services.AddKeyedTransient("doesnotthrow", (_, _) => new TestDisposable(false, synchronous));

            var scope = services.BuildServiceProvider().GetRequiredService<IServiceScopeFactory>().CreateScope().ServiceProvider;

            var disposables = new TestDisposable[]
            {
                scope.GetRequiredKeyedService<TestDisposable>("throws"),
                scope.GetRequiredKeyedService<TestDisposable>("doesnotthrow"),
                scope.GetRequiredKeyedService<TestDisposable>("throws"),
                scope.GetRequiredKeyedService<TestDisposable>("doesnotthrow"),
            };

            var exception = await Assert.ThrowsAsync<AggregateException>(async () => await ((IAsyncDisposable)scope).DisposeAsync());
            Assert.Equal(2, exception.InnerExceptions.Count);
            Assert.All(exception.InnerExceptions, ex => Assert.IsType<InvalidOperationException>(ex));
            Assert.All(disposables, disposable => Assert.True(disposable.IsDisposed));
        }

        private class TestDisposable : IDisposable, IAsyncDisposable
        {
            public const string ErrorMessage = "Dispose failed.";

            private readonly bool _throwsOnDispose;
            private readonly bool _synchronous;

            public bool IsDisposed { get; private set; }

            public TestDisposable(bool throwsOnDispose = false, bool synchronous = false)
            {
                _throwsOnDispose = throwsOnDispose;
                _synchronous = synchronous;
            }

            public void Dispose()
            {
                IsDisposed = true;

                if (_throwsOnDispose)