Skip to content

Postprocessor frames + IUsesServiceProviderFrame hook on ScopedContainerCreation (#385)#386

Merged
jeremydmiller merged 1 commit into
mainfrom
feat/385-scoped-postprocessors
May 27, 2026
Merged

Postprocessor frames + IUsesServiceProviderFrame hook on ScopedContainerCreation (#385)#386
jeremydmiller merged 1 commit into
mainfrom
feat/385-scoped-postprocessors

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Implements #385: a first-class extension point on the frame that emits the scoped-container line (await using var serviceScope = ...) so Wolverine can register frames guaranteed to run immediately after the scope line and before anything else in a generated method body.

Changes (in JasperFx)

  • IScopedContainerCreation (public) — void AddPostProcessor(SyncFrame frame). ScopedContainerCreation stays internal and implements it; consumers reach the live instance via the scoped IServiceProvider variable's Creator:
    if (scopedProviderVariable.Creator is IScopedContainerCreation scoped)
        scoped.AddPostProcessor(myFrame);
  • IUsesServiceProviderFrame (public marker) — void UseServiceProvider(Variable serviceProvider). In FindVariables, the parent hands its scoped provider to each such child before the child resolves its own variables, so the child never asks the arranger for an IServiceProvider (the scope line is itself the creator of that variable → bi-directional dependency).
  • GenerateCode chains the postprocessors (CompositeFrame-style) after the scope line and before Next, in registration order. The GetServiceFromScopedContainerFrame emits 'await using' without promoting the host method to async #228 AsyncMode == AsyncTask await using gate is unchanged.
  • Creates surfaces the postprocessors' created variables (plus Scope/Scoped) so downstream Next frames can consume them.

Re-parent decision (issue's open question #4)

The plan proposed surfacing child variables without re-parenting (Creator stays the child), matching CompositeFrame, with the guard test as the decision point. That approach recurses infinitely: the arranger resolves the downstream consumer via the surfaced variable, sees its Creator is the nested postprocessor (not a top-level frame), inserts that postprocessor as a duplicate top-level frame, and chains it back into the scope frame's own postprocessor emission → StackOverflow.

Fix (the documented fallback): Creates re-parents each surfaced variable to the scope frame via a new internal Variable.OverrideCreator (sets the creator with no creates-list side effect). Downstream ordering then points at the top-level scope frame and nothing is double-inserted.

Acceptance criteria

  • Postprocessors emit after the scope line and before Next, in registration order (async + sync)
  • IUsesServiceProviderFrame postprocessors receive the scoped provider and emit against it with no second scope / no cycle
  • Variables created by postprocessors are visible to downstream Next frames and resolve through the arranger (the guard test — proves the re-parent decision)
  • A postprocessor's own dependencies resolve via the normal FindVariables path
  • No postprocessors → byte-identical output; full suite green incl. GetServiceFromScopedContainerFrame emits 'await using' without promoting the host method to async #228 ScopedContainerCreationTests

Tests (src/CodegenTests/Services/ScopedContainerCreationPostprocessorTests.cs)

Emitted order (async/sync), provider hand-off + no-second-scope, the downstream-consumer arranger guard, a postprocessor's own dependency resolution, and the byte-identical regression. Full suite: CodegenTests 367, CoreTests 439, CommandLineTests 285, EventTests 302 (net9/net10) + AOT smoke, all green.

Out of scope

The Wolverine-side postprocessor frames + registration (tracked in the Wolverine repo), and any C#/F# emission-split (#383).

🤖 Generated with Claude Code

…nerCreation (#385)

Add a first-class extension point so consumers (Wolverine) can register frames that are
guaranteed to emit immediately after the scoped-container line and before any Next frame.

- Public IScopedContainerCreation { void AddPostProcessor(SyncFrame); }, implemented by the
  (still internal) ScopedContainerCreation. Reach the live instance by casting the scoped
  IServiceProvider variable's Creator.
- Public marker IUsesServiceProviderFrame { void UseServiceProvider(Variable); }. In
  FindVariables the parent hands its scoped provider to each such child BEFORE the child
  resolves its variables, so the child never asks the arranger for an IServiceProvider
  (which would create a bi-directional dependency with the scope line that creates it).
- GenerateCode chains the postprocessors (CompositeFrame-style) after the scope line and
  before Next, in registration order; the #228 AsyncMode gate is unchanged.

Re-parent decision (issue #4): the "no re-parent / surface child vars with the child as
Creator" approach was tried first and the guard test proved it recurses infinitely — the
arranger resolves the downstream consumer via the surfaced variable, sees its Creator is the
nested postprocessor (not a top-level frame), inserts that postprocessor as a duplicate
top-level frame, and chains it back into the scope frame's own postprocessor emission. Fix:
Creates re-parents each surfaced variable to the scope frame via a new internal
Variable.OverrideCreator (no creates-list side effect), so downstream ordering points at the
top-level frame and nothing is double-inserted.

With no postprocessors registered the output is byte-identical to today; full suite stays
green including the #228 ScopedContainerCreationTests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jeremydmiller jeremydmiller marked this pull request as ready for review May 27, 2026 21:51
@jeremydmiller jeremydmiller merged commit c4550e3 into main May 27, 2026
1 check passed
@jeremydmiller jeremydmiller deleted the feat/385-scoped-postprocessors branch May 27, 2026 21:54
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