feat: add generated request building support for path parameters in the URL.#2174
feat: add generated request building support for path parameters in the URL.#2174calebkiage wants to merge 21 commits into
Conversation
There was a problem hiding this comment.
Thanks for the draft. Know it's WIP - a few notes that should help when you pick it back up.
Runtime bug in GeneratedRequestRunner.BuildRequestPath:
var isPathParam = index > 0 && toReplace[index - 1] == '/';toReplace[index - 1] indexes the literal "{key}" instead of path; should be path[index - 1]. As written, /users/{user} throws IndexOutOfRangeException and /{id} leaves the placeholder unreplaced. One-char fix.
Test gap: the new tests only check generated text + CompilesWithoutErrors; nothing executes a path-param request. The one emit-load-invoke test uses [Get("/users")] (no param). An emit-load-invoke case asserting the final RequestUri (single, multiple, non-string, null/empty) would catch the bug above - most valuable addition here.
Smaller, for later:
- Empty value drops the whole segment (
/users/{user}->/users); reflection substitutes?? string.Empty->/users/. Worth aligning so inline and reflective URIs match. replacementBlockRegexadds a per-match alloc into the parse hot path stage 1 de-LINQ'd, and the camelCase static name likely trips the analyzers; a manual scan avoids both and drops the duplicate brace logic.- Confirm
params ReadOnlySpan<(string, string?)>(C# 13) compiles on the net4x TFMs - no CI ran on the branch.
Use scanning over regex matching in Parser.Request.cs. Support query parameters that are part of the path string.
# Conflicts: # src/Refit/PublicAPI/net10.0/PublicAPI.Unshipped.txt # src/Refit/PublicAPI/net11.0/PublicAPI.Unshipped.txt # src/Refit/PublicAPI/net462/PublicAPI.Unshipped.txt # src/Refit/PublicAPI/net470/PublicAPI.Unshipped.txt # src/Refit/PublicAPI/net471/PublicAPI.Unshipped.txt # src/Refit/PublicAPI/net472/PublicAPI.Unshipped.txt # src/Refit/PublicAPI/net48/PublicAPI.Unshipped.txt # src/Refit/PublicAPI/net481/PublicAPI.Unshipped.txt # src/Refit/PublicAPI/net8.0/PublicAPI.Unshipped.txt # src/Refit/PublicAPI/net9.0/PublicAPI.Unshipped.txt
|
@glennawatson, good catch on the bug. Was working on this late.
It looks like there are already tests that check for parameter substitution. They were failing and I've fixed most of them.
So if a path like How should generated code behave when |
Yep, agreed -- and I think that's the behavior we actually want. It matches what reflection already does (
This is the part I care more about -- it's a real parity gap vs reflection. Let's get the generated code calling into it: route each path value through |
…toryWithoutReflectionBuilder test
a2d898c to
11acde7
Compare
|
I have the first iteration of |
There was a problem hiding this comment.
Really nice progress, Caleb. Routing path values through UrlParameterFormatter is the right move for reflection parity, and the span-based scan in BuildRequestPath is clean. I traced single-param, multi-param, leading-placeholder, and trailing-segment cases and they all came out correct.
I left four inline suggestions below. Two are correctness blockers (the GetCustomAttributes throw and the AliasAs name check, both verified against a real build / Roslyn run) and two are small cleanups (SpecialType over name matching, and not emitting provider fields for non-path params). Each has a one-click suggestion attached.
A few things that are not inline:
Test coverage. The new tests assert generated text plus CompilesWithoutErrors, which is good for shape, but nothing executes a path-param request and checks the final RequestUri. An emit-load-invoke test (single param, multiple params, non-string, null/empty, and importantly a path param carrying a non-Query attribute) would have caught the GetCustomAttributes crash immediately. Highest-value addition before merge.
Type coverage (worth pulling in). Guid, DateTime/DateTimeOffset, decimal, double/float, and enums currently fall back to reflection. Guid and DateTime are probably the two most common route types, so accelerating them is a real win rather than an edge case. The SpecialType rewrite in the IsSimpleType suggestion makes the numeric ones (System_Decimal, System_Double, System_Single) a one-line add; Guid/DateTime/enum need a value-formatting parity check but are straightforward. Fine as a fast-follow if you would rather keep this PR tight.
Dotted and round-trip placeholders ({request.Prop}, {**param}). Reflection supports these and your code correctly falls back today, so there is no regression. The ask is just to add a test that pins that fallback so it cannot silently break later. If you decide to extend coverage to them, @TimothyMakkison models them in #2182 as a small RouteFragmentModel discriminated union (constant / standard-param / object-access / round-trip), which is a good reference.
Perf. We do care about allocations on this path, so this one is more than a nicety. BuildRequestPath re-parses the template and allocates a Dictionary plus a 1024-char pooled buffer on every call. For the common 1-2 param case, a linear scan over the tuple array (drop the dictionary) and a smaller stackalloc buffer would be allocation-free. The dictionary only pays off if the same placeholder repeats, which is rare. Bigger picture: #2182 sidesteps the runtime re-parse entirely by emitting straight-line ValueStringBuilder.Append calls at compile time. Not required for this PR, but worth seeing as a direction since perf is a focus here.
Heads-up on overlap: @TimothyMakkison is building the same feature in #2182. Rather than have you both work it in parallel, the plan is to land this PR as the base and pull in a few of his ideas as we go (all optional, your call on scope). Thanks for jumping on this, it is a useful chunk of the AOT story.
…ters' into u/calebkiage/aot-for-path-parameters
Update generator tests snapshots.
c49b870 to
cd38f95
Compare
Check whether AllowUnmatchedRouteParameters is set before throwing an exception on URL building. Use case insensitive matching for path parameters.
…ters' into u/calebkiage/aot-for-path-parameters
|
Thanks for the review. I've addressed most of the changes and have also added some tests. I will update the PR with more tests later. On the suggestion for inline path building, is it not better to share the logic than generate it on every request method? It would reduce the amount of generated code in the end. If I build the path inline, there will be a lot of duplicated code for parameter building. If perf is an issue, I can optimize it further by passing in the template and span ranges to replace instead of the parameter name. i.e. |
|
We can run some benchmarks and see either way. I'm happy whit the approach of using shared approach as long as our reflection costs go down to close as 0 in the known cases as possible. |
|
|
btw @calebkiage if it helps @TimothyMakkison is on Slack if you want to talk to him about his approach. Had a bit of a chat to him on there and he agreed taking your PR was best, since you put a bit of effort in before. But if you want you can swap ideas between each other there. https://reactiveui.net/Slack/ |
…canning at runtime. Add tests for generated parameters. Fix issue with ValueStringBuilder failing on debugger attached.
|
@glennawatson, I can't join that workspace. It says my account is deactivated. |
Ah ok, we can use comments then that's fine. |



What kind of change does this PR introduce?
Adds generated request building support for path parameters provided in the URL.
What is the new behavior?
If a URL has path parameters defined in the URL and the method signature has parameters matching the path parameter, then the source generator will create a method that uses source generation.
The method signature below will now use a generated method body.
What is the current behavior?
The old behavior used reflection for any methods that have parameters in the path.
What might this PR break?
None
Checklist
mainbranchAdditional information