Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
862df69
add try/catch blocks to disposing loops
Jan 19, 2026
99cdcf3
remove unused using
Jan 19, 2026
54c55a4
Merge branch 'main' into features/86426-di-aggregated-exceptions
rosebyte Jan 19, 2026
244edbe
Bring throw helpers to PUSH_COOP_PINVOKE_FRAME plan (#123015)
am11 Jan 16, 2026
94e3e25
Fix assertions generated by optCreateJumpTableImpliedAssertions (#123…
EgorBo Jan 16, 2026
d6a6535
[main] Source code updates from dotnet/dotnet (#123247)
dotnet-maestro[bot] Jan 16, 2026
0c3e4c2
Update NativeAOT Docker samples to use .NET 10 images (#123241)
Copilot Jan 16, 2026
15af27b
Add multiple environment variable sets to aspnet2 SPMI collection (#1…
Copilot Jan 16, 2026
01b4433
Fix interpreter threadabort in finally (#123231)
janvorli Jan 16, 2026
7797f55
[RyuJit/WASM] Some fixes related to local addresses and stores (#123261)
SingleAccretion Jan 16, 2026
95f82dd
[Startup] Bump PerfMap ahead of DiagnosticServer (#123226)
mdh1418 Jan 16, 2026
0bb8d77
Fix missing static libraries in NativeAOT packs for Apple mobile plat…
Copilot Jan 16, 2026
ac1c4e1
[LoongArch64] Fix the failed for Fp32x2StructFunc in the profiler tes…
LuckyXu-HF Jan 17, 2026
ec0c992
Cleanup __builtin_available where platform is now guaranteed on Apple…
vcsjones Jan 17, 2026
b142b07
Adding C#, F#, VB to StringSyntaxAttribute (#123211)
HakamFostok Jan 17, 2026
08553fc
[RyuJit/WASM] Establish SP & FP and home register arguments (#123270)
SingleAccretion Jan 17, 2026
169cea3
Remove nonExpansive parameter from Dictionary::PopulateEntry (#122758)
Copilot Jan 17, 2026
e56355d
Add early return in TryGetLast for empty results. (#123306)
prozolic Jan 18, 2026
5ea3028
[NativeAOT] Source to native mapping fix for out-of-order code (#123333)
rcj1 Jan 19, 2026
b1ab9b3
Simplify branching in ILImporter.Scanner.cs with Debug.Assert (#123235)
Copilot Jan 19, 2026
a2e63e7
SunOS process and thread support (#105403)
gwr Jan 19, 2026
1d202d4
fix analyzer warnings
Jan 19, 2026
c64cd8f
fix analyzer warnings
Jan 19, 2026
e658b0e
Merge branch 'main' into features/86426-di-aggregated-exceptions
rosebyte Jan 19, 2026
4308779
implement PR comments
Jan 23, 2026
02767e1
Merge branch 'features/86426-di-aggregated-exceptions' of https://git…
Jan 23, 2026
dfea777
Merge branch 'main' into features/86426-di-aggregated-exceptions
rosebyte Jan 23, 2026
c92eac0
implement PR comments
Jan 23, 2026
7a0c529
Merge branch 'features/86426-di-aggregated-exceptions' of https://git…
Jan 23, 2026
549f7c7
run try-catch inside loops
Jan 28, 2026
441655e
Merge branch 'main' into features/86426-di-aggregated-exceptions
rosebyte Jan 28, 2026
d1a6048
Merge branch 'main' into features/86426-di-aggregated-exceptions
rosebyte Jan 29, 2026
1eaea47
Merge branch 'main' into features/86426-di-aggregated-exceptions
rosebyte Jan 29, 2026
f03e2ba
capture exception dispatch info
Jan 30, 2026
2b4e111
simplify stack traces
Jan 30, 2026
8682b4a
add tests for true async disposal
Jan 30, 2026
c29af24
remove unused using
Jan 30, 2026
0bb9e29
Merge branch 'main' into features/86426-di-aggregated-exceptions
rosebyte Jan 30, 2026
6062908
checkpoint
Jan 31, 2026
a8926be
formatting
Jan 31, 2026
9a3d673
Merge branch 'main' into features/86426-di-aggregated-exceptions
rosebyte Jan 31, 2026
c95e5e5
Update src/libraries/Microsoft.Extensions.DependencyInjection/tests/D…
rosebyte Feb 1, 2026
d8aeaf3
Merge branch 'main' into features/86426-di-aggregated-exceptions
rosebyte Feb 1, 2026
e05e25c
Merge branch 'main' into features/86426-di-aggregated-exceptions
rosebyte Feb 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Microsoft.Extensions.Internal;

Expand Down Expand Up @@ -120,10 +121,15 @@ public object GetRequiredKeyedService(Type serviceType, object? serviceKey)
public void Dispose()
{
List<object>? toDispose = BeginDispose();
if (toDispose is null)
{
return;
}

if (toDispose != null)
object? exceptionsCache = null;
for (var i = toDispose.Count - 1; i >= 0; i--)
{
for (int i = toDispose.Count - 1; i >= 0; i--)
try
{
if (toDispose[i] is IDisposable disposable)
{
Expand All @@ -134,68 +140,126 @@ public void Dispose()
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;
}

if (toDispose != null)
object? exceptionsCache = null;
for (var i = toDispose.Count - 1; i >= 0; i--)
{
try
{
for (int i = toDispose.Count - 1; i >= 0; i--)
object disposable = toDispose[i];
if (disposable is IAsyncDisposable asyncDisposable)
{
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
ValueTask vt = asyncDisposable.DisposeAsync();
if (!vt.IsCompletedSuccessfully)
{
((IDisposable)disposable).Dispose();
return Await(i, vt, toDispose, exceptionsCache);
}

// 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)
catch (Exception exception)
{
return new ValueTask(Task.FromException(ex));
AddExceptionToCache(ref exceptionsCache, exception);
}
}

CheckExceptionCache(exceptionsCache);

return default;

static async ValueTask Await(int i, ValueTask vt, List<object> toDispose)
static async ValueTask Await(int i, ValueTask vt, List<object> toDispose, object? exceptionsCache)
{
await vt.ConfigureAwait(false);
try
{
await vt.ConfigureAwait(false);
}
catch (Exception exception)
{
AddExceptionToCache(ref exceptionsCache, exception);
}

// 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)
try
{
await asyncDisposable.DisposeAsync().ConfigureAwait(false);
object disposable = toDispose[i];
if (disposable is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync().ConfigureAwait(false);
}
else
{
((IDisposable)disposable).Dispose();
}
}
else
catch (Exception exception)
{
((IDisposable)disposable).Dispose();
AddExceptionToCache(ref exceptionsCache, exception);
}
}

CheckExceptionCache(exceptionsCache);
}
}

private static void AddExceptionToCache(ref object? exceptionsCache, Exception exception)
{
if (exceptionsCache is null)
{
exceptionsCache = ExceptionDispatchInfo.Capture(exception);
}
else if (exceptionsCache is ExceptionDispatchInfo exceptionInfo)
{
exceptionsCache = new List<Exception> { exceptionInfo.SourceException, exception };
}
else
{
((List<Exception>)exceptionsCache).Add(exception);
}
}

private static void CheckExceptionCache(object? exceptionsCache)
{
if (exceptionsCache is null)
{
return;
}

if (exceptionsCache is ExceptionDispatchInfo exceptionInfo)
{
exceptionInfo.Throw();
}

throw new AggregateException((List<Exception>)exceptionsCache);
}

private List<object>? BeginDispose()
{
lock (Sync)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection.Specification.Fakes;
using Xunit;
using Xunit.Abstractions;

namespace Microsoft.Extensions.DependencyInjection.ServiceLookup
{
Expand Down Expand Up @@ -41,5 +40,138 @@ public void ServiceProviderEngineScope_ImplementsAllServiceProviderInterfaces()
Assert.Contains(serviceProviderInterface, engineScopeInterfaces);
}
}

[Fact]
public void Dispose_ServiceThrows_DisposesAllAndThrows()
{
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")
};

var exception = Assert.Throws<InvalidOperationException>(() => ((IDisposable)scope).Dispose());
Assert.Equal(TestDisposable.ErrorMessage, exception.Message);
Assert.All(disposables, disposable => Assert.True(disposable.IsDisposed));
}

[Fact]
public void Dispose_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 = Assert.Throws<AggregateException>(() => ((IDisposable)scope).Dispose());
Assert.Equal(2, exception.InnerExceptions.Count);
Assert.All(exception.InnerExceptions, ex => Assert.IsType<InvalidOperationException>(ex));
Assert.All(disposables, disposable => Assert.True(disposable.IsDisposed));
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task DisposeAsync_ServiceThrows_DisposesAllAndThrows(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")
};

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)
{
throw new InvalidOperationException(ErrorMessage);
}
}

public ValueTask DisposeAsync()
{
if (_synchronous)
{
Dispose();
return default;
}

return new ValueTask(DisposeAsyncInternal());

async Task DisposeAsyncInternal()
{
await Task.Yield();
Dispose();
}
}
}
}
}
Loading