From 2cbedb49b5b7f4ba56adc1976f9852ac5690459e Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Thu, 28 May 2026 20:36:58 -0500 Subject: [PATCH] Exclude keyed registrations from IEnumerable / T[] resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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` 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` 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) --- src/Lamar/IoC/Enumerables/ArrayInstance.cs | 8 +++++++- src/Lamar/IoC/Enumerables/ListInstance.cs | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Lamar/IoC/Enumerables/ArrayInstance.cs b/src/Lamar/IoC/Enumerables/ArrayInstance.cs index 0f7ca3a5..ac92b01f 100644 --- a/src/Lamar/IoC/Enumerables/ArrayInstance.cs +++ b/src/Lamar/IoC/Enumerables/ArrayInstance.cs @@ -63,7 +63,13 @@ protected override IEnumerable createPlan(ServiceGraph services) } else { - Elements = services.FindAll(typeof(T)); + // MS DI's IServiceProvider.GetServices(T) (and by extension T[] resolution) returns + // only non-keyed registrations; keyed registrations are looked up explicitly via + // GetKeyedService(T, key). Match that here so a keyed-mirror registration of the + // same service type doesn't end up as a T[] element. Otherwise inlining the + // mirror's Singleton via QuickResolve invokes the mirror's factory, which calls + // sp.GetServices(T) again and recursively re-enters codegen — stack overflow. + Elements = services.FindAll(typeof(T)).Where(x => !x.IsKeyedService).ToArray(); } return Elements; diff --git a/src/Lamar/IoC/Enumerables/ListInstance.cs b/src/Lamar/IoC/Enumerables/ListInstance.cs index 494c723d..ef68c386 100644 --- a/src/Lamar/IoC/Enumerables/ListInstance.cs +++ b/src/Lamar/IoC/Enumerables/ListInstance.cs @@ -56,7 +56,13 @@ protected override IEnumerable createPlan(ServiceGraph services) } else { - Elements = services.FindAll(typeof(T)); + // MS DI's IServiceProvider.GetServices(T) returns only non-keyed registrations; + // keyed registrations are looked up explicitly via GetKeyedService(T, key). Match + // that here so a keyed-mirror registration of the same service type doesn't end up + // as an IEnumerable element. Otherwise inlining the mirror's Singleton via + // QuickResolve invokes the mirror's factory, which calls sp.GetServices(T) again + // and recursively re-enters ListInstance/ArrayInstance code-gen — stack overflow. + Elements = services.FindAll(typeof(T)).Where(x => !x.IsKeyedService).ToArray(); } return Elements;