Skip to content

Add Trellis.Asp.ApiVersioning package: CreatedAtVersionedRoute helpers + TRLS023 analyzer#480

Merged
xavierjohn merged 11 commits into
mainfrom
feat/api-versioning-helper
May 8, 2026
Merged

Add Trellis.Asp.ApiVersioning package: CreatedAtVersionedRoute helpers + TRLS023 analyzer#480
xavierjohn merged 11 commits into
mainfrom
feat/api-versioning-helper

Conversation

@xavierjohn
Copy link
Copy Markdown
Owner

@xavierjohn xavierjohn commented May 8, 2026

Issue

Under query/header API versioning, today's CreatedAtRoute(...) pattern produces a Location header that silently omits the api-version parameter unless every author remembers to add ["api-version"] = "<version>" to the route-value dictionary on every 201 Created. Forgetting it produces a Location URL that 404s on dereference — invisible without integration tests.

Both 2026-05-06 lab models hit this. The pattern is documented as a Mistake-regression routing hint in the cookbook, but doc-only guidance hasn't been enough to shift behaviour (B37: "Opus regressed on this between 2026-04-29 and 2026-04-30 despite Patterns Index callout").

Fix

New package Trellis.Asp.ApiVersioning with two integrated parts that together close the failure mode at runtime + compile time:

Part A — runtime helper

Three CreatedAtVersionedRoute extension overloads on HttpResponseOptionsBuilder<TDomain> resolve and inject the api-version route value at request time:

result.ToHttpResponse(opts => opts
    .CreatedAtVersionedRoute("Customers_GetById", c => c.Id.Value));
//   ↑ Location = /customers/{id}?api-version=<requested-version>

Per-request resolution: HttpContext.RequestedApiVersion → endpoint metadata's single declared version → ApiVersioningOptions.DefaultApiVersion → throw InvalidOperationException. Skips injection on [ApiVersionNeutral] endpoints and URL-segment-versioned routes. Three overloads cover multi-key, single-id-convenience, and explicit-version-pin cases.

Part B — TRLS023 analyzer + code-fix

Warns when HttpResponseOptionsBuilder<T>.CreatedAtRoute(routeName, c => new RouteValueDictionary { ... }) is invoked on a controller with [ApiVersion(...)] and the dictionary literal does not include an "api-version" key. The code-fix mechanically rewrites to CreatedAtVersionedRoute(...). Bails to a false-negative for non-literal route values, [ApiVersionNeutral] controllers, and non-versioned controllers — preventing alarm fatigue.

This closes B37's regression risk: the analyzer catches the bug at compile time even for code that hasn't migrated to the helper.

Architecture (additive)

Trellis.Asp gains one new generic primitive — HttpResponseOptionsBuilder<TDomain>.WithRouteValueResolver(string key, Func<HttpContext, string?> resolver) — that lets any consumer register a per-request route-value injector. Trellis.Asp.ApiVersioning builds CreatedAtVersionedRoute on top of this hook. The hook is reusable for tenant id, culture code, or any other cross-cutting per-request concern; the api-versioning package's Asp.Versioning.* dependency stays contained.

Tests

Test surface Count Coverage
Trellis.Asp.Tests (hook layer) 3 unit tests WithRouteValueResolver arg validation + chaining contract
Trellis.Asp.ApiVersioning.Tests 6 integration tests Single-version, multi-version with/without default, explicit-pin, single-id-convenience, fallback ordering
Trellis.Analyzers.Tests 5 analyzer tests Versioned controller missing key → warning; with key → no warning; non-versioned → no warning; [ApiVersionNeutral] → no warning; non-literal route values → no warning

Local verification: 924/924 Trellis.Asp; 211/211 Trellis.Analyzers (was 206 + 5 new); 6/6 new package; all 19 other framework test projects pass; local docfx build --warningsAsErrors clean.

Out of scope (deferred, tracked in BACKLOG)

  • Multi-version literal-propagation generator. A first-pass ApiVersionConstants.Current source generator was implemented and reverted in this PR after recognising the design flaw: a single Current constant doesn't model services that support multiple API versions concurrently — older [ApiVersion(...)] literals stay pinned to historical values when a new version ships, so Current only covers brand-new endpoints. A correct generator would scan [ApiVersion] attributes at build time and emit a multi-version directory (KnownApiVersions.V20261112, V20261201, Current = V20261201). Tracked separately as a design spike.
  • Text-asset substitution for http-client.env.json, .vscode/launch.json, api.http. The MSBuild target that mutates tracked files needs its own design pass (file-mutation vs. template-with-output, IDE behaviour, merge conflicts).
  • Custom route-value key configuration — hosts using a non-default IApiVersionReader parameter name should bypass CreatedAtVersionedRoute and call WithRouteValueResolver directly for now.

…lpers

Under query/header API versioning, today's CreatedAtRoute pattern produces a Location header that silently omits the api-version parameter unless every author remembers to add it to the route-value dictionary on every 201 Created. Forgetting it produces a Location URL that 404s on dereference — invisible without integration tests. Both 2026-05-06 lab models hit this; doc-only guidance has not been enough to shift behaviour.

New package Trellis.Asp.ApiVersioning adds three CreatedAtVersionedRoute extension overloads on HttpResponseOptionsBuilder<TDomain> that resolve and inject the api-version route value at request time:

- (routeName, routeValues) — multi-key route values.
- (routeName, idSelector, idRouteKey = "id") — single-id convenience.
- (routeName, routeValues, ApiVersion explicitVersion) — escape hatch for cross-version Location pinning.

Per-request resolution order:
1. HttpContext.RequestedApiVersion — primary signal from the configured IApiVersionReader chain.
2. Endpoint metadata's single declared version — fallback when (1) is null.
3. ApiVersioningOptions.DefaultApiVersion — host-level fallback.
4. Otherwise — throw InvalidOperationException. Silent picking would resurrect the original 404 bug.

Skips injection on [ApiVersionNeutral] endpoints and URL-segment-versioned routes (those carry the version as a route token; ambient routing handles substitution).

Architecture is two-layer:
- Trellis.Asp gains a generic primitive: HttpResponseOptionsBuilder<TDomain>.WithRouteValueResolver(string key, Func<HttpContext, string?> resolver) — registers a per-request route-value injector for Location-header generation. No Asp.Versioning dependency. Reusable for tenant id, culture code, or any other cross-cutting per-request concern. TrellisHttpResult.ResolveLocation invokes resolvers after the route-values selector.
- Trellis.Asp.ApiVersioning depends on Trellis.Asp + Asp.Versioning.Mvc/.Mvc.ApiExplorer/.Http and provides the api-versioning-specific resolver plus the three CreatedAtVersionedRoute extensions.

6 TestHost integration tests cover the resolution order, multi-version controllers, the explicit-version pin, and the single-id convenience overload. 3 new unit tests on Trellis.Asp side cover the WithRouteValueResolver argument validation and chaining contract.

Deferred follow-ups: TRLS_VER001 analyzer (warns on bare CreatedAtRoute calls missing the api-version key, with code-fix to migrate); build-time ApiVersionConstants literal-propagation generator; custom route-value key configuration.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 8, 2026

Test Results

5 545 tests  +24   5 530 ✅ +24   4m 35s ⏱️ +17s
   22 suites + 1      15 💤 ± 0 
   22 files   + 1       0 ❌ ± 0 

Results for commit 7e811ff. ± Comparison against base commit 0a19d40.

♻️ This comment has been updated with latest results.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 8, 2026

Codecov Report

❌ Patch coverage is 71.54150% with 72 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.88%. Comparing base (0a19d40) to head (7e811ff).

Files with missing lines Patch % Lines
...ers/src/CreatedAtRouteMissingApiVersionAnalyzer.cs 69.23% 12 Missing and 16 partials ⚠️
.../CreatedAtRouteMissingApiVersionCodeFixProvider.cs 67.53% 12 Missing and 13 partials ⚠️
...tpResponseOptionsBuilderApiVersioningExtensions.cs 65.38% 14 Missing and 4 partials ⚠️
Trellis.Asp/src/Response/TrellisHttpResult.cs 90.90% 0 Missing and 1 partial ⚠️

❌ Your patch check has failed because the patch coverage (71.54%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #480      +/-   ##
==========================================
- Coverage   85.17%   84.88%   -0.30%     
==========================================
  Files         302      305       +3     
  Lines       11503    11754     +251     
  Branches     2471     2542      +71     
==========================================
+ Hits         9798     9977     +179     
- Misses       1167     1205      +38     
- Partials      538      572      +34     
Files with missing lines Coverage Δ
Trellis.Analyzers/src/DiagnosticDescriptors.cs 100.00% <100.00%> (ø)
...lis.Asp/src/Response/HttpResponseOptionsBuilder.cs 100.00% <100.00%> (ø)
Trellis.Asp/src/Response/TrellisHttpResult.cs 92.02% <90.90%> (-0.19%) ⬇️
...tpResponseOptionsBuilderApiVersioningExtensions.cs 65.38% <65.38%> (ø)
.../CreatedAtRouteMissingApiVersionCodeFixProvider.cs 67.53% <67.53%> (ø)
...ers/src/CreatedAtRouteMissingApiVersionAnalyzer.cs 69.23% <69.23%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…erator)

Part B — TRLS023 analyzer + code-fix in Trellis.Analyzers package.

Warns when HttpResponseOptionsBuilder<T>.CreatedAtRoute(routeName, c => new RouteValueDictionary { ... }) is invoked on a controller with [ApiVersion(...)] and the dictionary literal does not include an "api-version" key. The code-fix mechanically rewrites to CreatedAtVersionedRoute(...). Bails to false-negative for non-literal route values, [ApiVersionNeutral] controllers, and non-versioned controllers — preventing alarm fatigue.

This closes B37's regression risk: the analyzer catches the bug at compile time even for code that hasn't migrated to Part A's helper. 5 analyzer tests cover the four-quadrant matrix (versioned/non-versioned × literal/non-literal route values) plus the [ApiVersionNeutral] opt-out.

Part C1 — ApiVersionConstantsGenerator source generator bundled in Trellis.Asp.ApiVersioning.nupkg.

Reads the <TrellisApiVersion> MSBuild property and emits ApiVersionConstants.g.cs with public const string Current and CurrentNamespaceSuffix. Const semantics let the values flow into [ApiVersion(...)] attribute arguments — eliminating the literal-propagation drift across controllers when the active API version changes. A build/Trellis.Asp.ApiVersioning.props ships in build/ + buildTransitive/ so consumers automatically get CompilerVisibleProperty without per-project boilerplate. 3 generator tests verify the property-to-constant flow plus const-in-attribute usability.

Verification: 924/924 Trellis.Asp.Tests pass; 211/211 Trellis.Analyzers.Tests pass (was 206 + 5 new); 9/9 Trellis.Asp.ApiVersioning.Tests pass; all 19 other framework test projects pass (4400 tests total); local docfx --warningsAsErrors clean.

Deferred: Part C2 (text-asset substitution for http-client.env.json/.vscode/launch.json/api.http) — needs its own design pass for file-mutation strategy. Custom route-value key configuration — hosts using non-default IApiVersionReader parameter names should bypass CreatedAtVersionedRoute and call WithRouteValueResolver directly for now.
@xavierjohn xavierjohn changed the title Add Trellis.Asp.ApiVersioning package with CreatedAtVersionedRoute helpers Add Trellis.Asp.ApiVersioning package: CreatedAtVersionedRoute helpers + TRLS023 analyzer + ApiVersionConstants generator May 8, 2026
The flat ApiVersionConstants.Current model didn't fit services that support multiple API versions concurrently. Older [ApiVersion("...)] literals stay pinned to specific historical versions when a new version ships (because those endpoints ARE that version), so a single Current constant only covers brand-new endpoints — most references in a long-lived service stay as version-specific literals.

A correct generator would emit a multi-version directory (e.g., scan [ApiVersion] attributes at build time, emit KnownApiVersions.V20261112, V20261201, Current = V20261201) where each version literal lives exactly once. Tracked in BACKLOG.md as a separate design spike.

This PR keeps Parts A (runtime CreatedAtVersionedRoute helpers) and B (TRLS023 analyzer + code-fix) which together close the original "201 Created Location 404s on dereference" failure mode at runtime + compile time. The deferred multi-version directory and text-asset substitution work both go to follow-up PRs.
@xavierjohn xavierjohn changed the title Add Trellis.Asp.ApiVersioning package: CreatedAtVersionedRoute helpers + TRLS023 analyzer + ApiVersionConstants generator Add Trellis.Asp.ApiVersioning package: CreatedAtVersionedRoute helpers + TRLS023 analyzer May 8, 2026
@xavierjohn xavierjohn requested a review from Copilot May 8, 2026 14:51
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds first-class support for API-version-aware 201 Created Location headers in Trellis ASP integration by introducing a generic per-request route-value injection hook, a new Trellis.Asp.ApiVersioning package that builds CreatedAtVersionedRoute(...) on top of that hook, and a new analyzer/code-fix (TRLS023) to prevent regressions when authors use CreatedAtRoute(...) without including "api-version".

Changes:

  • Add HttpResponseOptionsBuilder<TDomain>.WithRouteValueResolver(...) and apply registered resolvers during CreatedAtRoute/CreatedAtAction link generation.
  • Introduce Trellis.Asp.ApiVersioning with CreatedAtVersionedRoute overloads and integration tests.
  • Add TRLS023 analyzer + code fix and wire it into analyzer release tracking.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
Trellis.slnx Adds the new Trellis.Asp.ApiVersioning projects to the solution folder structure.
Trellis.Asp/tests/HttpResponseOptionsBuilderTests.cs Adds unit tests for WithRouteValueResolver argument validation + chaining.
Trellis.Asp/src/Response/TrellisHttpResult.cs Applies per-request route-value resolvers when generating Location for route/action modes.
Trellis.Asp/src/Response/HttpResponseOptionsBuilder.cs Adds WithRouteValueResolver API and plumbs resolver dictionary into built options.
Trellis.Asp.ApiVersioning/src/Trellis.Asp.ApiVersioning.csproj Introduces the new package project and its Asp.Versioning dependencies.
Trellis.Asp.ApiVersioning/src/HttpResponseOptionsBuilderApiVersioningExtensions.cs Adds CreatedAtVersionedRoute overloads and per-request version resolution logic.
Trellis.Asp.ApiVersioning/tests/Trellis.Asp.ApiVersioning.Tests.csproj Adds the new test project and references needed for integration testing.
Trellis.Asp.ApiVersioning/tests/CreatedAtVersionedRouteTests.cs Integration tests asserting Location contains (or omits) api-version correctly across configurations.
Trellis.Asp.ApiVersioning/NUGET_README.md Package README content for NuGet.
Trellis.Analyzers/src/TrellisDiagnosticIds.cs Adds TRLS023 diagnostic ID constant.
Trellis.Analyzers/src/DiagnosticDescriptors.cs Adds descriptor text for TRLS023.
Trellis.Analyzers/src/CreatedAtRouteMissingApiVersionAnalyzer.cs Implements TRLS023 detection.
Trellis.Analyzers/src/CreatedAtRouteMissingApiVersionCodeFixProvider.cs Implements code fix rewriting CreatedAtRouteCreatedAtVersionedRoute.
Trellis.Analyzers/tests/CreatedAtRouteMissingApiVersionAnalyzerTests.cs Adds analyzer tests for warning/no-warning scenarios.
Trellis.Analyzers/src/AnalyzerReleases.Unshipped.md Adds TRLS023 to the unshipped analyzer release log.
Directory.Packages.props Adds central version for Asp.Versioning.Http.
CHANGELOG.md Documents the new package + analyzer and the rationale.

Comment thread Trellis.Asp.ApiVersioning/src/Trellis.Asp.ApiVersioning.csproj
Comment thread Trellis.Asp.ApiVersioning/NUGET_README.md Outdated
Comment thread Trellis.Asp/src/Response/TrellisHttpResult.cs Outdated
Comment thread Trellis.Analyzers/src/CreatedAtRouteMissingApiVersionAnalyzer.cs Outdated
Comment thread Trellis.Asp/src/Response/TrellisHttpResult.cs Outdated
…nce, add using on fix

TRLS023 false-positive: case-sensitive 'api-version' key match. RouteValueDictionary uses case-insensitive comparison at runtime, so [API-VERSION] already supplies the route value. Compare with OrdinalIgnoreCase.

TRLS023 false-positive: walked BaseType chain to find [ApiVersion]. The attribute is declared with Inherited=false, so derived controllers without their own [ApiVersion] are not versioned by API Versioning itself. Inspect only GetAttributes() on the immediate type.

TRLS023 code fix produced uncompilable code: only renamed CreatedAtRoute to CreatedAtVersionedRoute without adding 'using Trellis.Asp.ApiVersioning;'. Add the using directive when missing, matching existing line-ending style.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 8 comments.

Comment thread Trellis.Analyzers/src/CreatedAtRouteMissingApiVersionAnalyzer.cs Outdated
Comment thread Trellis.Analyzers/src/CreatedAtRouteMissingApiVersionCodeFixProvider.cs Outdated
Comment thread Trellis.Asp.ApiVersioning/src/Trellis.Asp.ApiVersioning.csproj
Comment thread CHANGELOG.md
Comment thread Trellis.Asp.ApiVersioning/NUGET_README.md Outdated
Comment thread Trellis.Asp/src/Response/TrellisHttpResult.cs Outdated
- TrellisHttpResult: clone RouteValueDictionary before applying per-request route-value resolvers. Selectors that return cached/shared instances would otherwise mutate cross-request and create thread-safety issues. Applied at both LocationKind.Route and LocationKind.Action sites.

- HttpResponseOptionsBuilderApiVersioningExtensions: drop unused System.Linq and global::Asp.Versioning.ApiExplorer using directives.

- Trellis.Asp.ApiVersioning.csproj: add <TrellisApiRefName>asp-apiversioning</TrellisApiRefName> so Directory.Build.targets auto-packs the API reference markdown into the nupkg, matching the pattern used by every other Trellis package.

- docs/docfx_project/api_reference/trellis-api-asp-apiversioning.md: new LLM-targeted API reference for the package (3 CreatedAtVersionedRoute overloads + behavioural notes covering [ApiVersionNeutral], URL-segment versioning, resolution order, manual api-version key override).

- NUGET_README.md: align with implementation — say HttpContext.RequestedApiVersion (the Asp.Versioning.Http extension property), not the non-existent GetRequestedApiVersion() method.

- CreatedAtRouteMissingApiVersionAnalyzer: clarify that the RouteValueDictionary(new {...}) ctor shape is intentionally a false-negative — supporting it would require an anonymous-object-member visitor.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 5 comments.

Comment thread docs/docfx_project/api_reference/trellis-api-asp-apiversioning.md Outdated
Comment thread docs/docfx_project/api_reference/trellis-api-asp-apiversioning.md Outdated
Comment thread docs/docfx_project/api_reference/trellis-api-asp-apiversioning.md Outdated
TRLS023 analyzer: detect anonymous-object ctor-arg shape (new RouteValueDictionary(new { id = ... })) as definitely-missing api-version. C# property names cannot contain hyphens, so an anonymous-object route-values argument can never carry the api-version key. Refactor ExtractRouteValueDictionaryInitializer into ClassifyRouteValuesShape returning a tri-state (Initializer / AnonymousObjectCtorArg / Unrecognized).

TRLS023 code fix: HasUsing now walks namespace-scoped usings (NamespaceDeclarationSyntax.Usings + FileScopedNamespaceDeclarationSyntax.Usings) in addition to top-level CompilationUnitSyntax.Usings. The repo convention is file-scoped namespaces with usings inside the namespace block, so the previous top-level-only check would re-add the using even when already in scope.

Trellis.Asp.ApiVersioning: correct comment in CreatedAtVersionedRoute that incorrectly claimed the route-value key is read from IOptions. The key is the constant DefaultRouteValueKey.

trellis-api-asp-apiversioning.md: explicit-version overload signature corrected from string apiVersion to ApiVersion explicitVersion in Patterns Index, method table, and code example. Common traps corrected: the resolver runs after the user selector and overwrites pre-existing api-version entries; manual entries are silently discarded.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.

Comment thread Trellis.Asp/src/Response/TrellisHttpResult.cs Outdated
Comment thread Trellis.Analyzers/src/CreatedAtRouteMissingApiVersionAnalyzer.cs Outdated
Comment thread Trellis.Asp.ApiVersioning/tests/CreatedAtVersionedRouteTests.cs
TrellisHttpResult: defer the RouteValueDictionary clone in ApplyRouteValueResolvers until at least one resolver returns a non-null value. When every resolver returns null (e.g., api-version resolver short-circuits for [ApiVersionNeutral] or URL-segment versioning), the original dictionary is now returned unchanged with no allocation. Cross-request-leakage protection is preserved because the clone still happens before any write.

TRLS023 analyzer: const-string keys are now resolved via SemanticModel.GetConstantValue. Patterns like [ApiVersionKey] = ... where ApiVersionKey is const string ApiVersionKey = "api-version" no longer produce a false-positive missing-api-version warning. Applies to both ImplicitElementAccessSyntax keys and pre-C#6 collection-initializer-pair keys.

TRLS023 code fix: when adding using Trellis.Asp.ApiVersioning the directive is now placed in the same scope as existing usings. If the file uses a file-scoped or block-scoped namespace with usings inside it (the repo convention), the new using is added inside the namespace block rather than above the namespace declaration.

Trellis.Asp.ApiVersioning tests: add coverage for [ApiVersionNeutral] short-circuit branch — verifies the Location header omits api-version when the controller is version-neutral.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 6 comments.

Comment thread Trellis.Asp/src/Response/HttpResponseOptionsBuilder.cs
Comment thread Trellis.Analyzers/src/DiagnosticDescriptors.cs
Comment thread Trellis.Asp.ApiVersioning/NUGET_README.md
Trellis.Asp.ApiVersioning.Tests.csproj: add explicit FrameworkReference Microsoft.AspNetCore.App so the project pattern matches Trellis.Asp.Tests and Trellis.Testing.AspNetCore.Tests. Builds were succeeding via transitive metadata from Microsoft.AspNetCore.Mvc.Testing, but the explicit reference is the repo convention for ASP.NET Core test projects.

Trellis.Asp.ApiVersioning/README.md: new repo-level README alongside NUGET_README.md, matching the dual-README pattern used by the other Trellis packages. Provides a GitHub-friendly landing page distinct from the NuGet readme.

trellis-api-asp.md: documents WithRouteValueResolver as a public Trellis.Asp API surface (the per-request route-value injection hook that Trellis.Asp.ApiVersioning builds on). Updates the existing CreatedAtRoute row to point readers at the CreatedAtVersionedRoute extensions and the TRLS023 analyzer rather than recommending manual ["api-version"] = ApiVersion.

trellis-api-analyzers.md: TRLS023 added to the Diagnostics rule table, the analyzer-id table, the descriptors table, the descriptor section (with the recognised dictionary shapes and case-insensitive matching documented), and the code-fix providers table.

Trellis.Asp.ApiVersioning.Tests: add UrlSegment_versioned_route_omits_api_version_query_from_Location to lock in the URL-segment skip branch — verifies that when the route template uses :apiVersion the resolver does not also inject a ?api-version= query parameter into the Location.

The throw branch (multi-version + no client + no DefaultApiVersion) cannot be reached via integration tests with the standard Asp.Versioning configuration: AssumeDefaultVersionWhenUnspecified=true requires DefaultApiVersion, and without it a request without ?api-version= 400s before the controller runs. Reaching the branch requires a custom IApiVersionSelector and would not exercise the same path real consumers hit; left as a documented invariant in the resolver source.
…ning

The DocFX api_reference/ files (LLM-targeted) were updated in earlier review rounds, but the human-readable articles/ tree still pointed at the old "manually add api-version" pattern and there was no TRLS023 article.

articles/analyzers/TRLS023.md: new analyzer article matching the existing TRLSnnn template — describes detection, why-it-matters, bad/good examples (with the recommended CreatedAtVersionedRoute migration as the primary fix and manual ["api-version"] = ApiVersion as the no-package alternative), code-fix behaviour, configuration, suppression, and limitations.

articles/analyzers/toc.yml: TRLS023 entry added.

articles/integration-aspnet.md: existing CreatedAtRoute warning callout and "Practical guidance" bullet now point at CreatedAtVersionedRoute and TRLS023 as the recommended path. New subsection "API-version-aware Location headers" added between Created-responses and WriteOutcome — covers the resolver order (RequestedApiVersion -> declared -> default -> throw), the [ApiVersionNeutral] / URL-segment skip, the three overloads, and the WithRouteValueResolver hook.

articles/asp-tohttpresponse.md: warning callout updated similarly.

docfx build --warningsAsErrors clean.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 2 comments.

Comment thread docs/docfx_project/api_reference/trellis-api-asp.md Outdated
Comment thread Trellis.Asp/src/Response/TrellisHttpResult.cs Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 5 comments.

Comment thread Trellis.Asp/src/Response/HttpResponseOptionsBuilder.cs
Comment thread Trellis.Asp/src/Response/TrellisHttpResult.cs Outdated
Comment thread docs/docfx_project/api_reference/trellis-api-asp.md Outdated
Comment thread docs/docfx_project/articles/integration-aspnet.md Outdated
Comment thread docs/docfx_project/articles/asp-tohttpresponse.md Outdated
Doc-only and comment-clarity fixes; no behavioural change.

trellis-api-asp.md, integration-aspnet.md: WithRouteValueResolver signature corrected from Func<HttpContext, object?> to Func<HttpContext, string?> (the actual implemented signature). Reviewer noted the doc/impl mismatch on lines 109 / 338. Keeping the implementation at string? since all current and anticipated uses (api-version, tenant id, request culture, etc.) are string-shaped; widening to object? would expand the public surface without a concrete need.

TrellisHttpResult.cs ApplyRouteValueResolvers comment: dropped the misleading "previous unconditional clone" reference. The unconditional clone existed only briefly within this same PR; describing it as a "previous" state is confusing for future readers. The comment now describes the actual protection (lazy per-request clone on first non-null resolver write).

asp-tohttpresponse.md: corrected "omits the version segment" wording to "omits the api-version query parameter" — under query-string/header versioning the failure mode is a missing query param, not a missing path segment.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 2 comments.

Comment thread Trellis.Analyzers/src/CreatedAtRouteMissingApiVersionAnalyzer.cs
Comment thread Trellis.Analyzers/src/DiagnosticDescriptors.cs
ApiVersionNeutralAttribute is declared with AttributeTargets.Class | Method, so it can be applied either to the controller class or to an individual action. The analyzer previously only checked the containing class, producing false positives on bare CreatedAtRoute calls inside an [ApiVersionNeutral] action of an otherwise-versioned controller.

Now also inspect the immediate containing method symbol via HasAttributeOnMethod. Test added: CreatedAtRoute_on_method_level_ApiVersionNeutral_action_produces_no_warning. 220/220 Trellis.Analyzers (+1).
@xavierjohn xavierjohn merged commit f2e9ce2 into main May 8, 2026
4 of 5 checks passed
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.

2 participants