Skip to content

Keep the service key when resolving keyed services via service location#364

Merged
jeremydmiller merged 1 commit into
mainfrom
fix-2878-keyed-service-location
May 23, 2026
Merged

Keep the service key when resolving keyed services via service location#364
jeremydmiller merged 1 commit into
mainfrom
fix-2878-keyed-service-location

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Problem

Fixes the root cause of JasperFx/wolverine#2878: keyed services and opaque (lambda-factory) registrations each work in isolation, but not together in the same handler.

When anything in a generated method forces the scoped-IServiceProvider "service location" path — a dependency that injects IServiceProvider directly, or an opaque lambda registration like the ones the MS Graph SDK adds (AddScoped<IFoo>(_ => …)) — every sibling dependency in that method is resolved through GetServiceFromScopedContainerFrame. That frame always emitted:

GetRequiredService<T>(provider)

and dropped the key, so a keyed service dragged onto that path either threw (No service for type T has been registered) or resolved the wrong registration. The string GetRequiredKeyedService appeared nowhere in the generated output. The inline [FromKeyedServices] constructor path was fine — only the service-location frame discarded the key.

Diagnosis credit to @BlackChepo in the issue thread.

Fix

Thread the service key through GetServiceFromScopedContainerFrame and emit the keyed call when the descriptor is keyed:

GetRequiredKeyedService<T>(provider, key)   // key rendered via CodeFormatter.Write

Both call sites now pass the key:

  • ServiceLocationPlan.CreateVariable (keyed opaque registrations)
  • ServiceCollectionServerVariableSource.useServiceProvider (keyed services dragged onto location by a sibling opaque dependency)

Non-keyed resolution is byte-for-byte unchanged (the new constructor parameter defaults to null).

Tests

CodegenTests/Services/keyed_service_location.cs reproduces the bug the way Wolverine generates a handler — a MethodCall to a handler whose parameters are a [FromKeyedServices("blue")] IWidget plus an opaque IScopedLambda sibling that forces service location:

  • generated code uses GetRequiredKeyedService<IWidget>(provider, "blue") for both the keyed-concrete and keyed-opaque registrations;
  • end-to-end: the generated harness compiles and resolves the keyed service at runtime (threw No service for type IWidget before the fix).

All 3 fail before the fix and pass after. Full suite green locally: CodegenTests 346/346, CoreTests 439/439 (net9.0).

Note: consuming repos (Wolverine, Marten, …) pick this up with the next JasperFx release; #2878 itself is in Wolverine.

🤖 Generated with Claude Code

…on (GH-2878)

When a generated method is forced onto the scoped-IServiceProvider "service
location" path — because a dependency injects IServiceProvider directly or has
an opaque lambda registration (e.g. the ones MS Graph adds) — every sibling
dependency in that method is resolved through GetServiceFromScopedContainerFrame.
That frame always emitted GetRequiredService<T>(provider) and dropped the key,
so keyed services either threw ("No service for type T") or resolved the wrong
registration. Keyed services and opaque registrations each worked in isolation,
but not together in the same handler.

Thread the service key through GetServiceFromScopedContainerFrame and emit
GetRequiredKeyedService<T>(provider, key) (key rendered via CodeFormatter) when
the descriptor is keyed. Both call sites — ServiceLocationPlan.CreateVariable
and ServiceCollectionServerVariableSource.useServiceProvider — now pass the key.
Non-keyed resolution is unchanged.

Adds CodegenTests reproducing the bug end-to-end (generated code + compiled,
runtime resolution) the way a Wolverine handler is generated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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