Skip to content

Fix HTTP codegen infinite loop for [AsParameters] + [ReadAggregate]/[WriteAggregate]#3125

Merged
jeremydmiller merged 1 commit into
mainfrom
fix-asparameters-readaggregate-codegen-loop
Jun 17, 2026
Merged

Fix HTTP codegen infinite loop for [AsParameters] + [ReadAggregate]/[WriteAggregate]#3125
jeremydmiller merged 1 commit into
mainfrom
fix-asparameters-readaggregate-codegen-loop

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Problem

A hybrid message-handler / HTTP endpoint that uses Marten with [ReadAggregate] (or [WriteAggregate]), brings the aggregate identifier through a route argument, and binds the request with [AsParameters] sends HTTP code generation into an infinite loop (StackOverflow), so the app can't boot.

Reproduction (the reported shape):

public record GetJourney([FromRoute] Guid JourneyId);

public static class GetJourneyHandler
{
    [WolverineGet("/api/example-event-sourcing/journey/{journeyId:guid}")]
    public static JourneyResponse Handle([AsParameters] GetJourney query, [ReadAggregate] Journey journey)
        => JourneyResponse.From(journey);
}

Root cause (all in code generation)

The cyclic Next chain that overflows the stack:

AsParametersBindingFrame.GenerateCode
 → TagHttpHandlerFrame → OpenMartenSessionFrame → CreateMessageContextWithMaybeTenantFrame
 → ReadHttpFrame.GenerateCode
 → AsParametersBindingFrame.GenerateCode   ← cycles forever

AsParametersBindingFrame owns the route-read ReadHttpFrame for the id and generates it inline (as a constructor argument of the bound query object). The [ReadAggregate]/[WriteAggregate] identity resolution searches with ValueSource.Anything, and HttpChain.TryFindVariable returned that same owned route Variable. The aggregate-loader frame therefore depended on it, so MethodFrameArranger pulled the owned ReadHttpFrame into the method chain and gave it a Next — and generating it inline re-entered the whole chain, forever.

(This only happens with [AsParameters]: a plain route param or [FromQuery] id binds to a standalone frame that nothing re-embeds, so there's no cycle — which is why the existing /orders/latest/{id} and …/from-query endpoints work.)

Fix

When an endpoint binds an [AsParameters] object, a ValueSource.Anything lookup for a value that lives on that object now resolves to a member access on the bound object (query.Id) instead of the route/query read frame that AsParametersBindingFrame owns and generates inline. The aggregate loader then depends on the [AsParameters] object (a normal standalone frame in the chain) rather than on the inline-generated read frame — no cyclic Next.

  • New HttpChain.AsParametersVariable, set by the [AsParameters] strategy.
  • HttpChain.TryFindVariable short-circuits to new MemberAccessVariable(AsParametersVariable, member) for matching members, scoped to ValueSource.Anything so specific-source (RouteValue/FromQuery/Header) bindings are untouched.

Generated code is now straightforward and correct:

var getLatestOrderQuery = new GetLatestOrderQuery(Id);
var order = await documentSession.Events.FetchLatest<Order>(getLatestOrderQuery.Id, httpContext.RequestAborted);

Tests

  • New end-to-end test happy_path_reading_aggregate_with_id_from_asparameters_route exercising the route-id-through-[AsParameters] + [ReadAggregate] combination (would hang on regression), plus the sample endpoint in WolverineWebApi.
  • Entire Wolverine.Http.Tests suite run locally: 779 passed, 10 skipped, 0 failed — no regressions in the regression-prone HTTP codegen.

🤖 Generated with Claude Code

…Aggregate] hybrid endpoints

A hybrid HTTP endpoint that binds a parameter object with [AsParameters] (carrying
the aggregate id via a [FromRoute] member) and also loads the aggregate with
[ReadAggregate]/[WriteAggregate] sent HTTP code generation into an infinite loop
(StackOverflow):

  AsParametersBindingFrame -> ... -> ReadHttpFrame -> AsParametersBindingFrame -> ...

The route-read frame that AsParametersBindingFrame owns and generates inline (as a
constructor argument of the bound object) was ALSO pulled into the method's frame
chain, because the aggregate-id resolution (ValueSource.Anything) resolved the id to
that same route Variable. The arranger then wired the owned frame a Next back into
the chain, and generating it inline re-entered the whole chain forever.

Fix: when an endpoint binds an [AsParameters] object, a ValueSource.Anything lookup
for a value that lives on that object now resolves to a member access on the bound
object (e.g. query.Id) instead of the owned route/query read frame. The aggregate
loader then depends on the AsParameters object (a normal standalone frame) rather
than on the inline-generated read frame, so there is no cyclic Next. Scoped to
ValueSource.Anything so specific-source bindings are unaffected.

Adds an end-to-end test (route id through [AsParameters] + [ReadAggregate]) plus the
sample endpoint. Full Wolverine.Http.Tests suite green (779 passing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jeremydmiller jeremydmiller merged commit 7aef935 into main Jun 17, 2026
24 of 25 checks passed
This was referenced Jun 17, 2026
This was referenced Jun 26, 2026
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