You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
perf: reduce allocations & reflection in generated operation code (#455)
* perf: reduce allocations & reflection in generated operation code
Compile-time (OperationCompiler):
- Hoist toParam / toQueryParams / TaskExtensions.cast / AsyncExtensions.cast
MethodInfo resolution to the OperationCompiler instance (resolved once
per provider instead of per generated operation).
- Hoist string-list union-case reflection in Provider.OpenApiClient.
- Build a paramName -> IOpenApiParameter map once per operation and use
it for O(1) lookups in invokeCode (was Seq.tryFind per ShapeVar).
- Build the fixed Content-Type/Accept headers list at provider-generation
time instead of emitting a quotation that re-evaluates per call.
- Make response quotations (object / stream / string / unit) lazy so only
the branch actually used for the operation's return type is built.
Generated code (runtime):
- Emit RuntimeHelpers.createHttpRequestFromQueryLists instead of chained
List.append / List.concat for query parameter assembly.
- Emit RuntimeHelpers.fillHeadersAndCookies instead of an inline
Seq.filter / Seq.map / String.concat cookie-building quotation.
- For typed JSON responses, emit a direct generic TaskExtensions.cast<'T> /
AsyncExtensions.cast<'T> call via ProvidedTypeBuilder.MakeGenericMethod,
avoiding MethodInfo.Invoke on every API call.
- Avoid the previous shared-quotation Var hazard that caused FS3033
'An item with the same key has already been added' for operations with
multiple path/query parameters.
Runtime helpers (RuntimeHelpers):
- Add createHttpRequestFromQueryLists: flattens seq<#seq<string*string>>
into a ResizeArray, filtering nulls, and delegates to createHttpRequest.
- Add fillHeadersAndCookies: calls fillHeaders, then builds the Cookie
header imperatively with StringBuilder using the standards-compliant
'; ' separator and TryAddWithoutValidation.
Tests:
- Add regression tests in Schema.OperationCompilationTests that assert
generated invokeCode does not reuse the same Quotations.Var instance
across path/query parameters.
Validated:
- dotnet fantomas --check src/ tests/
- Unit tests: 479 total, 0 failed, 1 skipped
- Provider integration tests (Swashbuckle server): 127 total, 0 failed
- Stripe spec3.json (414 paths, 1422 schemas, 587 operations) compiles
successfully; generated code contains 0 reflective casts, 0 List.append
and 0 List.concat calls.
* test: cover createHttpRequestFromQueryLists and fillHeadersAndCookies
Address Copilot review feedback on PR #455 by adding focused
RuntimeHelpersTests for the two new helpers:
createHttpRequestFromQueryLists:
- flattens multiple query lists into the request URI
- strips leading slash when all lists are empty
- strips leading slash when all inner lists are empty
- skips null-valued pairs across multiple lists
- produces no query string when all values are null
- preserves the HTTP method
fillHeadersAndCookies:
- emits Cookie header with canonical '; ' separator
- single cookie has no separator
- skips null cookie values
- omits Cookie header when all values are null
- omits Cookie header when cookie list is empty
- still adds normal headers via fillHeaders
- still skips null-value headers
@@ -258,6 +302,12 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler,
258
302
|> Seq.toArray
259
303
|> Array.unzip
260
304
305
+
letfixedHeaders=
306
+
[ifnot(isNull payloadMime)then
307
+
"Content-Type", payloadMime
308
+
ifnot(isNull retMime)then
309
+
"Accept", retMime ]
310
+
261
311
letm=
262
312
ProvidedMethod(
263
313
providedMethodName,
@@ -271,13 +321,7 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler,
271
321
272
322
lethttpMethod= opTy.ToString()
273
323
274
-
letheaders=
275
-
<@
276
-
[ifnot(isNull payloadMime)then
277
-
"Content-Type", payloadMime
278
-
ifnot(isNull retMime)then
279
-
"Accept", retMime ]
280
-
@>
324
+
letheaders= stringPairListExpr fixedHeaders
281
325
282
326
// Locates parameters matching the arguments
283
327
let mutablepayloadExp= None
@@ -298,14 +342,7 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler,
298
342
apiArgs
299
343
|> List.choose (function
300
344
| ShapeVar sVar as expr ->
301
-
letparam=
302
-
openApiParameters
303
-
|> Seq.tryFind(fun x ->
304
-
// pain point: we have to make sure that the set of names we search for here are the same as the set of names generated when we make `parameters` above
0 commit comments