Skip to content

Read HttpGraph instead of EndpointDataSource in WolverineApiDescriptionProvider. Closes GH-3371 - #3373

Merged
jeremydmiller merged 2 commits into
JasperFx:mainfrom
uniquelau:fix/api-description-provider-http-graph
Jul 12, 2026
Merged

jeremydmiller merged 2 commits into
JasperFx:mainfrom
uniquelau:fix/api-description-provider-http-graph

Conversation

@uniquelau

Copy link
Copy Markdown
Contributor

Fixes GH-3371: ASP.NET Core caches the first ApiExplorer read for the lifetime of the host (keyed on ActionDescriptorCollection.Version, which endpoint routing never bumps), and Wolverine endpoints only reach the DI EndpointDataSource at server start — so any earlier read permanently empties every OpenAPI document.

The change

WolverineApiDescriptionProvider enumerates WolverineHttpOptions.Endpoints (the HttpGraph) instead of the injected EndpointDataSource. The graph — including each chain's built RouteEndpoint — is complete as soon as MapWolverineEndpoints() returns, so the provider's answer is the same before and after server start.

This is behavior-preserving: every value in the produced ApiDescription already comes from chain.Endpoint; the data source enumeration only ever recovered the chain from endpoint metadata. The ExcludeFromDescription filter and the QUERY-verb skip are unchanged. Chains are enumerated rather than graph.Endpoints because the chain is what descriptions are built from — happy to switch if you'd rather keep the endpoint-shaped loop.

OpenApiCommand.ComposeEndpointDataSources is intentionally untouched: ASP.NET's own minimal-API provider still reads the composite source, so the CLI's merge is still needed for non-Wolverine endpoints.

Behavior notes

  • A read before MapWolverineEndpoints() returns still sees nothing — unchanged.
  • Only Wolverine's descriptions become start-independent. Minimal-API descriptions still compose at server start, so a pre-start read on a hybrid host now freezes a document with Wolverine endpoints but no minimal-API ones, where previously it froze fully empty. Hybrid apps with boot-time ApiExplorer readers should still defer them.
  • Calling MapWolverineEndpoints() twice now describes only the last graph. Double-mapping duplicates every route and was never a supported configuration.
  • A chain.Endpoint == null guard skips chains added to the graph after mapping — they never reached the data source either.

Testing

  • New regression test api_explorer_before_host_start: maps endpoints, never starts the host, asserts descriptions are complete on the first ApiExplorer read. Fails on the old provider, passes on the new one.
  • Wolverine.Http.Tests 818/818 and Wolverine.Http.AspVersioning.Tests 66/66, including generate_openapi_without_database (Wolverine centric OpenAPI generation command that isn't as problematic as Microsoft's #2903). The openapi command emits the same document with no database running.
  • Verified in a consuming application (Wolverine.HTTP + Marten + CritterWatch + per-surface OpenAPI documents): full suite green against packages built from this branch, generated OpenAPI spec byte-identical, and the empty-document failure no longer reproduces even with the application's interim workaround removed.

Copilot AI review requested due to automatic review settings July 11, 2026 21:33

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes an ASP.NET Core ApiExplorer caching edge case (GH-3371) by making Wolverine’s IApiDescriptionProvider read from Wolverine’s HttpGraph (WolverineHttpOptions.Endpoints) instead of the runtime EndpointDataSource, ensuring Wolverine endpoint descriptions are available on the first ApiExplorer read after MapWolverineEndpoints() even before host start.

Changes:

  • Update WolverineApiDescriptionProvider to enumerate HttpGraph.Chains and build ApiDescription entries directly from the already-built RouteEndpoint on each chain.
  • Add a regression test ensuring Wolverine ApiExplorer descriptions are complete prior to app.StartAsync().
  • Document the “pre-start ApiExplorer read” behavior and the remaining limitation for minimal APIs in hybrid apps.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
src/Http/Wolverine.Http/WolverineApiDescriptionProvider.cs Switch ApiExplorer enumeration from EndpointDataSource to HttpGraph to avoid start-time population/caching issues.
src/Http/Wolverine.Http.Tests/api_explorer_before_host_start.cs Add regression test verifying descriptions are present before host start (after mapping).
docs/guide/http/metadata.md Document start-independent Wolverine descriptions and hybrid-app caveat for minimal APIs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 1 to 3
using JasperFx.Core.Reflection;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Routing;

@jeremydmiller jeremydmiller left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed in depth — this is exactly right, and the key insight makes it safely behavior-preserving: even in the old code, the composite EndpointDataSource enumeration was only ever used to rediscover the HttpChain from endpoint metadata; the ApiDescription itself was always built by chain.CreateApiDescription() off chain.Endpoint. Since the data-source RouteEndpoint for a Wolverine endpoint is the same instance as chain.Endpoint (both from chain.BuildEndpoint() in HttpGraph), switching discovery to WolverineHttpOptions.Endpoints changes nothing post-start — it just makes the provider complete from the moment MapWolverineEndpoints() returns. Ordering, the ExcludeFromDescription filter, and the QUERY-verb skip all carry over verbatim, and AspVersioning's document discovery was already graph-based, so this makes the provider consistent with it.

Also verified as correct calls:

  • Leaving the CLI ComposeEndpointDataSources path untouched — still needed for minimal-API endpoints in hybrid CLI generation.
  • Documenting (not fixing) the hybrid-host limitation; that half is ASP.NET's version-keyed cache.
  • The new chain.Endpoint == null guard, which matches prior behavior (such chains never reached the composite source either).
  • The disclosed double-MapWolverineEndpoints() semantic (only the last graph is described) — double-mapping was never supported, acceptable.

One non-blocking suggestion: the regression test asserts a complete pre-start read; adding a start-then-re-read assertion in the same test would lock in the full GH-3371 loop (the cached collection staying complete across StartAsync). Happy to take that as a follow-up if you'd rather not touch the PR again.

Merging once CI is green. Thanks for an unusually well-researched fix — the root-cause analysis on the version-keyed cache saved us real time, and this also unblocks a monitoring scenario on our side (CritterWatch's capability snapshot is an in-box pre-start ApiExplorer reader).

@jeremydmiller
jeremydmiller merged commit f1f056e into JasperFx:main Jul 12, 2026
25 checks passed
mysticmind added a commit that referenced this pull request Jul 14, 2026
GH-3373 made Wolverine's own endpoint descriptions start-independent, but that
only half-closed the freeze. ASP.NET Core surfaces endpoints to its own
description providers through an EndpointDataSource that UseEndpoints() does not
fill until the server starts, and it caches the first ApiExplorer read for the
lifetime of the host. So on a hybrid host, a pre-start read cached a document
carrying every Wolverine route and silently omitting every minimal API and MVC
one — a plausible-looking subset, which is worse than the empty document it
replaced.

Publish the application's endpoints ahead of that read rather than refusing to
serve it. It is the same merge UseEndpoints() performs at startup, with the same
data source instances, so UseEndpoints() de-dupes it away and the router is
untouched. The `openapi` command already did this merge privately (GH-2903, which
is why it was never affected); lift it into HostEndpointDataSources so the
in-process read shares it, and fail that command loudly instead of writing a
partial spec at exit 0 if the merge ever stops working.

Publish only from the root route builder: a route group's DataSources are its
inner, un-prefixed sources that ASP.NET Core already publishes via the outer
GroupDataSource, so republishing them would register every endpoint in the group
a second time without its prefix or conventions. Wolverine warns rather than
serve a partial document for that shape.
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.

Read to ApiExplorer before server startup permanently empties every OpenAPI document

3 participants