Skip to content

feat: optimization backlog — security, performance, PATCH verb, AOT, and test coverage#26

Merged
rickykaare merged 23 commits into
mainfrom
feat/optimization-backlog
Mar 13, 2026
Merged

feat: optimization backlog — security, performance, PATCH verb, AOT, and test coverage#26
rickykaare merged 23 commits into
mainfrom
feat/optimization-backlog

Conversation

@rickykaare

@rickykaare rickykaare commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Comprehensive optimization pass addressing security vulnerabilities, performance bottlenecks, and API completeness gaps identified during the 1.0 audit. All changes are backward-compatible at both source and binary level.

Changes

🔒 Security & Thread-Safety

  • BearerTokenProvider — Replaces the naive cache check with a SemaphoreSlim double-check pattern: fast path reads the cache lock-free; slow path acquires the semaphore and double-checks before refreshing. Eliminates concurrent token-refresh storms under high load. Implements IDisposable. All async paths add ConfigureAwait(false).
  • HttpRequestMessageExtensions — Guards HttpRequestMessage.Options vs .Properties with #if NET5_0_OR_GREATER, avoiding the obsolete .Properties API on .NET 5+ while preserving netstandard2.0 compatibility.
  • Per-request timeout — Removes the HttpClient.Timeout mutation (shared state, not thread-safe) and replaces it with a CancellationTokenSource.CancelAfter() wrapper per request. HttpClient is shared across concurrent calls; mutating Timeout mid-flight is a data race.

⚡ Performance

  • Conditional StringContent allocationMessageRequestBuilder now only creates StringContent when a [Body] parameter is actually present. Previously, every request allocated and sent an empty JSON body even for GET/DELETE, which some APIs reject.
  • Generator predicate narrowed — Incremental generator predicate changed from (_, _) => true to node is InterfaceDeclarationSyntax. Non-interface syntax nodes no longer trigger the semantic analysis pipeline, reducing unnecessary work during incremental rebuilds.
  • HTTP/2 as library default — Generator emits client.DefaultRequestVersion = new Version(2, 0) with HttpVersionPolicy.RequestVersionOrLower in the generated AddCabazureClient<TOptions> DI setup, guarded by #if NET5_0_OR_GREATER. This prefers HTTP/2 where available and gracefully falls back to HTTP/1.1, without hardcoding the version on each HttpRequestMessage.

🆕 Features

  • [Patch] verb — New PatchAttribute with full generator support (descriptor parsing, code emission, snapshot test). PATCH is RFC 5789; users expect it for partial-update endpoints.
  • ECL008 diagnostic — Compile-time error when a [Body] parameter is declared nullable. Nullable body parameters compile fine but fail at runtime; catching this in the generator produces an actionable error message at build time.
  • Per-verb success status code defaults — All verb attributes now accept params int[] successStatusCodes. Defaults match HTTP semantics: GET→200, POST→200,201, PUT→200, PATCH→200,204, DELETE→200,204. The generator emits one .AddSuccessResponse call per code, so response helpers work correctly out of the box.

🧪 Tests

  • 49 new unit tests across three areas:
    • 24 EndpointResponse<T> tests covering status helpers, typed body deserialization, and error paths
    • 15 PagedResponse<T[]> tests covering pagination state and continuation token handling
    • 10 multi-parameter / URL-encoding tests covering combined path+query+header params and edge-case encoding
  • Generator snapshot tests updated to reflect new generated output (multi-success-code emission, PATCH endpoint, HTTP/2 setup in DI).

⚙️ CI

  • AOT compatibility check — New GitHub Actions job runs dotnet build /p:TreatWarningsAsErrors=true on the IntegrationTests project. The project already declares <IsAotCompatible>true</IsAotCompatible> but had no CI gate; trim warnings now fail the build on any platform without requiring a full dotnet publish --aot.

🔧 Refactoring

  • EndpointNamingInterfaceDeclarationSyntaxExtensions — The EndpointNaming static helper class is replaced by extension methods on InterfaceDeclarationSyntax. Duplicate naming logic that existed in two descriptors is now in one place, and call sites read more naturally as fluent expressions.

Backward Compatibility

Fully backward-compatible — no action required by consumers.

The verb attributes ([Get], [Post], [Put], [Delete], [Patch]) added a params int[] successStatusCodes overload. The original single-argument constructors ((string routeTemplate)) are preserved via constructor chaining (Array.Empty<int>()), so:

  • Binary compatibility: existing compiled assemblies load the updated DLL without MissingMethodException.
  • Source compatibility: existing source code recompiles without any changes.

All other changes are implementation-internal (private fields, generated code shape, CI configuration).

Test Results

170 tests — all passing.

  • 84 runtime unit tests (Cabazure.Client.Runtime.Tests)
  • 53 integration tests (Cabazure.Client.IntegrationTests)
  • 33 generator snapshot tests (Cabazure.Client.Tests)

rickykaare and others added 12 commits March 11, 2026 14:59
- Add SemaphoreSlim double-check pattern to BearerTokenProvider to
  prevent concurrent token refresh races under high load
- Add ConfigureAwait(false) to all awaits in authentication handlers
- Use conditional compilation for HttpRequestMessage.Properties vs
  Options to suppress obsolete warnings on .NET 5+

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Only create StringContent when body is non-empty; prevents sending
  Content-Type: application/json on GET/DELETE with no body
- Remove hardcoded HTTP/2 version; let HttpClient negotiate
- Replace HttpClient.Timeout mutation with CancellationTokenSource
  to fix thread-unsafe per-request timeout handling
- Add ConfigureAwait(false) to MessageResponseBuilder async paths

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove snapshot files for tests that no longer exist:
- CanGenerate_Obsolete_GetEndpoint (never had a test)
- CanGenerate_UsingAliases (typo/duplicate, actual test is CanGenerate_Using_Alias)

These files were blocking CI with unapproved snapshots.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…gnostic

- Add [Patch] attribute and wire up PATCH HTTP method support in
  generator and descriptor layer
- Fix ClientEndpointGenerator incremental predicate to filter to
  InterfaceDeclarationSyntax, avoiding unnecessary semantic analysis
- Extract duplicate interface/class naming logic to EndpointNaming
  helper shared by EndpointDescriptor and EndpointReferenceDescriptor
- Add ECL008 diagnostic: [Body] parameter cannot be nullable

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update assertions for intentionally removed behaviors:
- Replace Should_Use_HttpVersion_20 with Should_Not_Force_Http_Version
- Replace Should_Set_Timeout_On_HttpClient with Should_Not_Mutate_HttpClient_Timeout
  (timeout is now applied via CancellationTokenSource, not HttpClient.Timeout)
- Split Should_Use_ApplicationJson_As_MediaType into separate Accept and
  ContentType tests; add Should_Not_Set_Content_When_No_Body to cover
  the new conditional StringContent behavior

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…dling

- Add EndpointResponseTests covering IsSuccess, StatusCode, Value,
  and error response behavior
- Add PagedResponseTests covering pagination properties
- Add multi-parameter and URL encoding tests to MessageRequestBuilderTest

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
HTTP verb attributes now carry default success status codes appropriate
for each verb:
- [Get] → 200
- [Post] → 200, 201
- [Put] → 200
- [Patch] → 200, 204
- [Delete] → 200, 204

Custom codes can be overridden: [Post("/items", 200, 202)]

The generator now emits AddSuccessResponse for each status code,
enabling proper response handling for 201 Created and 204 No Content
responses without requiring manual builder configuration.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a dedicated build step for Cabazure.Client.IntegrationTests with
TreatWarningsAsErrors=true. The project already has IsAotCompatible=true
which enables ILLink trim analysis; this step ensures any regressions
in AOT compatibility are caught in CI rather than at deployment time.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace static EndpointNaming helper class with an extension method on
InterfaceDeclarationSyntax per project code style preference.

  EndpointNaming.GetNames(syntax)  →  syntax.GetEndpointNames()

Rename file to InterfaceDeclarationSyntaxExtensions.cs to match the
extension class convention. Update copilot-instructions.md to document
the preference for extension methods over static helper classes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Consolidated 7 decision files from .squad/decisions/inbox/ into .squad/decisions.md
- Deleted inbox files after merger
- Documented optimization session outcomes:
  * Thread-safe token cache (SemaphoreSlim double-check locking)
  * HTTP semantics fixes (conditional StringContent, HTTP version negotiation)
  * Thread-safe timeout via CancellationTokenSource wrapper pattern
  * PATCH verb support (RFC 5789)
  * Per-verb success status code defaults (GET=200, POST=201, DELETE=204, etc.)
  * 49 new unit tests covering response wrappers, attributes, options, edge cases
  * AOT validation integrated into CI
  * Extension method refactoring per user directive
- No breaking changes; 164 tests passing; ship-ready for 1.0 release

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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

Optimization pass across Cabazure.Client’s generator + runtime to improve HTTP semantics, thread-safety, generator performance, and expand supported REST patterns (notably PATCH and per-verb success status defaults), with corresponding test/CI updates.

Changes:

  • Add PATCH verb support and extend generator output to emit per-verb success status handling + new nullable-body diagnostic (ECL008).
  • Improve runtime correctness/safety (token refresh concurrency control, per-request timeouts without mutating shared HttpClient.Timeout, avoid allocating request content when no body).
  • Add/adjust unit + integration tests and extend CI with an AOT-related validation step.

Reviewed changes

Copilot reviewed 35 out of 35 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
test/Cabazure.Client.Tests/ClientEndpointGeneratorTests.CanGenerate_UsingAliases#TestEndpoint.g.verified.cs Removes orphan/empty snapshot artifact.
test/Cabazure.Client.Tests/ClientEndpointGeneratorTests.CanGenerate_PostEndpoint#TestEndpoint.g.verified.cs Snapshot updated for POST success codes (adds 201 Created).
test/Cabazure.Client.Tests/ClientEndpointGeneratorTests.CanGenerate_DeleteEndpoint#TestEndpoint.g.verified.cs Snapshot updated for DELETE success codes (adds 204 NoContent).
test/Cabazure.Client.Runtime.Tests/PagedResponseTests.cs New unit tests for PagedResponse<T> behavior.
test/Cabazure.Client.Runtime.Tests/EndpointResponseTests.cs New unit tests for EndpointResponse / EndpointResponse<T> helpers and conversions.
test/Cabazure.Client.Runtime.Tests/Builder/MessageRequestBuilderTest.cs Updates existing assertions + adds coverage for encoding and multi-parameter scenarios.
test/Cabazure.Client.IntegrationTests/PostEndpointTests.cs Updates timeout semantics assertion + success-code expectations.
test/Cabazure.Client.IntegrationTests/ListEndpointTests.cs Updates timeout semantics assertion (no HttpClient.Timeout mutation).
test/Cabazure.Client.IntegrationTests/GetEndpointTests.cs Updates timeout semantics assertion (no HttpClient.Timeout mutation).
test/Cabazure.Client.IntegrationTests/DeleteEndpointTests.cs Updates success-code expectations for DELETE.
src/Cabazure.Client/TypeConstants.cs Adds PatchAttribute fully-qualified name constant.
src/Cabazure.Client/Diagnostics/DiagnosticDescriptors.cs Updates ECL003 message to include PATCH; adds ECL008.
src/Cabazure.Client/Descriptors/InterfaceDeclarationSyntaxExtensions.cs Adds extension method to centralize endpoint naming logic.
src/Cabazure.Client/Descriptors/EndpointReferenceDescriptor.cs Uses centralized endpoint naming extension.
src/Cabazure.Client/Descriptors/EndpointMethodDescriptor.cs Adds success-code parsing, PATCH recognition, and nullable-body validation.
src/Cabazure.Client/Descriptors/EndpointDescriptor.cs Uses centralized endpoint naming extension.
src/Cabazure.Client/ClientEndpointGenerator.cs Narrows incremental predicate, adds PATCH HttpMethod emission, and emits multiple success responses.
src/Cabazure.Client.Runtime/PutAttribute.cs Adds SuccessStatusCodes parameter/property with defaults.
src/Cabazure.Client.Runtime/PostAttribute.cs Adds SuccessStatusCodes parameter/property with defaults.
src/Cabazure.Client.Runtime/PatchAttribute.cs Introduces new PATCH verb attribute with default success codes.
src/Cabazure.Client.Runtime/HttpRequestMessageExtensions.cs Uses HttpRequestMessage.Options on NET5+ (falls back to .Properties).
src/Cabazure.Client.Runtime/GetAttribute.cs Adds SuccessStatusCodes parameter/property with defaults.
src/Cabazure.Client.Runtime/DeleteAttribute.cs Adds SuccessStatusCodes parameter/property with defaults.
src/Cabazure.Client.Runtime/Builder/MessageResponseBuilder.cs Adds ConfigureAwait(false) to async read paths.
src/Cabazure.Client.Runtime/Builder/MessageRequestBuilder.cs Avoids creating content when no body; stops forcing HTTP/2.
src/Cabazure.Client.Runtime/Builder/HttpClientExtensions.cs Introduces per-request timeout wrapper (HttpClientWithOptions).
src/Cabazure.Client.Runtime/Authentication/BearerTokenProvider.cs Adds semaphore-based token refresh coordination + disposal.
src/Cabazure.Client.Runtime/Authentication/AzureAuthenticationHandler.cs Adds ConfigureAwait(false) on async calls.
.squad/decisions.md Documents optimization backlog decisions and executed items.
.squad/agents/ripley/history.md Records security/API analysis context and findings.
.squad/agents/hudson/history.md Records testing analysis and snapshot cleanup history.
.squad/agents/dallas/history.md Records implementation analysis and applied patterns.
.github/workflows/ci.yml Adds a build step labeled as AOT compatibility check.
.github/copilot-instructions.md Documents preference for extension methods over static helpers.

Comment thread src/Cabazure.Client/Descriptors/EndpointMethodDescriptor.cs
Comment thread src/Cabazure.Client/ClientEndpointGenerator.cs
Comment thread test/Cabazure.Client.Runtime.Tests/Builder/MessageRequestBuilderTest.cs Outdated
Comment thread .squad/decisions.md Outdated
Comment thread src/Cabazure.Client/ClientEndpointGenerator.cs Outdated
Comment thread src/Cabazure.Client.Runtime/Builder/HttpClientExtensions.cs Outdated
Comment thread src/Cabazure.Client/Descriptors/EndpointMethodDescriptor.cs
Comment thread .squad/decisions.md
Comment thread .github/workflows/ci.yml
rickykaare and others added 3 commits March 11, 2026 19:43
- Revert WithRequestOptions to return HttpClient (backward compatible)
- Add SendAsync extension on HttpClient taking IRequestOptions? and
  CancellationToken, handling per-request timeout internally
- Update generator to emit client.SendAsync(request, options, ct)
  directly instead of chaining WithRequestOptions().SendAsync()
- Remove HttpClientWithOptions class from public API
- Fix GetHttpStatusCodeExpression to return full expressions including
  cast (e.g. (HttpStatusCode)418) for non-mapped status codes
- Regenerate all affected snapshots

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add ECL008 (BodyParameterCannotBeNullable) diagnostic test
- Add value-type check for [Body] parameters: emit ECL005 when body
  param is a value type, since WithBody<T> requires T : class
- Add [Patch] generator snapshot test with approved verified snapshot
- Fix Should_URL_Encode_Special_Characters_In_Path_Parameters: use
  InlineData template param instead of hardcoded string
- Fix decisions.md: PUT default status code is 200 (not 200+204)
- Fix decisions.md: describe SendAsync extension approach (not wrapper)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rickykaare rickykaare force-pushed the feat/optimization-backlog branch from 9330497 to 27b9853 Compare March 12, 2026 07:22
rickykaare and others added 7 commits March 12, 2026 08:28
…BC-01)

Add back the original single-argument constructor to all five HTTP verb
attributes (Get, Post, Put, Delete, Patch) by chaining to the params
constructor. This preserves the original IL signature that binary
consumers depend on, preventing MissingMethodException in assemblies
compiled against the previous API surface.

Also fixes DeleteAttribute which had its params constructor and
SuccessStatusCodes assignment commented out, leaving the property unset.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…TTP/2 (BC-03)

Add a nullable Version property to ClientRequestOptions so consumers can
opt into HTTP/2 (or any version) per-request without the old hardcoded
behaviour. When set, the version is applied to the HttpRequestMessage in
ConfigureHttpRequest(); when unset, HttpClient version negotiation is
unchanged.

PagedRequestOptions inherits from ClientRequestOptions and gains the
property automatically.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Set HttpRequestMessage.Version = new Version(2, 0) unconditionally in
MessageRequestBuilder.Build(). HttpRequestMessage.Version exists in
netstandard2.0 — no platform guard required.

HttpVersionPolicy.RequestVersionOrLower (the default on .NET 5+) ensures
graceful fallback to HTTP/1.1 if the server does not support HTTP/2.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rickykaare rickykaare merged commit 212a094 into main Mar 13, 2026
2 checks passed
@rickykaare rickykaare deleted the feat/optimization-backlog branch March 13, 2026 14:51
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