Skip to content

Honor [FromKeyedServices] on constructor parameters in generated code - #445

Merged
jeremydmiller merged 1 commit into
mainfrom
fix-3081-keyed-ctor-injection
Jun 12, 2026
Merged

jeremydmiller merged 1 commit into
mainfrom
fix-3081-keyed-ctor-injection

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Fixes the root cause of JasperFx/wolverine#3081: Wolverine handler code generation ignored [FromKeyedServices] on a handler's constructor parameters.

Root cause

ConstructorFrame.FindVariables resolved each constructor parameter by type only:

Parameters[i] = chain.FindVariable(parameter.ParameterType);

That discards the ParameterInfo and the attributes on it, so the [FromKeyedServices("…")] key was dropped. The parameter fell through to the default registration — meaning two keyed parameters of the same service type both resolved to whichever registration was last (the family default).

This only affected constructor injection. Method parameters already worked, because MethodCall.FindVariables resolves via the attribute-aware chain.FindVariable(ParameterInfo) overload that MethodFrameArranger uses to detect [FromKeyedServices] and resolve the keyed service. That asymmetry is exactly why keyed services on a Handle(...) method parameter worked in Wolverine but the same attribute on the handler's constructor silently resolved to the wrong instance.

Fix

One line — ConstructorFrame now uses the same ParameterInfo overload as MethodCall:

- Parameters[i] = chain.FindVariable(parameter.ParameterType);
+ Parameters[i] = chain.FindVariable(parameter);

Non-keyed parameters are unaffected: MethodFrameArranger.FindVariable(ParameterInfo) falls through to the type-based resolution when there is no attribute (and name-matches a same-typed variable, consistent with how method parameters already resolve).

Test

keyed_constructor_injection (in CodegenTests/Services) mirrors the existing keyed_service_location harness but exercises the constructor path via ServiceContainer.TryCreateConstructorFrames — the exact path Wolverine uses to build a handler instance. A class with two [FromKeyedServices] constructor parameters of the same service type:

  • each_keyed_constructor_parameter_resolves_to_its_own_registration — the generated code references both implementations (before the fix, only the last/default appeared);
  • keyed_constructor_parameters_resolve_correctly_at_runtime — compiles and runs the generated harness and asserts each parameter lands the correct keyed instance in the correct slot.

Both fail without the fix. Full CodegenTests Services/codegen/frames suite (406 tests) is green; the existing keyed_service_location tests still pass.

Follow-up

Once this is published, a follow-up PR to Wolverine will bump the JasperFx dependency and add an end-to-end reproduction test for #3081 (keyed constructor injection through a Wolverine handler, including under Lamar — whose keyed resolution is already correct).

🤖 Generated with Claude Code

ConstructorFrame.FindVariables resolved each constructor parameter via
chain.FindVariable(parameter.ParameterType) — the type-only overload — which
discards the ParameterInfo and any attributes on it. As a result the
[FromKeyedServices] attribute on a constructor parameter was ignored during code
generation: the parameter fell through to the default registration, so two keyed
parameters of the same service type both resolved to whichever registration was
last (the family default).

This only affected constructor injection. Method parameters already worked
because MethodCall.FindVariables resolves via the attribute-aware
chain.FindVariable(ParameterInfo) overload, which MethodFrameArranger uses to
detect [FromKeyedServices] and resolve the keyed service. ConstructorFrame now
uses the same overload.

Adds keyed_constructor_injection covering a constructor with two
[FromKeyedServices] parameters of the same service type: it asserts the generated
code resolves each key to its own implementation, and compiles + runs the result
to prove each parameter lands the correct keyed instance in the correct slot.

Reported in JasperFx/wolverine#3081.
@jeremydmiller
jeremydmiller merged commit 0fa024c into main Jun 12, 2026
1 of 2 checks passed
@jeremydmiller
jeremydmiller deleted the fix-3081-keyed-ctor-injection branch June 12, 2026 13:44
jeremydmiller added a commit to JasperFx/wolverine that referenced this pull request Jun 12, 2026
…loses #3081) (#3086)

JasperFx 2.9.10 fixes ConstructorFrame to resolve constructor parameters via the
attribute-aware FindVariable(ParameterInfo) overload, so [FromKeyedServices] on a
handler's constructor is honored (JasperFx/jasperfx#445). Before, keyed
constructor parameters were resolved by type only and fell through to the default
(last) registration — two keyed params of the same service type both bound to the
same instance.

- Bump JasperFx, JasperFx.Events, JasperFx.Events.SourceGenerator and
  JasperFx.SourceGenerator 2.8.2 -> 2.9.10 (RuntimeCompiler stays on its 5.x line).
- Add keyed_services_on_handler_constructor: a handler whose constructor takes two
  [FromKeyedServices] params of the same service type, asserting each resolves to
  its own registration. Companion to using_with_keyed_services, which covers keyed
  services on Handle METHOD parameters (that path already worked).
- Bump MessagePack 3.1.3 -> 3.1.7 to clear the NU1903 advisory that otherwise fails
  the build under TreatWarningsAsErrors.

Closes #3081

Co-authored-by: Claude Opus 4.8 (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