Skip to content

feat: add AOT path parameter support#2209

Closed
TimothyMakkison wants to merge 37 commits into
reactiveui:mainfrom
TimothyMakkison:aot_route
Closed

feat: add AOT path parameter support#2209
TimothyMakkison wants to merge 37 commits into
reactiveui:mainfrom
TimothyMakkison:aot_route

Conversation

@TimothyMakkison

Copy link
Copy Markdown
Collaborator

Follow on from #2182

Hey, here's my most recent take on adding path parameter support, don't know which one you prefer. I reckon I could modify Main to support object parameters and **param types, and possibly query params if needed, this may have some issues, I can elaborate as needed. I haven't bothered to rebase this PR onto main yet as I'm not sure if it's still wanted or what issues need to be resolved.

I'd be curious to see what the performance difference is between this and the current approach, my benchmarks suggest this uses a little less memory but might be slower due to dictionary lookups, plus I don't trust my laptop to do microbenchmarks during a heatwave. The advantage of adding each parameter individually is that we can prevent boxing of value types and potentially use ISpanFormattable when DefaultUrlFormatter is used for allocation free writes, ie an integer parameter with 2 digits will likely save 24+32 bytes with this approach, not a massive saving.

ValueStringBuilder

How to handle the use of ValueStringBuilder

Emit

Emit ValueStringBuilder as a inner class of Generated, would also have to emit AddPathParameter and AddPathObjectProperty as they need access to it. This would make testing function that rely on the emitted ValueStringBuilder very tricky.

Expose

Make ValueStringBuilder public, this means that people who use Refit will have access to it. Not a great idea IMO

Inner class

Make ValueStringBuilder an inner class of Generated, this way all emitted code can still reference it. We would have to make GeneratedRequestRunner an inner class of Generated so it can see ValueStringBuilder, this way users can't access ValueStringBuilder but we can still write tests.

Reflection

My approach to reflection is continued from #1965 where I've tried to make ICustomAttributeProvider identical to the runtime generated version. As I imagine some people internally casted the types back into ParameterInfo
If my understanding of AOT is correct, my current approach should safely access all reflection data, even if generics are used.
I did experiment with a version which didn't accept generic arrays and just used the generic argument count, but wouldn't close the method to correct generic type.

I could use #2174 approach, as you seem to be okay with this. This will have an edge case where it will break in the rare cases where a parameter has a generic attribute, however this can be fixed.

// This will break as the generated code will be outside the scope of the method and not know what T is.
Task Get<T>(MyAttribute<T> string input);

Failing tests

How to handle the failing query tests, most of the tests are failing because they expect query values alongside object path parameters.

  • I could disable the tests and then re add them when query support is added
  • Alternatively I could disable object property parameter support and add it back later
  • Write logic to determine when a query parameter may be expected and fall back to reflection driven code.

Generated code:

private static readonly global::System.Type[] ______typeParameters = new global::System.Type[] { typeof(int) };
/// <inheritdoc />
public global::System.Threading.Tasks.Task<global::System.Net.Http.HttpResponseMessage> DynamicRouteAsync(int @id)
{
    var refitSettings = _settings;
    global::System.Span<char> span = stackalloc char[256];
    var valueStringBuilder = new global::Refit.ValueStringBuilder(span);
    valueStringBuilder.Append("/users/");
    global::Refit.GeneratedRequestRunner.AddPathParameter<global::Refit.Benchmarks.IPerformanceService, int>(ref valueStringBuilder, @id, refitSettings, nameof(@id), ______typeParameters);
    var refitRequest = new global::System.Net.Http.HttpRequestMessage(global::System.Net.Http.HttpMethod.Get, global::Refit.GeneratedRequestRunner.BuildRelativeUri(this.Client, valueStringBuilder.ToString(), refitSettings.UrlResolution));
    #if NET6_0_OR_GREATER
    refitRequest.Version = refitSettings.Version;
    refitRequest.VersionPolicy = refitSettings.VersionPolicy;
    #endif
    global::Refit.GeneratedRequestRunner.AddConfiguredRequestOptions(refitRequest, refitSettings, typeof(global::Refit.Benchmarks.IPerformanceService));
    return global::Refit.GeneratedRequestRunner.SendAsync<global::System.Net.Http.HttpResponseMessage, global::System.Net.Http.HttpResponseMessage>(
        this.Client,
        refitRequest,
        refitSettings,
        false,
        false,
        false,
        global::System.Threading.CancellationToken.None);
}

@TimothyMakkison TimothyMakkison marked this pull request as draft July 11, 2026 18:57
glennawatson added a commit that referenced this pull request Jul 13, 2026
- Resolved in favour of the request-building rework: the branch predates
  it and its path-parameter code is superseded by the inline rework.
- Dropped PathFragmentModel (the rework has its own path models).
- Kept the updated analyzer rule configuration.

Co-Authored-By: Timothy Makkison <timmakkison@gmail.com>
glennawatson added a commit that referenced this pull request Jul 14, 2026
* feat: added route generation, currently not emitting, don't know why

* fix: can run tests without error messages

* feat: add exceptions and unmatched guard logic

* feat: move `subProperties` into `RefitModel`

* feat: partially move route logic into `Parser`

* chore: refactor and rename `BuildObjectParamValidationDict`

* feat: enable inline generation for route parameters

* feat: run tests and accept changes

* feat: use route fragment based generation in `Emitter`

* fix: correctly emit object property fragment

* chore: remove unused code

* chore: remove unneeded model

* chore: reorder and try to fix some errors

* chore: refactor add summaries

* chore: add summaries for `GeneratedRequestRunner`

* fix: use `ConcurrentDictionary` to fix concurrency issue

* fix: constant path test

* fix: update snapshot tests

* feat: add type parameter array and add @ to generated parameter names

* chchore: remove unused property and summary

* feat: pass `genericCount`

* feat: refactor `AddStandardParameter`

* feat: refactor `AppendObjectPropertyFragment` to `AddRouteObjectProperty`

* feat: add `TryParseRouteParameter`

* fix: remove local `stackalloc` to support C# 7.3

* feat: add `BuildInlineTypeParameterField`

* chore: rename `AddPathObjectProperty` and `AddPathParameter`

* chore: make some analysers happy

* feat: add `ParameterInfoKey` and `ParameterInfoCacheKeyComparer`

* feat: change generated code for `AddRouteParameter` and `AddPathObjectProperty`

* chore: code cleanup, add comments, minor refactor

* feat: add skeleton for framework `GetMethod`

* chore: remove debug code

* feat: add `genericTypeArgument` emitter

* chore: add generic argument, refactor and add documentation

* chore: add comments, remove replaces references to `Route` to `Path`

* feat: fix missing argument and invert generic emitter

* feat!: reflection-free source-generated request building

Completes the move to generated, trim- and Native-AOT-clean request
building, keeping the reflection path as an opt-in package.

- Generate query strings, query-object flattening, dictionary query
  params, and implicit [Body] inline, so the common method shapes work
  in generated-only and Native AOT clients (#2185, #2190, #2191, #2192).
- Add [QueryName] valueless flags, [Encoded] encoding opt-out, and
  [QueryConverter] for runtime-only shapes (#2168, #2169, #2002).
- Add pluggable return-type adapters via IReturnTypeAdapter (#2165).
- Generate generic interface methods inline; support extern aliases (#1101).
- Split the reflection request builder into the opt-in Refit.Reflection
  package; add RF006/RF007 analyzers for methods that still need it.
- Stop generated fallback methods leaking IL2026/IL3050 (#2200).
- Unroll all-scalar form bodies straight-line; keep generated code at the
  C# 7.3 baseline and emit modern syntax when the language version allows.
- Reduce hot-path allocations and move some hot async members to ValueTask.

BREAKING CHANGE: some hot-path async members return ValueTask instead of
Task; the reflection request builder is now the opt-in Refit.Reflection
package; fully generated interfaces validate on first call, not in
RestService.For; query objects flatten by declared type and honor the
content serializer property names. See docs/breaking-changes.md.

* perf(generator): span-formattable path parameter fast-write

- Format an integer path parameter straight into the path buffer with no
  intermediate string and no escaping (net6+); its invariant rendering is
  URL-safe, so nothing is escaped.
- Format any other ISpanFormattable path value into a stack buffer and
  escape span-to-string (net9+), skipping the ToString allocation.
- Detect ISpanFormattable and the span Uri.EscapeDataString overload once
  per compilation and thread the result, so the generator emits the best
  path for the consumer target and nothing extra for older frameworks.

Brings over the ISpanFormattable idea from #2209.

Co-authored-by: Timothy Makkison <timmakkison@gmail.com>

* refactor(generator): make InterfaceGenerationContext a reference type

- Change the transient InterfaceGenerationContext from a readonly record
  struct to a sealed record, so the compilation and symbols it carries are
  passed by reference, not copied, and it reads as pass-scoped scaffolding.
  It is never a pipeline value; the emitted models stay the equatable,
  cache-safe types, so nothing symbol-bound crosses the incremental cache.
- Use a concrete INamedTypeSymbol[] for the discovered return-type adapters
  (context and analyzer state) instead of IReadOnlyList.
- Drop the now-redundant in modifier on the context parameters.

* perf(generator): cut generator allocations ~17-18%

- Drop the discarded RequestParameterModel builds on the Try* parameter
  false paths; the caller only reads the out value on success.
- Replace per-parameter ToDisplayString comparisons with structural
  matches (cancellation token, Refit attributes, generic enumerable).
- Cache extern-alias lookups per pass instead of re-resolving a metadata
  reference for every type node in QualifyType.
- Walk each interface's inherited members once (methods + properties),
  and resolve IFormattable/ISpanFormattable in a single interface pass.
- Back emitter accumulation with a pooled-buffer PooledStringBuilder
  (ArrayPool) in place of StringBuilder across the emitter.
- Append query statements and the request prologue straight into the
  builder rather than building intermediate interpolated strings.
- Cache indentation strings and add no-escape fast paths for C#/XML
  literal escaping; fuse the parameter-info emission into one pass.

Measured on a 60-interface corpus: per-keystroke and cold-build
allocations both down ~17-18%. Generated output is byte-identical.

* feat(generator): close reflection-fallback gaps and expand coverage

- Flatten nullable nested value-type (Nullable<T>) query-object properties
  inline, unwrapping to the underlying struct and accessing via .Value.
- Generate an inline Authorization header for [Authorize] parameters using
  the compile-time scheme; multiple [Authorize] falls back so it still throws.
- Flatten dictionary properties inside a query object inline, expanding
  entries under the property key.
- Generate non-[Encoded] round-trip {**param} catch-all paths of any type
  inline via GeneratedRequestRunner.RoundTripEscapePath.
- Add runtime parity tests (generated output matches the reflection builder)
  and generator coverage tests across the touched files.

* feat(generator): bind path objects and flatten their residual query

- Split a dotted {param.Prop} object between the path and the query: matched
  properties fill the path placeholders and every property left unbound
  flattens into the query string inline, mirroring how the reflection builder
  splits a bound object instead of falling back. An unsupported residual
  property shape still falls the whole parameter back.
- Sort path replacements by template position before emitting. The runtime
  BuildRequestPath overloads slice between consecutive placeholders, so an
  object binding filling an earlier placeholder than a later parameter threw
  ArgumentOutOfRange in the parameter-precedence case.
- Cover the feature with a live reflection-parity test (generated URI equals
  the reflection builder) and a PathObjectBindingGenerationTests file spanning
  inline, residual-inline and unsupported-residual fallback.
- Upgrade StyleSharp/PerformanceSharp analyzers to 3.19.0 and satisfy the new
  rules without suppressions or editorconfig changes: name repeated test
  literals, seal the top-level Program types, return a cached read-only view
  from StubHttp.Requests, and scope the SST1472 case to the tested constructor.

* test: cover PR-touched files to 100% and narrow coverage exclusions

- Add tests across generator, reflection and runtime paths so every
  production file changed in this PR reaches full line and branch coverage:
  alias/adapter parsing, query-object flattening, path-object binding,
  the reflection request/adapter builders, the query-string builder, the
  System.Text.Json query converter, and request-execution helpers.
- Restructure genuinely-unreachable defensive code so it no longer needs a
  method-level [ExcludeFromCodeCoverage] that hid real, tested logic: drop
  the path-parameter Locations-null guard, unconditionally treat a second
  IEnumerable<T> as ambiguous, simplify the first-query-attribute lookup,
  extract the ExceptionDispatchInfo rethrow into a tiny helper, return the
  string body after its await-using, and drop the adapter arity guard whose
  caller already guarantees equal counts.
- The only remaining exclusions are a debugger-only display string and the
  unreachable trailing return of the rethrow helper.

* build: disable S4027 to match SonarCloud ruleset

- Set S4027 severity to none so the local build agrees with SonarCloud,
  where it is disabled.
- Remove the now-redundant S4027 suppressions on the API exception classes
  and the DerivedApiException test fixture (SST1462).

* build: apply uniform analyzer standards to test code

- Reorganize the analyzer config so production and test code share one
  standard: keep only genuine test-specific LINQ relaxations in
  src/tests/.editorconfig and drop the blanket SST1432 test exemption.
- Correct S4027's rationale to a CA1032 duplicate rather than a SonarCloud match.
- Suppress SST1432 on the Http.Client naming fixture, which is passed to
  UniqueName.ForType<T> as a type argument and so cannot be static.

* test: drop the fragile synthetic-net10 span-escape generator test

- Remove GeneratesInlinePathAgainstNet10SpanEscapeReferenceSet and its
  TryCreateNet10Compilation helper. Hand-building a compilation from the
  net10 reference pack plus the host-target Refit.dll mixed corelibs, which
  on a net11 host bound the [Get] path argument to an error constant and
  dropped the path entirely — a deterministic failure, not a product bug
  (normal generation is correct on net11).
- The span-escape probe's positive branch is exercised naturally on the
  net10.0/net11.0 target test runs, where the host references genuinely
  expose Uri.EscapeDataString(ReadOnlySpan<char>).

* build: disable analyzer rules that duplicate their SST equivalents

- Turn off ~80 CA and Sonar (S) diagnostics that are already covered by a
  canonical StyleSharp (SST) rule, so each concern is enforced by exactly one
  analyzer rather than reported twice.

* perf(generator): span-format query values without an intermediate string

Render default-formatted scalar and collection query values straight into
the builder as ISpanFormattable, dropping the per-value throwaway string on
the source-generated request hot path. Query URIs stay byte-identical.

- Add GeneratedQueryStringBuilder.AddFormatted / AddCollectionValueFormatted
  (net6+): TryFormat into a stack buffer, append URL-unreserved integers
  verbatim and span-escape other values (net9+), matching Add/AppendPair.
- Emit the fast call on the default-formatting branch for span-formattable
  values, reusing the parser's IsUrlSafeSpanFormattable /
  IsSpanFormattableEscapable tiers; customized-formatter, TreatAsString,
  enum and string paths are unchanged.
- Extract the enum/converter formatter helpers into a new partial file to
  keep Emitter.Inline.Query.cs within the file-length limit.
- Cover int/long scalars, a formatted double, int Csv/Multi collections and
  a null nullable int in the reflection-parity live suite.

* feat(generator): source-generated multipart request building

- Build [Multipart] method content inline instead of falling back to
  reflection, matching the reflection builder's AddMultipartItem dispatch
  and part field-name/file-name selection for statically-resolvable types
- Support StreamPart/ByteArrayPart/FileInfoPart (and MultipartItem
  subclasses), Stream, string, FileInfo, byte[], HttpContent, and
  date/time and Guid parts, plus reference-typed enumerables (one part per
  element); reproduce custom boundaries and the null-parameter skip
- Keep object/serialized parts and [Multipart]+[Body] on the reflection
  builder (the whole method falls back), so RF006/RF007 stay in lockstep
- Add MultipartPartKind/MultipartPartModel plus Parser.Request.Multipart
  and Emitter.Inline.Multipart partials; register the shared files in both
  analyzer csprojs
- Cover with a reflection-parity live suite and a compliance-project
  scenario; flip the multipart-falls-back tests to assert inline generation

* build: fix the SST reference in the CA1044 dedup comment

- CA1044 (write-only properties) is covered by SST1421, not SST2307.

* perf(generator): drop per-call query-builder scan and empty attribute dicts

- Emit the compile-time query-presence into a new GeneratedQueryStringBuilder
  ctor overload so the built path is not rescanned for '?' on every call
  (gated off when a pre-encoded path segment could inject one at runtime).
- Emit a shared GeneratedParameterAttributeProvider.Empty singleton for
  parameters that declare no attributes, instead of a fresh empty dictionary
  and Lazy per parameter.

* fix(tests): update Verify.TUnit version and remove carriage return handling

* feat(generator): widen inline request coverage and unroll path builder

- Replace the fixed-arity and params BuildRequestPath overloads with a single
  ReadOnlySpan overload; the generator emits a [...] collection expression on
  C# 12 (stack-allocated on net8.0+, zero heap for any arity) and an inferred
  array on older consumers, both binding through the array-to-span conversion.
- Bind sealed and value ToString-only path parameters inline through the same
  UrlParameterFormatter.Format call the reflection builder makes; open, object,
  and interface types stay on reflection to avoid runtime-type divergence.
- Source-generate [QueryUriFormat] methods, re-encoding the whole path and query
  with the attribute's UriFormat via a helper that mirrors the reflection
  builder's Uri.GetComponents(PathAndQuery, format) pass.
- Render a [Query(Format)] on a complex or collection query-object property as a
  single whole-value pair inline instead of falling back.
- Flatten a dictionary of a sealed complex value type under each entry key
  (key.Property=value), matching the reflection builder's nested BuildQueryMap.
- Match return-type adapters whose wrapper reorders or concretely mixes the
  adapter's type parameters, mirrored in the reflection resolver so both agree.
- Inline a custom HTTP method attribute whose Method getter is a literal
  new HttpMethod("VERB"); other overrides keep using the reflection builder.

Every closed gap is pinned by a generated-vs-reflection URI parity test.

* test(generator): pin the HTTP QUERY verb scenario

- Add a custom QUERY verb attribute and body-carrying methods to the live query
  harness, covering the draft-standard HTTP QUERY method.
- A generated QUERY request uses the custom verb, carries an explicit [Body], and
  - since the verb is not yet body-capable - flattens an un-attributed complex
  parameter into the query, all at parity with the reflection request builder.
- Compare the HTTP method in AssertParityAsync so every parity test now pins the
  verb, not just the URI.

* perf(generator): span-format nullable value-type query parameters

- A nullable value type (int?/Guid?/DateTime?) query parameter now writes its
  unwrapped .Value through the span-formattable fast path after the existing null
  guard, instead of materializing an intermediate string via FormatInvariant.
- ComputeSpanFormattableTiers no longer excludes nullables; the path and
  collection-element fast paths opt out separately, keeping their existing
  string-formatting path (a nullable path value needs a null branch the fast
  overload does not model).
- Correctness is covered by the existing Paged(int? page) live parity test;
  a generation test pins that the fast write fires.

* feat(generator): source-generate serialized multipart parts

- A multipart part whose declared type is a bool, enum, or sealed DTO now
  serializes inline through settings.ContentSerializer.ToHttpContent(value),
  matching the reflection builder's AddSerializedMultipartItem serializer
  fallback; the declared type drives ToHttpContent<T>, so a sealed/value part's
  serialized form is byte-identical.
- An open, interface, or object-typed part keeps falling back (runtime type
  decides the serialized shape); a non-sealed DTO part is a declared-type
  divergence handled separately.
- Live parity test serializes a bool and a sealed DTO part byte-for-byte.

* feat(generator): inline concrete-class path parameters

- A path parameter of any concrete class or struct (not only sealed/value) now
  binds inline. The reflection builder renders a route value with a virtual
  ToString call that dispatches to the runtime type; the generated ToString does
  the same, so a runtime subtype renders identically - the only divergence is the
  vanishing case of a subtype whose IFormattable.ToString(null, invariant)
  differs from its parameterless ToString().
- object, interface, and open generic path parameters keep falling back: they
  have no usable declared shape and must be inspected at runtime.

* feat(generator): inline collection and array path parameters

- A collection or array path parameter (List<int>, byte[], int[]) now binds
  inline: the reflection builder renders it with the URL parameter formatter,
  which for a non-IFormattable value is value.ToString() (the System.Collections
  text), and the generated ToString reproduces that identically.

* feat(generator): inline concrete (non-sealed) dict values and multipart parts

- A dictionary value or multipart part of any concrete class or struct (not only
  sealed/value) now flattens/serializes inline by its declared type, the same
  accepted declared-type divergence a non-sealed query object already carries
  (matching the System.Text.Json source generator).
- A value that is not a runtime subtype renders identically to the reflection
  builder; a polymorphic subtype renders by its declared type. object, interface,
  and open generic types keep falling back - they have no usable declared shape.
- Live parity tests flatten a non-sealed dict value and serialize a non-sealed
  multipart part, both byte-for-byte vs reflection.

* chore: untrack agent worktree and ignore .claude

- Remove the .claude/worktrees gitlink accidentally staged into the previous
  commit; the directory is a transient local agent worktree, not project content.
- Add .claude/ to .gitignore.

* feat(generator): inline IObservable return shape as cold observable

- Generate IObservable<T> methods inline through a new
  GeneratedRequestRunner.SendObservable helper instead of the reflection
  request builder, closing the last non-Task return-shape fallback where
  the shape is statically known.
- The helper returns a ReactiveUI.Primitives cold FromAsyncSignal that
  rebuilds and re-sends the request per subscription, linking the method
  and subscription cancellation tokens.
- Wrap request construction in a per-subscription local function so a
  second subscription never reuses a disposed request.
- Mirror the async send's isApiResponse/dispose/buffer flags, so
  IObservable<ApiResponse<T>> and IObservable<IApiResponse<T>> reach
  parity with the async path.
- Move the return-shape inline/fallback switch tests into their own
  file and add a live cold-observable parity test.

* feat(generator): inline no-leading-slash relative paths

- Stop forcing a reflection fallback for method paths without a leading
  slash; they now build inline like any other path.
- Under RFC 3986 resolution the HttpClient merges the base address with
  the relative path; under legacy resolution the generated request
  rejects it with the same ArgumentException the reflection builder
  raises, so behaviour matches in both modes.
- Add a live test covering inline generation, RFC 3986 parity, and the
  legacy rejection.

* feat: inline and reflect multi-level dotted path params

- Bind nested {a.b.c} route placeholders through a property chain in both
  the source-generated and reflection request builders, where only a
  single {a.b} level bound before.
- Walk the chain with null-conditional access so a null intermediate
  renders an empty segment instead of throwing, matching the reflection
  builder; the top-level property is marked consumed so residual object
  properties still flatten into the query correctly.
- Extend the public RestMethodParameterProperty with the navigation chain
  (single-level bindings remain a one-element chain).

* fix: restore reflection parity for inlined multipart and slashless routes

- Wrap a generated multipart part's serialization failure in the same
  descriptive ArgumentException the reflection request builder raises,
  via a new GeneratedRequestRunner.SerializeMultipartPart helper.
- Update the leading-slash-less route tests: generated request building
  validates the slash when the request is built, so the throw now
  surfaces on the first call, not from RestService.For. Document the
  timing in the README.
- Switch the generated-factory test interface off the now-inline
  IObservable shape onto a dynamic query-map parameter so it still
  exercises the reflection factory registration path.

* perf(generator): cache custom-verb HttpMethod in a static field

- A custom HTTP verb previously allocated a new HttpMethod on every call
  through inline request building. Emit it once into a static field and
  reference it, matching the reflection builder, which reads the verb
  from the attribute once per method.
- A known verb still resolves to the framework-cached singleton, so only
  custom-verb methods gain the field.

* perf(generator): pre-escape constant query keys at generation time

- A scalar query parameter's key is a compile-time constant, so escape it
  once during generation and append it verbatim at request time instead
  of escaping it on every call.
- Add GeneratedQueryStringBuilder.AddPreEscapedKey / AddFormattedPreEscapedKey
  overloads that take an already-escaped key; the escaping-key overloads
  remain for runtime keys (dictionary entries, [QueryName] values).
- Uri.EscapeDataString follows RFC 3986 consistently across target
  frameworks, so the generation-time escape matches the reflection
  builder's runtime escape (parity tests confirm).

* perf: percent-encode span-formatted values in place, extend to net8

- Escape a span-formatted URL value directly into the builder with an
  in-place RFC 3986 encoder (StringHelpers.AppendUriDataEscaped) shared
  by path and query building, instead of allocating an escaped string
  via Uri.EscapeDataString(span).
- Enable the span-escape tier wherever ISpanFormattable exists (net8+),
  not just where Uri.EscapeDataString(ReadOnlySpan) does (net9+), so net8
  formats-and-escapes escapable values without the intermediate string.
- The formatted spans are invariant ASCII; a non-ASCII character defers
  to Uri.EscapeDataString for exact UTF-8 parity.

* fix: use the net9 span overload of Uri.EscapeDataString in the escaper fallback

* test(benchmarks): cover the verb-cache and span-escape query paths

- Add TimestampQuery / TimestampPath rows (a DateTimeOffset whose
  invariant form needs escaping) so the span-escape encoder is measured
  against the reflection builder for both query and path values.
- Add a custom-verb row so the cached HttpMethod path is exercised.

* test(benchmarks): use explicit new HttpMethod so the custom verb inlines

* test(analyzers): treat IObservable as inline-supported, not a fallback

- IObservable methods now build inline, so RF006 no longer fires for
  them; move the shape from the reflection-fallback analyzer cases to the
  inline-supported cases, matching the generator's fallback contract.

* refactor: extract attribute flattening to drop the SST1443 suppression

- Move the nested attribute-array flatten out of the AllAttributesCache
  getter into a static FlattenAttributes helper that copies each array
  with Array.Copy, removing the nested loop that needed the suppression.

* build: move hot-path LINQ analyzer rules to PerformanceSharp

- Replace the StyleSharp SST2229/2230/2233 and stylesharp.* hot-path LINQ
  configuration with the PerformanceSharp PSH1100/1101/1102 and
  performancesharp.* equivalents in the root and tests .editorconfig.
- Point the S103/S3880 duplicate-rule notes at their new owners and keep
  the 200-char line limit the repo enforces.

* refactor: update severity levels for various diagnostics in .editorconfig

* test: cover source-generated request building to 100%

- Add runtime, generator, and reflection tests bringing every file touched by the
  reflection-free request-building feature to 100% line coverage.
- Remove dead code surfaced by the coverage push: inline FindHttpMethodProperty so
  its unreachable return null folds into the covered fallback, drop the dead
  PropertyDeclarationSyntax getter arm, and delete the redundant adapter bounds
  guard (the source-generated binder indexes the same slots without one).
- Bump StyleSharp/PerformanceSharp to 3.21.2, SonarAnalyzer.CSharp to
  10.29.0.143774, TUnit to 1.59.0, Verify.TUnit to 31.24.1, and Serilog to 4.4.0;
  the analyzer update clears the SonarCloud SST2015/SST1462 build errors.

* feat: Add support for parsing query objects and generating request bodies

- Implemented `Parser.Request.Query.Objects` to flatten complex query values into query pairs for Refit stubs.
- Introduced `GeneratedRequestRunner.BodyContent` for serializing request bodies in various formats (JSON, JSON Lines, URL-encoded).
- Added `GeneratedRequestRunner.Sending` to handle sending requests and processing responses, including support for observables and streaming responses.

* fix: update SharpAnalyzersVersion to 3.21.5

* refactor: bring remaining projects into analyzer compliance

- Split oversized files into behavior-named partial classes and extract long
  methods (SST1522/SST1523) across Refit.Reflection, Refit.Testing, and the test
  and benchmark projects.
- Fix mechanical findings: buried ++/-- (SST2015), numeric-suffix casing
  (SST2244), redundant interface public (SST1491), replaceable collections
  (SST2305), per-call serializer options (PSH1416), and assorted others.
- Suppress the few genuinely-unfixable findings with justifications: CS0501-
  mandated abstract on an explicit-interface re-abstraction, options capturing a
  per-call resolver, the interface-dispatch and sync-stream test surfaces, and the
  BCL-mirror [Flags] enum polyfill.
- Behavior unchanged; the full solution builds analyzer-clean and all tests pass.

* refactor: update SharpAnalyzersVersion to 3.21.13 and apply static lambdas in various files

* test: cover reachable branches across the touched surface

- Add branch-covering tests across the generator, runtime, reflection, testing,
  analyzer and code-fix files, closing 74 of the 125 partial/uncovered branches
  flagged on the touched files.
- Drive the real generated/parsed and reflection request-building surface for
  each untested condition outcome (attribute combos, param/return shapes, dotted
  paths, query objects, dictionaries, multipart parts, custom verbs, adapters,
  RFC-3986 escaping, cancellation linking, and error/fallback paths).
- The remaining 51 branches are provably unreachable (compiler-generated string
  jump tables, dead null-conditionals on never-null operands, exhaustive switch
  defaults, async-iterator dispose-mode IL edges, contract-guaranteed non-null).

* refactor: close the uncoverable branch residual to reach 100% per file

- Remove provably-dead branches surfaced by the coverage push: drop `?.`/`??`
  guards on operands the surrounding invariant proves non-null (Roslyn symbol
  namespaces/containers, JsonSerializerOptions.GetTypeInfo, a String JSON
  element's GetString, a CanRead property's getter, the resolved adapter type),
  and dead ternary/switch arms that cannot be reached.
- Isolate the structurally-uncoverable branches into tiny private helpers marked
  [ExcludeFromCodeCoverage] (compiler string-switch jump tables, async-iterator
  dispose-mode IL edges, BCL-nullable defensive guards, analyzer-blocked arms),
  keeping the excluded surface minimal and the surrounding coverable logic intact.
- Consolidate the duplicated known-verb string switch into one shared
  Parser.MapKnownHttpVerb, and the linked-cancellation-token pattern into one
  GeneratedRequestRunner.ResolveRequestCancellationToken.
- Behavior unchanged: generator snapshots byte-identical, all tests pass, and
  every touched file now reports 100% branch coverage.

* build: adopt SST2307 in place of S4018 for uninferable generics

- Bump StyleSharp/PerformanceSharp to 3.22.0, which adds SST2307 (a
  generic method whose type parameter cannot be inferred from its
  parameters, so every caller must name it).
- Disable Sonar S4018 as a duplicate of SST2307 and enable SST2307 as
  error, matching the repo's swap convention for Sonar/Sharp overlaps.
- Swap every product and test [SuppressMessage] from S4018 to SST2307.

* build: clear suppressions targeting analyzer-disabled rules

- Bump StyleSharp/PerformanceSharp to 3.23.1.
- Swap S1541 -> SST1442 and S2360 -> SST2309, the Sonar rules now
  disabled as duplicates of the Sharp equivalents that fire on the
  same code.
- Remove suppressions of rules set to none with no firing equivalent
  (CA1031, CA1040, CA1044, CA1835, CA2208, IDE0079, IDE1006, S100,
  S1118, S2326); delete the now-empty GlobalSuppressions.cs and the
  usings that orphaned.
- Swap CA2227 -> SST2305 on the settable-collection test model where
  the Sharp rule does fire once the CA suppression is gone.
- Clears the SonarScanner build, where disabled Sonar rules made those
  suppressions dead and tripped SST1462.

* build: finish Sonar->Sharp rule migration on analyzers 3.25.2

- Bump StyleSharp/PerformanceSharp to 3.25.2.
- Swap the remaining Sonar S-rules (S1133, S2339, S2342, S2930, S3903,
  S4022, S4070, S6566, S6966) to their Sharp/CA/PSH equivalents, so the
  SonarScanner build no longer trips SST1462 on suppressions of the
  S-rules it strips.
- Repair escaped-quote corruption in the S2930/S6566 swaps, whose rule
  titles contain \" and broke the substitution.
- Drop now-dead suppressions (S4070, RCS1157, CA1849) and an orphaned using.
- Disable SST2314: ObsoleteAttribute.DiagnosticId is unavailable on the
  net4x targets.
- Relax SST2016 and SST2410 in the test tree for DateTime test models and
  short-lived test disposables.

---------

Co-authored-by: Timothy Makkison <timmakkison@gmail.com>
@glennawatson

glennawatson commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Hey Tim - as discussed on Slack, closing this one out.

The couple of ideas we grabbed from here - the ISpanFormattable allocation-free writes for value-type path params, and the ValueStringBuilder-based path assembly - made it into the reflection-free request-building work that recently merged in #2210, so thanks again for those. Really appreciated the exploration here.

Cheers!

@TimothyMakkison

Copy link
Copy Markdown
Collaborator Author

Thanks, excited to check out the query support 👍

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