feat(http): multi-version handlers + [MapToApiVersion] support#2662
Closed
outofrange-consulting wants to merge 6 commits into
Closed
feat(http): multi-version handlers + [MapToApiVersion] support#2662outofrange-consulting wants to merge 6 commits into
outofrange-consulting wants to merge 6 commits into
Conversation
added 4 commits
May 1, 2026 21:50
…ToApiVersion] Wolverine.Http now expands handler chains that declare multiple API versions into one endpoint per version at bootstrap time, before any IHttpPolicy runs. This lets a single handler method serve several versions (via repeated [ApiVersion] attributes or class-level [ApiVersion] declarations) while keeping per-version metadata, routes, and deprecation/sunset policies isolated to each clone. [MapToApiVersion] is honoured on methods of classes that advertise multiple versions: the listed subset is registered, with strict validation that every mapped version exists at class level. Mixing [ApiVersion] and [MapToApiVersion] on the same method fails fast with a descriptive error. Per-version OperationId is suffixed with the version to keep ASP.NET Core's "endpoint names must be globally unique" invariant intact when handlers carry explicit OperationIds. Each clone receives its own MethodCall instance so JasperFx codegen wires frame chains independently.
Allow patch and minor updates within the 10.x line so security and bug fixes flow through automatically while still pinning out of 11.x where breaking changes might land.
…ne attributes, simplify resolver
ApiVersioningPolicy.AttachMetadata now groups expanded clones by (verb, route-without-prefix)
and seeds each clone's ApiVersionModel with the union of sibling versions in
SupportedApiVersions / DeprecatedApiVersions. Consumers that read ImplementedApiVersions
(api-supported-versions header generators, OpenAPI tooling) see the full sibling set
instead of just the one declared version per clone.
HttpChain.CloneForVersion now scrubs the per-clone endpoint Metadata of any
[ApiVersion] / [MapToApiVersion] attribute that does not match the clone's own version.
applyMetadata() copies every class- and method-level attribute onto each clone, which
made downstream tooling report each clone as implementing every sibling version.
HttpChain.CloneForVersion also sanitises the OperationId suffix with a regex that
keeps only ASCII alphanumerics, so date-based versions like 2024-01-01 produce a
legal identifier (_v2024_01_01) instead of leaking hyphens.
MultiVersionExpansion is renamed Expand -> ExpandInPlace to make the in-place mutation
intent explicit at the call site, and the single-version branch is removed -
ApiVersioningPolicy.ResolveAttributes already handles single-version chains via
ResolveVersions, so the duplication is gone.
ApiVersionResolver loses the throwing Resolve shim (callers now invoke
ResolveVersions(...).FirstOrDefault() directly) and gets a single BuildResolutions
helper that the class-only, method-only, and [MapToApiVersion] branches all call.
WolverineWebApi gains CustomersV4AttributeDeprecatedEndpoint, a v4 route with no
options.Deprecate("4.0") policy, used by the new integration test
v4_endpoint_deprecation_comes_from_attribute_alone to prove the attribute-driven
deprecation path works independently of the options-driven map. The pre-existing
v1 deprecation test was a false positive (Program.cs registers
options.Deprecate("1.0") globally), so it is renamed to clarify it pins down the
options-driven path; the v4 test pins down the attribute-only path.
Tests added: per-clone supported/deprecated split, per-clone endpoint metadata
attribute scrubbing, two distinct multi-version handlers overlapping at a
(verb, route, version) triple, [MapToApiVersion] producing exactly one chain,
unversioned handler alongside multi-version handlers, date-based version
operation-id sanitisation, attribute-only v4 deprecation.
ApiVersionResolverTests.cs migrated from Resolve shim to ResolveVersions API.
Dead Apply helper removed from MultiVersionExpansionTests.cs.
docs/guide/http/versioning.md per-version metadata table updated for OperationId
suffix, the ApiVersionMetadata sibling-union semantics, and the Endpoint.Metadata
attribute-filter behaviour.
…ghten policy diagnostics The api-supported-versions response header now reads from the matched endpoint's ApiVersionMetadata (seeded by ApiVersioningPolicy with the full sibling union at the shared (verb, route-after-strip-prefix)) rather than the options-driven SunsetPolicies + DeprecationPolicies key union. Chains without per-endpoint metadata fall back to the previous options-based header. Cross-class sibling merging in AttachMetadata is now documented in <remarks> and pinned by an integration test (cross_class_chains_at_same_route_share_supported_versions) plus the new multi_version_endpoint_emits_api_supported_versions_with_sibling_set integration test. The duplicate-route diagnostic now uses the version-suffixed OperationId (instead of the shared DisplayName) so distinct conflicting clones are individually identifiable; the unversioned-policy error path keeps DisplayName to preserve user-supplied diagnostic labels. ResolveAttributes now indexes the resolver result explicitly after a count check, avoiding the default(ApiVersionResolution) struct foot-gun. The CloneForVersion attribute filter loop is condensed to one cast per item. Date-based version assertions in MultiVersionExpansionTests use ShouldStartWith to leave room for future Asp.Versioning suffix decorations. The duplicate-route test now asserts both conflicting handler class names appear in the diagnostic. The OperationId table in docs/guide/http/versioning.md is reworded to describe the sanitised-version-string suffix and includes the date-based example inline.
Contributor
Author
|
@jeremydmiller — could you take a look when you have a moment? This PR touches the core
Two design calls I'd particularly value your eyes on:
Tests cover the load-bearing pieces (TDD RED → GREEN trail on |
Member
|
@outofrange-consulting I'm going to let you deal w/ the merge conflicts:) |
added 2 commits
May 4, 2026 21:09
Merge upstream/main into feature/http-multiversion-handlers. Conflicts isolated to src/Http/Wolverine.Http/ApiVersioning/ApiVersioningPolicy.cs where upstream introduced [ApiVersionNeutral] support while this branch introduced multi-version expansion. Resolutions: - ResolveAttributes: skip resolver work when multi-version expansion has already assigned ApiVersion (avoids picking versions[0] on a multi- version method); otherwise check ApiVersionNeutralResolver.Resolve and fall through to ResolveVersions for single-version chains. - DetectConflicts: keep upstream's describe() callback for both versioned and neutral collision messages; keep this branch's OperationId-based naming so version-suffixed clones remain individually identifiable. - AttachMetadata docstring: combine upstream's neutral-chain rationale with this branch's sibling-grouping rationale. - Helpers: keep both StripVersionPrefix (sibling grouping) and EnsureExplicitOperationId (deduped from per-branch usage). Build green; 102 ApiVersioning tests pass.
…guard Conflict resolution in 5804e93 re-ordered ResolveAttributes such that the early `chain.ApiVersion is not null` skip ran *before* the neutrality check. That broke `method_level_neutral_clears_prior_fluent_apiversion_assignment` on CI: a chain with fluent `HasApiVersion(...)` set and a method-level `[ApiVersionNeutral]` would skip the resolver entirely and never get `IsApiVersionNeutral = true` / its stale fluent version cleared. Reverse the order so neutrality runs first. Multi-version clones cannot be neutral (their underlying method declares one or more `[ApiVersion]`, so `ApiVersionNeutralResolver.Resolve` returns false), so reaching the neutrality check on a clone is a no-op. The pre-assigned guard then preserves the clone's already-correct version (and any genuine fluent single-version assignment).
This was referenced May 6, 2026
Contributor
Author
jeremydmiller
pushed a commit
that referenced
this pull request
May 7, 2026
…ordering Squashed from PR #2662 review iterations: - tighten multi-version metadata isolation and per-clone attribute filtering - emit per-endpoint api-supported-versions header using sibling version union - reorder neutrality check before pre-assigned ApiVersion guard
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.
Follow-up to #2633.
Summary
Wolverine.Http now expands handler chains that declare multiple API versions into one endpoint per version at bootstrap, before any
IHttpPolicyruns. A single handler method can serve several versions (via repeated[ApiVersion]attributes or class-level[ApiVersion]declarations) while keeping per-version metadata, routes, and deprecation/sunset policies isolated to each clone.[MapToApiVersion]is honoured on methods of classes that advertise multiple versions: the listed subset is registered, with strict validation that every mapped version exists at class level. Mixing[ApiVersion]and[MapToApiVersion]on the same method fails fast with a descriptive error.Floats
Asp.Versioning.Abstractionsto[10.0.0,11.0.0).Implementation notes
HttpChain.CloneForVersion(ApiVersion, bool isDeprecated)is the per-version expansion primitive. Each clone gets a freshMethodCall(sharing throws "Frame chain re-arranged" from JasperFx codegen). Per-cloneOperationIdis suffixed with_v{sanitised-version-string}to keep ASP.NET Core's "globally unique endpoint name" invariant; sanitisation is regex-based ([^A-Za-z0-9]→_) so date-based versions like2024-01-01produce_v2024_01_01instead of breaking the rule.Metadata.Addcallback that scrubs[ApiVersion]/[MapToApiVersion]attributes mismatched with the clone's own version. Without this, downstream OpenAPI tooling (Swashbuckle'sWolverineApiVersioningSwaggerOperationFilter) would report each clone as implementing all sibling versions.MultiVersionExpansion.ExpandInPlaceruns inHttpGraph.DiscoverEndpointsbefore anyIHttpPolicyso middleware composition applies uniformly to all clones. Single-version chains are left toApiVersioningPolicy.ResolveAttributes— no duplicated work.ApiVersionHeaderWriternow readsApiVersionMetadata.ImplementedApiVersionsfrom the matched endpoint when emittingapi-supported-versions. Falls back tooptions.SunsetPolicies.Keys ∪ DeprecationPolicies.Keysfor chains without per-endpoint metadata. The header therefore correctly reflects the sibling-version union for clones (e.g. v1 of a[ApiVersion("1.0"), ApiVersion("2.0"), ApiVersion("3.0")]handler emitsapi-supported-versions: 1.0, 2.0, 3.0).ApiVersioningPolicy.AttachMetadataseedsApiVersionModel(declared, supported, deprecated)per clone by grouping all chains by(verb, route-without-version-prefix). Cross-class merging at the same route is intentional (matches Asp.Versioning conventions) and documented in the method's<remarks>.Risk areas (touches core abstractions)
This PR modifies
HttpChain.csandHttpGraph.cs— please review the expansion-before-policy ordering and the per-cloneMethodCallcloning rationale carefully. TheMetadata.Addfilter callback inCloneForVersionis also load-bearing for OpenAPI correctness.Test plan
MultiVersionExpansionTests— 13 unit tests (expansion mechanics, attribute filtering, sibling union, date-based versions, overlap detection across distinct handlers,[MapToApiVersion]single-version path, unversioned-alongside, cross-class union)MultiVersionResolverTests— 138 lines covering attribute resolution and[MapToApiVersion]validationmulti_version_integration_tests— e2e via WolverineWebApi includingmulti_version_endpoint_emits_api_supported_versions_with_sibling_set(TDD-confirmed RED → GREEN), per-version deprecation propagation, isolated v4 attribute-only deprecation fixture (decoupled fromProgram.csglobalDeprecate("1.0")to avoid false positives)ApiVersionResolverTestsmigrated toResolveVersionsAPISample app
WolverineWebApi/ApiVersioning/CustomersMultiVersionEndpoint.cs— three versions on one classWolverineWebApi/ApiVersioning/CustomersV4AttributeDeprecatedEndpoint.cs— isolates per-version-attribute deprecation from options-driven deprecationDocs
docs/guide/http/versioning.mdextended with multi-version +[MapToApiVersion]sectionsOperationIdrow (_v{sanitised-version-string})