Skip to content

fix: resolve IEnumerable<T> of an unregistered element type as an empty sequence - #137

Merged
vbreuss merged 1 commit into
mainfrom
fix/msdi-empty-enumerable
Aug 1, 2026
Merged

fix: resolve IEnumerable<T> of an unregistered element type as an empty sequence#137
vbreuss merged 1 commit into
mainfrom
fix/msdi-empty-enumerable

Conversation

@vbreuss

@vbreuss vbreuss commented Aug 1, 2026

Copy link
Copy Markdown
Member

MS.DI guarantees that IEnumerable<T> resolves for every T, empty when nothing is registered, so consumers enumerate one without a null check and frameworks use a collection as their extension point. The provider-replacement path answered null instead, because the generator emits collection cases only for element types the graph mentions.

The objection recorded against this gap was that matching MS.DI means building a collection for a type unknown at compile time. That holds for a non-empty collection; an empty one constructs no elements, only an array of the element type the caller already handed over, so it needs no compile-time expansion.

An element type the container does have a registration for is excluded, so a collection with an async-tainted member still throws the container's guidance rather than quietly reporting nothing. Only IEnumerable<T> is covered, matching MS.DI, which answers null for T[], IList<T> and IReadOnlyList<T> of an unregistered element type as well. The keyed surface is untouched: MS.DI has no equivalent guarantee there.

A value-typed element stays unreported, which is the one remaining divergence and an AOT constraint rather than a semantic one: manifesting the array needs the T[] type, which native AOT generates on demand for every reference element kind but not for a value one, where Array.CreateInstance throws NotSupportedException.

@vbreuss vbreuss self-assigned this Aug 1, 2026
@vbreuss vbreuss added the bug Something isn't working label Aug 1, 2026
@vbreuss
vbreuss force-pushed the fix/msdi-empty-enumerable branch 2 times, most recently from ee47011 to af46bcc Compare August 1, 2026 01:48
…ty sequence

MS.DI guarantees that `IEnumerable<T>` resolves for every `T`, empty when nothing is registered, so consumers enumerate one without a null check and frameworks use a collection as their extension point. The provider-replacement path answered null instead, because the generator emits collection cases only for element types the graph mentions.

The objection recorded against this gap was that matching MS.DI means building a collection for a type unknown at compile time. That holds for a non-empty collection; an empty one constructs no elements, only an array of the element type the caller already handed over, so it needs no compile-time expansion.

An element type the container does have a registration for is excluded, so a collection with an async-tainted member still throws the container's guidance rather than quietly reporting nothing. Only `IEnumerable<T>` is covered, matching MS.DI, which answers null for `T[]`, `IList<T>` and `IReadOnlyList<T>` of an unregistered element type as well. The keyed surface is untouched: MS.DI has no equivalent guarantee there.

A value-typed element stays unreported, which is the one remaining divergence and an AOT constraint rather than a semantic one: manifesting the array needs the `T[]` type, which native AOT generates on demand for every reference element kind but not for a value one, where `Array.CreateInstance` throws `NotSupportedException`.
@vbreuss
vbreuss force-pushed the fix/msdi-empty-enumerable branch from af46bcc to 9a0d9ef Compare August 1, 2026 01:49
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

🚀 Benchmark Results

Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
[Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Resolve Size Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 8 6.991 ns 0.0144 ns 0.0128 ns 1.00 - NA
Awaiten 8 6.985 ns 0.0216 ns 0.0192 ns 1.00 - NA
MsDI 8 7.387 ns 0.0111 ns 0.0098 ns 1.06 - NA
Autofac 8 110.260 ns 2.2097 ns 2.0670 ns 15.79 656 B NA
Jab 8 2.646 ns 0.0050 ns 0.0042 ns 0.38 - NA
PureDI 8 5.429 ns 0.0128 ns 0.0114 ns 0.78 - NA
DryIoc 8 8.450 ns 0.0093 ns 0.0072 ns 1.21 - NA
SimpleInjector 8 11.202 ns 0.0103 ns 0.0086 ns 1.60 - NA
baseline* 256 7.847 ns 0.0424 ns 0.0354 ns 1.00 - NA
Awaiten 256 7.815 ns 0.0040 ns 0.0034 ns 1.00 - NA
MsDI 256 7.245 ns 0.0126 ns 0.0106 ns 0.93 - NA
Autofac 256 109.133 ns 3.0507 ns 2.8536 ns 13.96 656 B NA
Jab 256 42.688 ns 0.0386 ns 0.0323 ns 5.46 - NA
PureDI 256 7.651 ns 0.0034 ns 0.0029 ns 0.98 - NA
DryIoc 256 8.436 ns 0.0046 ns 0.0036 ns 1.08 - NA
SimpleInjector 256 14.858 ns 0.1155 ns 0.1024 ns 1.90 - NA
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.61GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
[Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Realistic Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 242.8 ns 1.51 ns 1.41 ns 0.96 568 B 1.00
Awaiten 251.8 ns 1.63 ns 1.45 ns 1.00 568 B 1.00
MsDI 617.2 ns 4.13 ns 3.66 ns 2.45 1104 B 1.94
Autofac 7,072.7 ns 44.30 ns 41.44 ns 28.09 10784 B 18.99
Jab 184.9 ns 1.19 ns 1.11 ns 0.73 432 B 0.76
DryIoc 418.6 ns 1.81 ns 1.61 ns 1.66 944 B 1.66
SimpleInjector 759.9 ns 3.73 ns 3.49 ns 3.02 1096 B 1.93
PureDI 199.2 ns 2.83 ns 2.65 ns 0.79 632 B 1.11
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 3.01GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
[Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Build Size Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 8 13.700 ns 0.1232 ns 0.1153 ns 0.64 144 B 1.00
Awaiten 8 21.57 ns 0.381 ns 0.318 ns 1.00 144 B 1.00
MsDI 8 1,735.12 ns 29.513 ns 27.606 ns 80.46 5688 B 39.50
Autofac 8 31,943.61 ns 126.550 ns 112.183 ns 1,481.29 33094 B 229.82
Jab 8 12.85 ns 0.298 ns 0.279 ns 0.60 96 B 0.67
PureDI 8 18.80 ns 0.114 ns 0.096 ns 0.87 128 B 0.89
DryIoc 8 913.35 ns 6.418 ns 5.690 ns 42.35 1528 B 10.61
SimpleInjector 8 13,795.51 ns 64.512 ns 60.345 ns 639.73 24760 B 171.94
baseline* 256 91.176 ns 0.5961 ns 0.5284 ns 0.59 2128 B 1.00
Awaiten 256 154.50 ns 2.945 ns 2.459 ns 1.00 2128 B 1.00
MsDI 256 16,803.40 ns 126.511 ns 98.772 ns 108.78 61016 B 28.67
Autofac 256 772,547.28 ns 4,288.717 ns 4,011.668 ns 5,001.36 720914 B 338.78
Jab 256 144.19 ns 4.009 ns 3.130 ns 0.93 2080 B 0.98
PureDI 256 157.48 ns 4.150 ns 3.679 ns 1.02 2112 B 0.99
DryIoc 256 51,437.41 ns 329.639 ns 292.216 ns 333.00 80098 B 37.64
SimpleInjector 256 411,296.90 ns 3,472.973 ns 3,078.700 ns 2,662.68 572947 B 269.24

baseline* rows show the corresponding Awaiten benchmark from the most recent successful main branch build with results, for regression comparison.

@sonarqubecloud

sonarqubecloud Bot commented Aug 1, 2026

Copy link
Copy Markdown

@vbreuss
vbreuss enabled auto-merge (squash) August 1, 2026 01:56
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

Test Results

   18 files  ± 0     18 suites  ±0   9m 1s ⏱️ +25s
1 299 tests + 2  1 298 ✅ + 2  1 💤 ±0  0 ❌ ±0 
6 294 runs  +14  6 293 ✅ +14  1 💤 ±0  0 ❌ ±0 

Results for commit 9a0d9ef. ± Comparison against base commit ebd1ab0.

This pull request removes 2 and adds 4 tests. Note that renamed tests count towards both.
Awaiten.Extensions.DependencyInjection.Tests.FeatureDetectionTests+AcceptedDivergences ‑ AnEnumerableOfAnUnmentionedElementTypeIsUnderReported
Awaiten.Extensions.DependencyInjection.Tests.MsDiConformanceTests+AbsentServices ‑ AnEnumerableOfAnUnmentionedTypeReturnsNull_KnownGap
Awaiten.Extensions.DependencyInjection.Tests.FeatureDetectionTests+AcceptedDivergences ‑ AnEnumerableOfAnUnmentionedValueElementTypeIsUnderReported
Awaiten.Extensions.DependencyInjection.Tests.MsDiConformanceTests+AbsentServices ‑ AnArrayOfAnUnmentionedTypeReturnsNull
Awaiten.Extensions.DependencyInjection.Tests.MsDiConformanceTests+AbsentServices ‑ AnEnumerableOfAnUnmentionedTypeIsEmpty
Awaiten.Extensions.DependencyInjection.Tests.MsDiConformanceTests+AbsentServices ‑ AnEnumerableOfAnUnmentionedValueTypeReturnsNull_AotConstraint

@vbreuss
vbreuss merged commit 58129e7 into main Aug 1, 2026
15 checks passed
@vbreuss
vbreuss deleted the fix/msdi-empty-enumerable branch August 1, 2026 01:57
github-actions Bot added a commit that referenced this pull request Aug 1, 2026
…istered element type as an empty sequence (#137) by Valentin Breuß
github-actions Bot added a commit that referenced this pull request Aug 1, 2026
…istered element type as an empty sequence (#137) by Valentin Breuß
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant