Skip to content

[Http] [AsParameters] + compound handler LoadAsync: generated code fails to compile (CS0136 / CS0841) #3374

Description

@outofrange-consulting

Version

WolverineFx.Http / WolverineFx.EntityFrameworkCore 6.17.1, .NET 10.

What happens

Combining [AsParameters] on the handler with a compound handler LoadAsync that also binds a route variable makes the runtime codegen emit code that does not compile, so the host fails at startup with InvalidOperationException: Compilation failures!:

CS0841: Cannot use local variable 'orderDbContext' before it is declared   (x5)
CS0136: A local or parameter named 'order_id_rawValue' cannot be declared in this scope because that name is used in an enclosing local scope
CS0136: A local or parameter named 'order_id' cannot be declared in this scope because that name is used in an enclosing local scope
CS0136: A local or parameter named 'stockCardOrder' cannot be declared in this scope because that name is used in an enclosing local scope

The route variable is bound twice — once for the LoadAsync scope, once for the [AsParameters] binding frame — and the two sets of locals collide.

Repro

public sealed record UpsertLinesRequest
{
    [FromRoute(Name = "order-id")]
    public long OrderId { get; set; }

    [FromBody]
    public LinesBody Body { get; set; } = new([]);
}

public sealed record LinesBody(IReadOnlyList<string> Lines);

public static class UpsertLinesEndpoint
{
    // compound handler: loads the aggregate from the same route variable
    public static async Task<Order> LoadAsync(
        [FromRoute(Name = "order-id")] long orderId,
        OrderDbContext dbContext,
        CancellationToken cancellationToken)
        => await dbContext.Orders.FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken)
           ?? throw new OrderNotFoundException();

    [WolverinePut("/v3/orders/{order-id:long}/order-lines")]
    public static async Task<Response> Put(
        [AsParameters] UpsertLinesRequest request,
        [NotBody] Order order,
        [NotBody] OrderDbContext dbContext,
        CancellationToken cancellationToken)
    {
        // ... uses request.Body.Lines and the loaded `order`
    }
}

Variant also tried: making LoadAsync take the same [AsParameters] UpsertLinesRequest instead of the raw route parameter. Same failure, plus CS0136: A local or parameter named 'request' ... — the [AsParameters] binding frame is emitted in both scopes.

Works

Binding the route variable directly on the handler (and separately on LoadAsync) compiles and runs fine:

public static async Task<Response> Put(
    [FromRoute(Name = "order-id")] long orderId,   // unused in the body, aggregate still comes from LoadAsync
    UpsertLinesRequest request,
    Order order,
    OrderDbContext dbContext,
    CancellationToken cancellationToken)

Notes

This looks like the EF Core / compound-handler analogue of GH-3135, which was fixed for Marten's [WriteAggregate] (write_aggregate_with_asparameters): there, the route id flows through an [AsParameters] object while the aggregate is resolved from that same id. The same combination with a LoadAsync compound handler still breaks, at codegen time rather than with a 500.

Nothing in the AsParameters docs rules the combination out — the only documented restriction is form + body together.

Happy to send a PR if you point me at the right seam (the duplicate route-binding locals look like they come from the binding frames being emitted once per scope rather than reused).

Side note (possibly a separate issue — tell me and I'll open one)

Route variables consumed only by LoadAsync are missing from the generated OpenAPI operation. An endpoint whose route is /v3/orders/{orderId:long}/order-lines/{orderLineId:long}, where both ids are bound in LoadAsync and neither appears in the handler signature, produces an operation with "parameters": [] — the path parameters are not declared at all. It looks like the OpenAPI metadata is derived from the handler signature rather than from the route template plus the full binding chain.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions