Skip to content

Exclude keyed registrations from IEnumerable<T> / T[] resolution#430

Merged
jeremydmiller merged 1 commit into
masterfrom
fix/iEnumerable-excludes-keyed-registrations
May 29, 2026
Merged

Exclude keyed registrations from IEnumerable<T> / T[] resolution#430
jeremydmiller merged 1 commit into
masterfrom
fix/iEnumerable-excludes-keyed-registrations

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Summary

IServiceProvider.GetServices(T) and T[] resolution in MS DI return
only non-keyed registrations; keyed registrations are looked up
explicitly via GetKeyedService(T, key).

Lamar's ListInstance / ArrayInstance built their Elements straight
from ServiceGraph.FindAll(T), which is sourced from
ServiceFamily.All — the flat union of keyed and non-keyed instances
for a given service type. As a result a keyed registration of T
ended up in the materialised IEnumerable<T> / T[], deviating from
MS DI semantics.

The same divergence is the load-bearing cause of a hard
stack overflow with JasperFx 2.2.x's
AddJasperFxEnumerableSingletonSupport():

EnumerableSingletons.KeyedMirror registers a keyed Singleton lambda
whose factory calls sp.GetServices(elementType) to return the shared
non-keyed singleton instance. When Lamar inlines the mirror's Singleton
via InjectedServiceField.ToVariableExpression's QuickResolve, the
factory invokes sp.GetServices(T), Lamar resolves the
IEnumerable<T> family (still including the mirror itself), the
generated build frame inlines the mirror again, and so on — ~750-frame
stack overflow before any handler runs.

Filtering IsKeyedService out of createPlan in both ListInstance
and ArrayInstance matches MS DI's behaviour and breaks the cycle.
Explicit keyed lookups (GetKeyedService(T, key)) are unaffected.

Test plan

  • Lamar.Testing — 709/709 pass locally (net8 + net9 + net10).
  • Lamar.AspNetCoreTests — 23/23 pass (net8/9/10).
  • OpenApiKeyedServiceIntegrationTests — 4/4 pass.
  • MinimalApiTests — 2/2 pass.
  • Real-world repro: a host on JasperFx 2.2.1 / Marten 9.3.1 /
    Wolverine 6.2.1 that previously stack-overflowed in
    ListInstance.CreateBuildFrame
    EnumerableSingletons.KeyedMirror.b__0 now starts cleanly and
    runs an end-to-end handler invocation.
  • CI green.

🤖 Generated with Claude Code

MS DI's `IServiceProvider.GetServices(T)` and `T[]` resolution return
only non-keyed registrations; keyed registrations are looked up
explicitly via `GetKeyedService(T, key)`. Lamar's `ListInstance` /
`ArrayInstance` populated their `Elements` straight from
`ServiceGraph.FindAll`, which is built from `ServiceFamily.All` — the
flat union of keyed and non-keyed instances for a given service type.
As a result a keyed registration of `T` ended up as an `IEnumerable<T>`
element, deviating from MS DI semantics and, more importantly, causing
infinite recursion for code-generating consumers:

JasperFx's `EnumerableSingletons.KeyedMirror` registers a keyed
Singleton lambda whose factory calls `sp.GetServices(elementType)` to
return the shared non-keyed singleton instance. When Lamar inlines the
mirror's Singleton via `InjectedServiceField.ToVariableExpression`'s
`QuickResolve`, the factory invokes `sp.GetServices(T)`, Lamar resolves
the `IEnumerable<T>` family (still including the mirror itself), the
generated build frame inlines the mirror again, and so on — ~750-frame
stack overflow before any handler runs.

Filter `IsKeyedService` out of `createPlan` in both `ListInstance` and
`ArrayInstance`, matching MS DI's behaviour and breaking the cycle.
Explicit keyed lookups (`GetKeyedService(T, key)`) are unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jeremydmiller jeremydmiller merged commit 73aa9f6 into master May 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant