perf: reduce allocations & reflection in generated operation code#455
Merged
Conversation
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.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR optimizes OpenAPI operation generation by reducing repeated reflection and simplifying generated runtime request/response code.
Changes:
- Hoists method/union-case reflection and parameter lookup setup into provider/operation compilation paths.
- Adds runtime helpers for query-list flattening and combined header/cookie filling.
- Adds regression tests for duplicate quotation
Varreuse in generated invoke code.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/SwaggerProvider.DesignTime/OperationCompiler.fs |
Optimizes generated operation code and response casting. |
src/SwaggerProvider.DesignTime/Provider.OpenApiClient.fs |
Hoists string-list union-case lookup. |
src/SwaggerProvider.Runtime/RuntimeHelpers.fs |
Adds query-list and header/cookie runtime helpers. |
tests/SwaggerProvider.Tests/Schema.OperationCompilationTests.fs |
Adds quotation Var reuse regression tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Reduces allocations and reflection in both the OperationCompiler (compile-time) and the code generated for every API operation (runtime). Validated end-to-end against the full Stripe
spec3.json(414 paths, 1422 schemas, 587 operations).Compile-time (
OperationCompiler/Provider.OpenApiClient)RuntimeHelpers.toParam/RuntimeHelpers.toQueryParams/TaskExtensions.cast/AsyncExtensions.castMethodInforesolution to theOperationCompilerinstance — resolved once per provider instead of per generated operation.string listunion-case reflection inProvider.OpenApiClient.paramName -> IOpenApiParametermap once per operation and use it for O(1) lookups ininvokeCode(wasSeq.tryFindperShapeVar).Content-Type/Acceptheaders list at provider-generation time instead of emitting a quotation that re-evaluates per call.object/stream/string/unit) lazy so only the branch actually used for the operation's return type is built.Generated code (runtime)
RuntimeHelpers.createHttpRequestFromQueryListsinstead of chainedList.append/List.concatfor query parameter assembly.RuntimeHelpers.fillHeadersAndCookiesinstead of an inlineSeq.filter/Seq.map/String.concatcookie-building quotation.TaskExtensions.cast<'T>/AsyncExtensions.cast<'T>call viaProvidedTypeBuilder.MakeGenericMethod, avoidingMethodInfo.Invokeon every API call.Varhazard that caused FS3033 "An item with the same key has already been added" for operations with multiple path/query parameters.Runtime helpers (
RuntimeHelpers)createHttpRequestFromQueryLists: flattensseq<#seq<string*string>>into aResizeArray, filtering nulls, and delegates to existingcreateHttpRequest.fillHeadersAndCookies: callsfillHeaders, then builds theCookieheader imperatively withStringBuilderusing the standards-compliant"; "separator andTryAddWithoutValidation.Tests
Schema.OperationCompilationTeststhat assert generatedinvokeCodedoes not reuse the sameQuotations.Varinstance across path/query parameters (would otherwise cause FS3033 in real providers).Behavior changes
";"to"; "(RFC 6265 canonical form). Servers accept both; flagged for visibility.Validation
dotnet fantomas --check src/ tests/✅localhost:5000): 127 total, 0 failed ✅spec3.json(414 paths, 1422 schemas, 587 operations) compiles successfully.List.appendcalls: 0List.concatcalls: 0Varbindings: 0