Postprocessor frames + IUsesServiceProviderFrame hook on ScopedContainerCreation (#385)#386
Merged
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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).ScopedContainerCreationstaysinternaland implements it; consumers reach the live instance via the scopedIServiceProvidervariable'sCreator:IUsesServiceProviderFrame(public marker) —void UseServiceProvider(Variable serviceProvider). InFindVariables, 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 anIServiceProvider(the scope line is itself the creator of that variable → bi-directional dependency).GenerateCodechains the postprocessors (CompositeFrame-style) after the scope line and beforeNext, in registration order. The GetServiceFromScopedContainerFrame emits 'await using' without promoting the host method to async #228AsyncMode == AsyncTaskawait usinggate is unchanged.Createssurfaces the postprocessors' created variables (plusScope/Scoped) so downstreamNextframes 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 itsCreatoris 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):
Createsre-parents each surfaced variable to the scope frame via a new internalVariable.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
Next, in registration order (async + sync)IUsesServiceProviderFramepostprocessors receive the scoped provider and emit against it with no second scope / no cycleNextframes and resolve through the arranger (the guard test — proves the re-parent decision)FindVariablespathScopedContainerCreationTestsTests (
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