Skip to content

feat!: reflection-free source-generated request building#2210

Merged
glennawatson merged 87 commits into
mainfrom
feature/generated-request-building
Jul 14, 2026
Merged

feat!: reflection-free source-generated request building#2210
glennawatson merged 87 commits into
mainfrom
feature/generated-request-building

Conversation

@glennawatson

@glennawatson glennawatson commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

What kind of change does this PR introduce?

Feature, performance, and a breaking release (V14). It completes the move to source-generated, reflection-free request building.

What is the new behavior?

Refit builds requests directly, so the common method shapes work in generated-only and Native AOT clients with no runtime reflection:

  • Query strings: auto-appended parameters, [AliasAs], [Query] (Format, CollectionFormat), scalar collections, and query-object flattening.
  • Dictionary query parameters, plus [QueryConverter] for shapes only known at runtime.
  • [QueryName] valueless flags and [Encoded] per-parameter encoding opt-out.
  • Implicit [Body] on POST/PUT/PATCH, and an all-scalar form body serialized straight-line with no descriptor array and no boxing.
  • Generic interface methods, and custom return types via IReturnTypeAdapter.
  • Path parameters format straight into the path buffer: an integer with no intermediate string and no escaping (net6+), and other span-formattable values escaped span-to-string (net9+).

Generated code targets C# 7.3 and lights up newer syntax (nullable annotations and so on) when the consumer's language version allows.

What is the current behavior?

Most of these shapes went through the reflection request builder, which is not trim- or AOT-safe. The RF006 analyzer now flags any method that still needs reflection, so a generated-only build fails at compile time instead of throwing at runtime.

Closes #2185
Closes #2190
Closes #2191
Closes #2192
Closes #2168
Closes #2169
Closes #2002
Closes #2165
Closes #1101
Closes #2200

What might this PR break?

  • Some hot-path async members return ValueTask instead of Task (ApiResponse<T>.EnsureSuccess*Async, the IApiResponse<T> extensions, DefaultApiExceptionFactory.CreateAsync, and the RefitSettings async delegates).
  • The reflection request builder is now the opt-in Refit.Reflection package. Interfaces that all generate inline do not need it; RF006 names any method that does.
  • A fully generated interface validates its route template on the first call, not in RestService.For.
  • Query objects flatten by their declared type, not the runtime type, and honor the content serializer's property names.

Full detail is in docs/breaking-changes.md.

Checklist

  • I have read the Contribute guide
  • Tests have been added or updated
  • Docs have been added or updated
  • Changes target the main branch
  • PR title follows Conventional Commits

Additional information

A lot of the path-parameter work here grew out of @TimothyMakkison's ideas. He was already exploring AOT path-parameter support in #2209 and said he planned to work on it. The ISpanFormattable fast-write for path values is his idea, and that commit credits him as co-author. Thanks, @TimothyMakkison.

Verified across net8 to net11: the full generator and runtime suites pass, the solution builds with no analyzer warnings, and generated code compiles down to the C# 7.3 baseline.

glennawatson and others added 13 commits July 13, 2026 20:57
- 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.
- 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.
…uest-building

# Conflicts:
#	src/Refit/GeneratedParameterAttributeProvider.cs
- 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>
- 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.
- 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.
- 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.
…dies

- 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.
- 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.
- 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).
- 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.
- 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.
- 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.
- 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.
@glennawatson glennawatson force-pushed the feature/generated-request-building branch from d851392 to dec46db Compare July 14, 2026 09:55
@sonarqubecloud

Copy link
Copy Markdown

@glennawatson glennawatson merged commit e511df2 into main Jul 14, 2026
14 checks passed
@glennawatson glennawatson deleted the feature/generated-request-building branch July 14, 2026 10:34
glennawatson added a commit to TimothyMakkison/refit that referenced this pull request Jul 14, 2026
Integrate the reflection-free source-generated request building (reactiveui#2210)
with the version-configuration move. Emitter no longer inlines the NET6+
version block; GeneratedRequestRunner.AddConfiguredRequestOptions applies
it instead. Generator snapshots are regenerated in the following commit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment