Skip to content

Conversation

@rosebyte
Copy link
Member

Fixes: #86426

This PR introduces a behavior‑breaking change: when a DI scope is disposed, the first exception thrown currently propagates immediately, leaving all subsequent disposables in the scope undisposed. With this PR:

All disposables in the scope will be disposed, and if any exceptions are thrown, they will be propagated only after disposal completes.
If multiple exceptions are thrown, an aggregated exception will be propagated.

This breaking change may be acceptable since throwing from Dispose methods goes against .NET coding guidelines. Additionally, there are no clear scenarios where users rely on specific exception types thrown during DI scope disposal.

The try/catch blocks were placed outside the foreach loop to avoid repetitive Exception Handling Table writes, which may have performance implications when a scope contains many disposables. While disposing a DI scope typically isn't a hot path, it can still impose unnecessary CPU overhead in high‑throughput scenarios. For example, ASP.NET disposes a DI scope after each request, which can result in many EHT writes on heavily loaded servers. That said, if this feels over‑engineered, we can rewrite it for improved readability.

For further optimization, we could consider allocating the list of exceptions only when multiple exceptions occur. This adjustment would be straightforward to implement.

Copilot AI review requested due to automatic review settings January 19, 2026 11:22
@github-actions github-actions bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Jan 19, 2026
@rosebyte rosebyte added area-Extensions-DependencyInjection and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Jan 19, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR changes the disposal behavior of ServiceProviderEngineScope to aggregate exceptions during disposal rather than throwing on the first exception. Previously, when a disposable service threw an exception during disposal, the first exception would propagate immediately, leaving subsequent disposables undisposed. With this change, all disposables are disposed, and exceptions are aggregated into an AggregateException if multiple exceptions occur, or thrown as a single exception if only one occurs.

Changes:

  • Modified Dispose() and DisposeAsync() methods to use exception aggregation with try-catch blocks
  • Added comprehensive test coverage for both single and multiple exception scenarios in synchronous and asynchronous disposal
  • Implemented a while/for loop structure to continue disposal after catching exceptions

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.

File Description
ServiceProviderEngineScope.cs Modified Dispose() and DisposeAsync() to aggregate exceptions and continue disposal after exceptions occur
ServiceProviderEngineScopeTests.cs Added 4 new test methods and a TestDisposable helper class to verify exception aggregation behavior
Comments suppressed due to low confidence (2)

src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs:234

  • The Await helper method does not aggregate exceptions. If any disposal throws an exception, it will propagate immediately without disposing remaining items. This contradicts the PR's goal to ensure all disposables are disposed before propagating exceptions. The method needs to be refactored with try-catch blocks to collect exceptions and continue disposal, similar to the synchronous path in the DisposeAsync method.
            {
                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();
                    }
                }
            }
        }

src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs:8

  • The using directive for System.Linq is unused and should be removed.
using System.Threading.Tasks;

rosebyte and others added 21 commits January 19, 2026 13:57
Fixes dotnet#116375.

---------

Co-authored-by: Jan Vorlicek <janvorli@microsoft.com>
…et#123240)

Fixes dotnet#122254

bug repro:
```c
switch (X)
{
    case 2:
        // ...
        break;
    case 3:
        // ...
        break;
    case 4:
        // ...
        break;
    default:
        // "X >= 5" assertion was created here (which is incorrect, X can be 0 or 1 as well)  
        break;
}
```
> [!NOTE]
> This is a codeflow update. It may contain both source code changes
from
> [the VMR](https://github.com/dotnet/dotnet)
> as well as dependency updates. Learn more
[here](https://github.com/dotnet/dotnet/tree/main/docs/Codeflow-PRs.md).

This pull request brings the following source code changes

[marker]: <> (Begin:f7901f87-9f24-40d6-9bc1-564863937237)

## From https://github.com/dotnet/dotnet
- **Subscription**:
[f7901f87-9f24-40d6-9bc1-564863937237](https://maestro.dot.net/subscriptions?search=f7901f87-9f24-40d6-9bc1-564863937237)
- **Build**:
[20260115.1](https://dev.azure.com/dnceng/internal/_build/results?buildId=2879841)
([297491](https://maestro.dot.net/channel/8298/github:dotnet:dotnet/build/297491))
- **Date Produced**: January 15, 2026 12:49:22 PM UTC
- **Commit**:
[bac69806be267e5c3fbd02ea60d69f6bdc0356ae](dotnet/dotnet@bac6980)
- **Commit Diff**:
[4d383e9...bac6980](dotnet/dotnet@4d383e9...bac6980)
- **Branch**: [main](https://github.com/dotnet/dotnet/tree/main)

**Updated Dependencies**
- From [5.4.0-2.26064.120 to 5.4.0-2.26065.101][1]
  - Microsoft.CodeAnalysis
  - Microsoft.CodeAnalysis.Analyzers
  - Microsoft.CodeAnalysis.CSharp
  - Microsoft.Net.Compilers.Toolset
- From [11.0.100-alpha.1.26064.120 to 11.0.100-alpha.1.26065.101][1]
  - Microsoft.CodeAnalysis.NetAnalyzers
  - Microsoft.DotNet.ApiCompat.Task
- Microsoft.NET.Workload.Emscripten.Current.Manifest-11.0.100.Transport
- From [11.0.0-beta.26064.120 to 11.0.0-beta.26065.101][1]
  - Microsoft.DotNet.Arcade.Sdk
  - Microsoft.DotNet.Build.Tasks.Archives
  - Microsoft.DotNet.Build.Tasks.Feed
  - Microsoft.DotNet.Build.Tasks.Installers
  - Microsoft.DotNet.Build.Tasks.Packaging
  - Microsoft.DotNet.Build.Tasks.TargetFramework
  - Microsoft.DotNet.Build.Tasks.Templating
  - Microsoft.DotNet.Build.Tasks.Workloads
  - Microsoft.DotNet.CodeAnalysis
  - Microsoft.DotNet.GenAPI
  - Microsoft.DotNet.GenFacades
  - Microsoft.DotNet.Helix.Sdk
  - Microsoft.DotNet.PackageTesting
  - Microsoft.DotNet.RemoteExecutor
  - Microsoft.DotNet.SharedFramework.Sdk
  - Microsoft.DotNet.XliffTasks
  - Microsoft.DotNet.XUnitExtensions
- From [0.11.5-alpha.26064.120 to 0.11.5-alpha.26065.101][1]
  - Microsoft.DotNet.Cecil
- From [2.9.3-beta.26064.120 to 2.9.3-beta.26065.101][1]
  - Microsoft.DotNet.XUnitAssert
  - Microsoft.DotNet.XUnitConsoleRunner
- From [11.0.0-alpha.1.26064.120 to 11.0.0-alpha.1.26065.101][1]
  - Microsoft.NET.Sdk.IL
  - Microsoft.NETCore.App.Ref
  - Microsoft.NETCore.ILAsm
  - runtime.native.System.IO.Ports
  - System.Reflection.Metadata
  - System.Reflection.MetadataLoadContext
  - System.Text.Json
- From [7.3.0-preview.1.6520 to 7.3.0-preview.1.6601][1]
  - NuGet.Frameworks
  - NuGet.Packaging
  - NuGet.ProjectModel
  - NuGet.Versioning
- From [3.0.0-alpha.1.26064.120 to 3.0.0-alpha.1.26065.101][1]
  - System.CommandLine

[marker]: <> (End:f7901f87-9f24-40d6-9bc1-564863937237)

[1]: dotnet/dotnet@4d383e9...bac6980
[marker]: <> (Start:Footer:CodeFlow PR)

## Associated changes in source repos
-
dotnet/razor@53776e4...3eb8321
-
dotnet/roslyn@52e8114...e6f840b
-
dotnet/runtime@73294eb...d120108
-
dotnet/winforms@01eefd4...448e82a

<details>
<summary>Diff the source with this PR branch</summary>

```bash
darc vmr diff --name-only https://github.com/dotnet/dotnet:bac69806be267e5c3fbd02ea60d69f6bdc0356ae..https://github.com/dotnet/runtime:darc-main-f266d9fc-0def-4f1b-bd7a-692fc2b1696b
```
</details>

[marker]: <> (End:Footer:CodeFlow PR)

---------

Co-authored-by: dotnet-maestro[bot] <dotnet-maestro[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
…tnet#123242)

## Description

The `superpmi_aspnet2.py` script ran each benchmark scenario only once.
Modified to run each scenario 6 times with different JIT configuration
environment variables to collect comprehensive SPMI data across
compilation modes.

## Changes

- **Modified `run_crank_scenario` signature**: Added `env_vars: dict`
parameter to accept environment variable configurations
- **Added environment variable processing**: Converts dictionary to
`--application.environmentVariables DOTNET_<variable>=<value>` crank
arguments
- **Defined 6 environment variable sets**:
  1. `Dummy=0`
  2. `TieredCompilation=0`
  3. `TieredPGO=0`
  4. `TieredPGO=1, ReadyToRun=0`
5. `ReadyToRun=0, OSR_HitLimit=0,
TC_OnStackReplacement_InitialCounter=10`
  6. `TC_PartialCompilation=1`
- **Updated main loop**: Nested iteration runs each of 4 scenarios with
all 6 environment sets (24 total runs)

## Impact

Before: 4 scenarios × 1 run = 4 total runs  
After: 4 scenarios × 6 env sets = 24 total runs

Each run now displays the active configuration:
```
### Running about-sqlite benchmark (set 1/6: Dummy=0)... ###
### Running about-sqlite benchmark (set 2/6: TieredCompilation=0)... ###
...
```

> [!WARNING]
>
> <details>
> <summary>Firewall rules blocked me from connecting to one or more
addresses (expand for details)</summary>
>
> #### I tried to connect to the following addresses, but was blocked by
firewall rules:
>
> - `aka.ms`
> - Triggering command: `/usr/bin/curl curl -I -sSL --retry 5
--retry-delay 2 --connect-timeout 15 REDACTED` (dns block)
>
> If you need me to access, download, or install something from one of
these locations, you can either:
>
> - Configure [Actions setup
steps](https://gh.io/copilot/actions-setup-steps) to set up my
environment, which run before the firewall is enabled
> - Add the appropriate URLs or hosts to the custom allowlist in this
repository's [Copilot coding agent
settings](https://github.com/dotnet/runtime/settings/copilot/coding_agent)
(admins only)
>
> </details>

<!-- START COPILOT CODING AGENT SUFFIX -->



<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> # Add more scenarios to aspnet2 SPMI collection
> 
> superpmi_aspnet2.py script currently runs all scenarios just once
(e.g. "about-sqlite", "json", etc). I want you to runeach scenario
multiple times with different sets of environment variables here are the
sets:
> 
> ```
> Dummy=0
> TieredCompilation=0
> TieredPGO=0
> TieredPGO=1, ReadyToRun=0
> ReadyToRun=0, OSR_HitLimit=0, TC_OnStackReplacement_InitialCounter=10
> TC_PartialCompilation=1
> ```
> 6 sets in total (some of them have multiple variables).
> 
> each variable is basically extra `--application.environmentVariables
DOTNET_<variable>=<value>` argument passed to crank.
> 
> You don't have to build the whole runtime or run the script (it
requires a complex setup). Just modify the script to add the above
functionality. Make sure the code is correct and compiles without
errors.


</details>



<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: EgorBo <523221+EgorBo@users.noreply.github.com>
Co-authored-by: Egor Bogatov <egorbo@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This fixes the
System.Runtime.Tests.ControlledExecutionTests.CancelItselfFromFinally
libraries test that was failing with the interpreter. The issue was that
finally block was being aborted due to the fact that instead of the
complex COMPlusCheckForAbort, the interpreter was only checking if abort
was requested.
Fixes dotnet#122472

From some inspection, it looks like `PerfMap::Initialize` does not
depend on the methods right above it.
```
#ifdef FEATURE_PERFTRACING
        DiagnosticServerAdapter::Initialize();
        DiagnosticServerAdapter::PauseForDiagnosticsMonitor();
#endif // FEATURE_PERFTRACING

#ifdef FEATURE_GDBJIT
        // Initialize gdbjit
        NotifyGdb::Initialize();
#endif // FEATURE_GDBJIT

#ifdef FEATURE_EVENT_TRACE
        // Initialize event tracing early so we can trace CLR startup time events.
        InitializeEventTracing();

        // Fire the EE startup ETW event
        ETWFireEvent(EEStartupStart_V1);
#endif // FEATURE_EVENT_TRACE

        InitGSCookie();

#ifdef LOGGING
        InitializeLogging()
#endif
```
`PerfMap::Initialize` depends on `SString::Startup()` which occurs
earlier in startup and some PAL/OS services.

Instead of adding more logic to handle a racing DiagnosticServer IPC
[`EnablePerfMap`
command](https://github.com/dotnet/diagnostics/blob/main/documentation/design-docs/ipc-protocol.md#enableperfmap)
and `PerfMap::Initialize` (e.g. lazy-init/queued commands), bump the
PerfMap initialization ahead of the DiagnosticServer initialization. The
DiagnosticServer also doesn't depend on PerfMap being initialized
afterwards, and PerfMap's `CrstStatic` and `PerfMap::Enable(type,
sendExisting)` suggests that the IPC enable perfmap command should occur
after `PerfMap::Initialize`.

This way, DiagnosticServer is still early in startup, and its
`PerfMap::Enable` will not crash the runtime.
As for the IPC command to set environment variables, it is unlikely that
it was used to `DOTNET_PerfMapEnabled` because that would either not
have applied early enough or have crashed the runtime like in the issue.
Instead, the `EnablePerfMap`/`DisablePerfMap` commands should be used to
toggle PerfMap status.
…forms (dotnet#123274)

## Description

NativeAOT runtime packs for iOS and other Apple mobile platforms were
missing required static libraries (libSystem.Globalization.Native.a,
libSystem.IO.Compression.Native.a, libSystem.Native.a, libbrotli*.a)
starting in version 11.0.0-alpha.1.26065.101.

## Changes

Modified `eng/liveBuilds.targets` in the
`ResolveLibrariesRuntimeFilesFromLocalBuild` target:

- Added `BuildNativeAOTRuntimePack != 'true'` condition to the
`ExcludeNativeLibrariesRuntimeFiles` item for Apple mobile platforms
- Updated comment to clarify that the exclusion does not apply to
NativeAOT

**Rationale**: Regular CoreCLR packs retrieve these libraries from the
CoreCLR shared framework directory, so they're excluded from
LibrariesRuntimeFiles to avoid duplicates. NativeAOT packs do not reach
back into CoreCLR shared framework artifacts and require these libraries
from LibrariesRuntimeFiles.

```xml
<!-- Before -->
<ExcludeNativeLibrariesRuntimeFiles Condition="'$(TargetsAppleMobile)' == 'true' and '$(RuntimeFlavor)' == 'CoreCLR'"

<!-- After -->
<ExcludeNativeLibrariesRuntimeFiles Condition="'$(TargetsAppleMobile)' == 'true' and '$(RuntimeFlavor)' == 'CoreCLR' and '$(BuildNativeAOTRuntimePack)' != 'true'"
```

## Testing

The fix will be validated when NativeAOT runtime packs for Apple mobile
platforms are built in CI/CD, verifying that the static libraries are
included in the package output.

<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>Latest .NET 11 NativeAOT packs missing some static
libraries</issue_title>
> <issue_description>The `11.0.0-alpha.1.26065.101` version of the
`microsoft.netcore.app.runtime.nativeaot.ios-arm64` pack (and probably
other packs for other RIDs as well, I haven't checked) are missing the
following files:
> 
>  * libSystem.Globalization.Native.a
>  * libSystem.IO.Compression.Native.a
>  * libSystem.Native.a
>  * libbrotlicommon.a
>  * libbrotlidec.a
>  * libbrotlienc.a
> 
> They were present in `11.0.0-alpha.1.26062.101`
> 
> Complete file list:
https://gist.github.com/rolfbjarne/7f42f3cf3ed3a815e2e9fe2b28c580fb#file-gistfile1-txt-L76-L82
> 
> This started happening in this maestro PR:
dotnet/macios#24508
> 
> specifically this commit:
dotnet/macios@ee15359
> 
> I tried looking through the relevant dotnet/runtime commits, but I
didn't see anything obvious causing this.</issue_description>
> 
> <agent_instructions>In the ResolveLibrariesRefAssembliesFromLocalBuild
target in eng/livebuilds.targets, add BuildNativeAOTRuntimePack !=
'true' to the condition in the ExcludeNativeLibrariesRuntimeFiles. It
should only apply to the item where TargetsAppleMobile == 'true'.
> 
> Additionally, update the comment above the item to indicate that it
does not apply to nativeaot because we do not reach back into coreclr
sharedFramework artifacts.</agent_instructions>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> <comment_new><author>@MichalStrehovsky</author><body>
> > and probably other packs for other RIDs as well, I haven't checked
> 
> I checked
Microsoft.NETCore.App.Runtime.NativeAOT.linux-arm64.11.0.0-preview.1.26065.113
and that one has these.
Microsoft.NETCore.App.Runtime.NativeAOT.ios-arm64.11.0.0-preview.1.26065.113
doesn't have these.
> 
> So looks to be iDevice specific. I can't build these things locally as
I don't have a mac.</body></comment_new>
> <comment_new><author>@steveisok</author><body>
> In the local build, it looks like coreclr and nativeaot are supposed
to look back to
`artifacts/bin/coreclr/ios.<arch>.<config>/sharedFramework` for the
native lib artifacts. NativeAOT does not appear to be doing that, hence
the reason the static libs aren't included. </body></comment_new>
> <comment_new><author>@steveisok</author><body>
> `ResolveLibrariesRuntimeFilesFromLocalBuild` typically copies all the
libraries and native lib bits. There's a step to exclude some native
bits if the runtime is coreclr or nativeaot because they are supposed to
be accounted for in `ResolveRuntimeFilesFromLocalBuild`. That's not
happening for nativeaot due to `BuildNativeAOTRuntimePack=true`.
</body></comment_new>
> <comment_new><author>@steveisok</author><body>
> @kotlarmilos if you want, I can fix it. It's no problem, so let me
know.</body></comment_new>
> </comments>
> 


</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes dotnet#123253

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 We'd love your input! Share your thoughts on Copilot coding agent in
our [2 minute survey](https://gh.io/copilot-coding-agent-survey).

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: steveisok <471438+steveisok@users.noreply.github.com>
…t slowpatheltleave.sh catched on 2K3000. (dotnet#123250)

like [PR#122714](dotnet#122714), use
`pData->buffer` to return `struct{single, single}` in
`ProfileArgIterator::GetReturnBufferAddr()`.
… platforms (dotnet#122789)

We had a couple of places that were using `__builtin_available` to guard
against some platform version features. Now that our Apple platform
floor is macOS 12 and iOS/tvOS 13, we can remove some of them.

Co-authored-by: Jeremy Barton <jbarton@microsoft.com>
Adding 3 constants that represent C#, F#, and Visual Basic to the
StringSyntaxAttribute

Fix dotnet#122604
…23270)

I've decided to do a bespoke function for WASM, mainly due to the
awkwardness of porting the register mask code that would be of
questionable value. Instead, we place an allocation constraint on the
RA.

With these changes, we are very close to getting a functional "add(x,
y)". The only remaining part is the SP arg, being worked on in
dotnet#123262.

This is not a fully sound implementation since we have a limit on the
number of instructions in the prolog (and here we're generating a
potentially unbounded number of instruction). I've put a point about
this into dotnet#121865. It seems this single-IG restriction ought to be
fixable...
…122758)

The `nonExpansive` parameter in `Dictionary::PopulateEntry` was always
passed as `FALSE` from its only call site in `jithelpers.cpp`. This
change removes the dead parameter and simplifies the implementation by
eliminating unreachable code paths.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com>
This PR adds a fast path in `IEnumerableSkipTakeIterator.TryGetLast`.
When source is an Iterator and `GetCount(onlyIfCheap: true)` returns a
valid count (not -1), check if count <= _minIndexInclusive. In this
case, immediately return with found=false.

---------

Co-authored-by: Stephen Toub <stoub@microsoft.com>
…t#123333)

Sometimes code is placed at a higher native offset than the code at IL
offsets adjacent to it. Our debugger algorithm deals with this by,
during traversal of mappings, [setting the IL offset
back](https://github.com/dotnet/runtime/blob/6afecd4adc64210e5b46767d02a6495af1a08c46/src/coreclr/vm/debugdebugger.cpp#L1321)
if one is encountered that is lower than the current IL offset in the
traversal. Native AOT does not have this mechanism, which leads to such
code being misattributed to higher-than-actual source lines.

This change reproduces this mechanism in NativeAOT by inserting a native
sequence point whenever the IL offset drops below the previous offset.

Repro issue (line attribution of DoWork):

```csharp
using System.Runtime.CompilerServices;

static class Program
{
#line 53
	[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
	public async static Task Main()
	{
		await DoWork(0);
	}
	[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
	public async static Task BackgroundWorker1()
	{
		// await Task.Delay(10000);
		// await Task.Yield();
		throw new InvalidOperationException("BackgroundWorker1 exception.");
	}
	[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
	public async static Task BackgroundWorker2()
	{
		await Task.Delay(10);
		Console.WriteLine("BackgroundWorker2 completed.");
	}
	[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
	public async static Task BackgroundWorker3()
	{
		await Task.Delay(10);
		Console.WriteLine("BackgroundWorker3 completed.");
	}
	
	[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
	static async Task DoWork(int i)
	{
		Task worker1 = BackgroundWorker1();
		Task worker2 = BackgroundWorker2();
		Task worker3 = BackgroundWorker3();
		await Task.WhenAll(worker1, worker2, worker3);
		Console.WriteLine("All background workers completed.");
	}
}
```
Config: Windows x64 Debug, .NET 11.0.100-alpha.1.25618.104,
runtime-async on
<table>
  <tr>
    <th>NativeOffset</th>
    <td>0</td>
    <td>106</td>
    <td>107</td>
    <td>107</td>
    <td>116</td>
    <td>124</td>
    <td>124</td>
    <td>133</td>
    <td>141</td>
    <td>141</td>
    <td>150</td>
    <td>158</td>
    <td>169</td>
    <td>183</td>
    <td>192</td>
    <td>205</td>
    <td>222</td>
    <td>231</td>
    <td>244</td>
    <td>261</td>
    <td>270</td>
    <td>283</td>
    <td>307</td>
    <td>312</td>
    <td>357</td>
    <td>379</td>
    <td>380</td>
    <td>387</td>
    <td>392</td>
    <td>393</td>
    <td>420</td>
    <td>431</td>
    <td>867</td>
    <td>878</td>
    <td>1067</td>
    <td>1096</td>
  </tr>
  <tr>
    <th>ILOffset</th>
    <td>0</td>
    <td>0</td>
    <td>1</td>
    <td>1</td>
    <td>6</td>
    <td>7</td>
    <td>7</td>
    <td>12</td>
    <td>13</td>
    <td>13</td>
    <td>18</td>
    <td>19</td>
    <td>27</td>
    <td>30</td>
    <td>35</td>
    <td>37</td>
    <td>40</td>
    <td>45</td>
    <td>47</td>
    <td>50</td>
    <td>55</td>
    <td>57</td>
    <td>60</td>
    <td>65</td>
    <td>70</td>
    <td>75</td>
    <td>76</td>
    <td>81</td>
    <td>86</td>
    <td>87</td>
    <td>87</td>
    <td>70</td>
    <td>87</td>
    <td>70</td>
    <td>0</td>
    <td>87</td>
  </tr>
</table>

<table>
  <tr>
    <th>ILOffset</th>
    <td>0</td>
    <td>1</td>
    <td>7</td>
    <td>13</td>
    <td>19</td>
    <td>76</td>
  </tr>
  <tr>
    <th>LineNumber</th>
    <td>80</td>
    <td>81</td>
    <td>82</td>
    <td>83</td>
    <td>84</td>
    <td>85</td>
  </tr>
</table>

Before:
<table>
  <tr>
    <th>NativeOffset</th>
    <td>0</td>
    <td>106</td>
    <td>107</td>
    <td>124</td>
    <td>141</td>
    <td>158</td>
    <td>380</td>
    <td>1067</td>
  </tr>
  <tr>
    <th>LineNumber</th>
    <td>80</td>
    <td>80</td>
    <td>81</td>
    <td>82</td>
    <td>83</td>
    <td>84</td>
    <td>85</td>
    <td>80</td>
  </tr>
</table>
Notice in particular that code after native offset 431, the suspension
code for DoWork, is mistakenly attributed to line 85
(Console.WriteLine).

After:
<table>
  <tr>
    <th>NativeOffset</th>
    <td>0</td>
    <td>106</td>
    <td>107</td>
    <td>124</td>
    <td>141</td>
    <td>158</td>
    <td>380</td>
    <td>431</td>
    <td>878</td>
    <td>1067</td>
  </tr>
  <tr>
    <th>LineNumber</th>
    <td>80</td>
    <td>80</td>
    <td>81</td>
    <td>82</td>
    <td>83</td>
    <td>84</td>
    <td>85</td>
    <td>84</td>
    <td>84</td>
    <td>80</td>
  </tr>
</table>
Here we see that 380-431 is attributed to Console.WriteLine, while the
suspension code that follows is correctly attributed to the previous
line of await.

[aot.txt](https://github.com/user-attachments/files/24697543/aot.txt)
Read binary psinfo for System.Diagnostic.Process on SunOS (Solaris or
illumos).
No failures in System.Diagnostic.Process.Tests (but lots of skip)

---------

Co-authored-by: Austin Wise <AustinWise@gmail.com>
Co-authored-by: Adeel Mujahid <3840695+am11@users.noreply.github.com>
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
}
else
{
throw new InvalidOperationException(SR.Format(SR.AsyncDisposableServiceDispose, TypeNameHelper.GetTypeDisplayName(toDispose[index])));
Copy link
Member

@CarnaViire CarnaViire Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 regarding IAsyncDisposables in DI:

Specifically, implementations of IDisposable and 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() 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:

foreach (KeyValuePair<TKey, Lazy<RateLimiter>> limiter in _limiters)
{
try
{
limiter.Value.Value.Dispose();
}
catch (Exception ex)
{
exceptions ??= new List<Exception>();
exceptions.Add(ex);
}
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That`s a good point, how about making an issue specifically about this so we collect more information and fix it in another PR?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I`ve created an issue to further discuss the behaviour, #123620.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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