OpenAPI: derive parameters from the full binding chain, plus a shape-test harness (GH-3380)#3418
Merged
jeremydmiller merged 1 commit intoJul 14, 2026
Conversation
…ndpoint signature (GH-3380) HttpChain's ApiDescription only knew about values bound by the endpoint method itself (plus its [AsParameters] container). Anything bound elsewhere in the chain could go undeclared: - query string / header values bound only by an After/Finally postprocessor were omitted from the operation entirely - route parameter types were read off resolved binding *variables*, so they silently degraded to the route constraint (or string) whenever the description was assembled before those frames were resolved — ASP.NET Core caches the first ApiExplorer read, which can happen long before codegen CreateApiDescription now walks every MethodCall in the chain — the endpoint method, middleware (compound handler Load/LoadAsync/Before, attribute- and policy-applied middleware) and postprocessors — plus their [AsParameters] container members: - every RoutePattern parameter is still declared as a required path parameter, with its type taken from a resolved route variable, then the binding chain (honoring [FromRoute(Name = "...")]), then the route constraint, and finally string - [FromQuery]/[FromHeader] arguments anywhere in the chain are declared once, deduplicated by name + binding source Also adds the OpenAPI shape-test coverage that was missing (and that let this class of omission ship): openapi_shape_tests renders the real Microsoft.AspNetCore.OpenApi document for an infrastructure-free host and asserts on the operation's parameters and request body, and the WolverineWebApi Expect* attributes (now with [ExpectParameterCount] and ExpectParameter.Required) cover the same shapes on the Swashbuckle stack. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jul 13, 2026
This was referenced Jul 14, 2026
mysticmind
added a commit
that referenced
this pull request
Jul 14, 2026
… of falling back to string (GH-3420) On the [AggregateHandler] shape: [AggregateHandler] [WolverinePost("/orders/{id}/confirm")] public static OrderConfirmed Handle(ConfirmOrder command, Order order) nothing in the endpoint signature binds {id} -- the aggregate id is read off the command by the aggregate workflow during codegen -- so with no route constraint to fall back on, GH-3418's resolution ladder (route variable -> binding chain -> route constraint -> string) came up empty on every rung and the operation declared `id` as `string` rather than `uuid`. The aggregate's identity type is Marten/Polecat domain knowledge that Wolverine.Http cannot infer from method signatures, so this adds a seam for the aggregate workflow to hand that type back: - IRoutedChain (Wolverine core, so Wolverine.Marten/Wolverine.Polecat can reach it without referencing Wolverine.Http) exposes the chain's route parameter names and lets middleware declare the CLR type behind one. It is purely descriptive: it creates no binding and does not affect generated code. - HttpChain implements it, and CreateApiDescription consults declared types *after* the route constraint -- an explicit {id:guid} is what ASP.NET Core actually enforces, so a declared type can only ever improve on the `string` fallback and cannot change an endpoint that already resolves a type. - AggregateHandling.Apply (Marten and Polecat both) declares the aggregate id's type against the first route token matching WriteAggregateAttribute.FindIdentity's own precedence: RouteOrParameterName, then {aggregate}Id, then id. [WriteAggregate]/[ReadAggregate]/[Aggregate] already bound the route value themselves and were unaffected; they are now pinned by tests as part of the same family. Coverage on both OpenAPI stacks: openapi_shape_tests grows Marten aggregate shapes (Microsoft.AspNetCore.OpenApi) and /orders/{id}/confirm in WolverineWebApi grows an [ExpectParameter] expectation (Swashbuckle). The shape harness registers Marten against an unreachable database (port 9999, per generate_openapi_without_database) so it stays infrastructure-free by construction -- verified by running it with the Postgres container stopped. api_explorer_before_host_start builds a second host over that same assembly and needs the same registration.
This was referenced Jul 14, 2026
This was referenced Jul 16, 2026
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.
Fixes #3380
What was actually wrong (and what wasn't)
Being straight about the repro first: the literal
parameters: []symptom in the issue does not reproduce onmain(6.17.2).CreateApiDescriptionhas always loopedRoutePattern.Parameters, so a route token is emitted no matter which method binds it. I reproduced every reported shape — compoundLoadAsyncbinding both ids (with a service +CancellationToken), kebab-case{order-id:long}+[FromRoute(Name = ...)],[AsParameters]+LoadAsynccombinations — against both OpenAPI stacks (Swashbuckle and Microsoft.AspNetCore.OpenApi), and the path parameters were present and correctly typed in all of them. My best guess is the reporter's document predates the GH-3374 fix (their host wasn't starting) — but I could not confirm it.What the investigation did turn up is the real version of the root cause the issue describes — the API description was derived from the endpoint method signature plus its own resolved binding variables, not from the full chain:
After/Finallypostprocessor were omitted from the operation entirely. This is a genuine, reproducing bug (the newquery_values_bound_only_by_a_postprocessor_are_declaredtest fails onmain).string) whenever the description was assembled before those frames were resolved. ASP.NET Core caches the first ApiExplorer read, and that read can happen long before codegen (build-time OpenAPI generation, theopenapicommand, monitoring snapshots).The fix
HttpChain.CreateApiDescriptionnow walks everyMethodCallin the chain — the endpoint method, all middleware (compound handlerLoad/LoadAsync/Before, attribute- and policy-applied middleware) and all postprocessors — plus their[AsParameters]container members:RoutePatternparameter is declared as a required path parameter, with its type resolved in order: resolved route variable → the binding chain (honoring[FromRoute(Name = "order-id")], ignoring name collisions with non-bindable types like services) → the route constraint ({id:guid}) →string. That is the non-negotiable floor, now structurally guaranteed rather than dependent on codegen having run.[FromQuery]/[FromHeader]arguments anywhere in the chain are declared, deduplicated by name + binding source.The shape-test harness (the actual gap)
There were essentially no assertions on the rendered document's
parameters/requestBody. Now there are, on both stacks:1.
src/Http/Wolverine.Http.Tests/openapi_shape_tests.cs— Microsoft.AspNetCore.OpenApi. Renders the real OpenAPI document (same path theopenapicommand uses, no host start, no database) for a host pinned to the infrastructure-freeWolverine.Http.Tests.DifferentAssembly, and asserts on it viaParametersFor(path, method)/HasRequestBody(...)/RequestBodyPropertiesFor(...).To add a shape: add an endpoint to
Wolverine.Http.Tests.DifferentAssembly/OpenApiShapeEndpoints.cs, then add a[Fact]using those helpers.Covered today: compound-handler-only route bindings (the #3380 case), an unconstrained route token typed purely from the binding chain, a renamed
[FromRoute(Name = ...)]compound binding, compound-handler-only query + header, postprocessor-only query, plain handler-signature route + query, a request body alongside a compound route binding, a route value nothing binds (floor → requiredstring),[AsParameters]route + query, and an[AsParameters]container sharing a route value with a compound handler (declared exactly once).2. WolverineWebApi
Expect*attributes — Swashbuckle. Folded into the existing GH-3135 harness (open_api_generation.verify_open_api_expectations) rather than duplicated. Added[ExpectParameterCount](so[ExpectParameter]can't miss a phantom/duplicate parameter) andExpectParameter.Required, plusCompoundHandlerOpenApiEndpoints.cswith the #3380 shapes.To add a shape: annotate an endpoint method in WolverineWebApi with the
Expect*attributes. No test code required — the theory picks it up automatically.Known gap (not fixed here)
Route ids bound only by a Marten aggregate frame on an unconstrained route (e.g.
[AggregateHandler] [WolverinePost("/orders/{id}/confirm")]) still render asstringrather thanuuid— that binding is decided during codegen, after the description is cached, and the id type is Marten-domain knowledge thatWolverine.Httpcan't infer from the method signatures. Worth a follow-up issue.Tests
Wolverine.Http.Tests: 857 passed, 0 failed, 10 skipped (net9.0)Wolverine.Http.AspVersioning.Tests: 66 passed (net10.0)dotnet build wolverine.slnx -c Release: succeeded🤖 Generated with Claude Code