From 5df1df87d4540ed21655f9abeef6d7346d0849cd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 20:15:50 +0000 Subject: [PATCH 1/5] perf: replace String.Format/sprintf with string concatenation; fast-path createHttpRequest for empty query params - combineUrl: replace sprintf with string concatenation (avoids format-string parsing on every HTTP call) - createHttpRequest: skip UriBuilder+ParseQueryString allocation when queryParams is empty (common for operations with no query parameters) - formatObject: replace String.Format calls with direct string concatenation (avoids boxing string args) - buildXmlDoc: use String.Concat to combine parts in a single allocation instead of chained + Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/SwaggerProvider.DesignTime/Utils.fs | 2 +- src/SwaggerProvider.Runtime/RuntimeHelpers.fs | 32 +++++++++++-------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/SwaggerProvider.DesignTime/Utils.fs b/src/SwaggerProvider.DesignTime/Utils.fs index ac5f7d6b..783a09cf 100644 --- a/src/SwaggerProvider.DesignTime/Utils.fs +++ b/src/SwaggerProvider.DesignTime/Utils.fs @@ -382,7 +382,7 @@ module XmlDoc = | Some rd when not(String.IsNullOrWhiteSpace rd) -> $"{escapeXml rd}" | _ -> "" - summaryPart + remarksPart + paramParts + returnsPart + String.Concat(summaryPart, remarksPart, paramParts, returnsPart) type UniqueNameGenerator(?occupiedNames: string seq) = let hash = System.Collections.Generic.HashSet<_>() diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index 4807e259..1fac9192 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -343,7 +343,7 @@ module RuntimeHelpers = let vTy = v.GetType() if vTy = typeof then - String.Format("\"{0}\"", v) + "\"" + v.ToString() + "\"" elif vTy.IsArray then let elements = (v :?> Array) @@ -351,13 +351,13 @@ module RuntimeHelpers = |> Seq.map(fun x -> if isNull x then "null" else x.ToString()) |> Array.ofSeq - String.Format("[{0}]", String.Join("; ", elements)) + "[" + String.Join("; ", elements) + "]" else v.ToString() - String.Format("{0}={1}", p.Name, s)) + p.Name + "=" + s) - String.Format("{{{0}}}", String.Join("; ", strs)) + "{" + String.Join("; ", strs) + "}" // Cached constructor for JsonPropertyNameAttribute to avoid repeated reflection lookups // when compiling large schemas with many properties. @@ -501,7 +501,7 @@ module RuntimeHelpers = new HttpClient(handler, true, BaseAddress = Uri(host)) let combineUrl (urlA: string) (urlB: string) = - sprintf "%s/%s" (urlA.TrimEnd('/')) (urlB.TrimStart('/')) + urlA.TrimEnd('/') + "/" + urlB.TrimStart('/') // Pre-built map of standard HTTP method names to their corresponding static HttpMethod // instances. Uses an ordinal case-insensitive comparer so callers passing different @@ -529,18 +529,22 @@ module RuntimeHelpers = | true, m -> m | false, _ -> HttpMethod(method.ToUpperInvariant()) - let createHttpRequest (httpMethod: string) address queryParams = + let createHttpRequest (httpMethod: string) address (queryParams: (string * string) list) = let requestUrl = - let fakeHost = "http://fake-host/" - let builder = UriBuilder(combineUrl fakeHost address) - let query = System.Web.HttpUtility.ParseQueryString(builder.Query) + // Fast path: avoid UriBuilder + ParseQueryString allocation when there are no query params. + if List.isEmpty queryParams then + address + else + let fakeHost = "http://fake-host/" + let builder = UriBuilder(combineUrl fakeHost address) + let query = System.Web.HttpUtility.ParseQueryString(builder.Query) - for name, value in queryParams do - if not <| isNull value then - query.Add(name, value) + for name, value in queryParams do + if not <| isNull value then + query.Add(name, value) - builder.Query <- query.ToString() - builder.Uri.PathAndQuery.TrimStart('/') + builder.Query <- query.ToString() + builder.Uri.PathAndQuery.TrimStart('/') let method = resolveHttpMethod httpMethod new HttpRequestMessage(method, Uri(requestUrl, UriKind.Relative)) From bcc965b189e063dd1e7c2d4adb030b485a6e2ebb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 13 May 2026 20:15:53 +0000 Subject: [PATCH 2/5] ci: trigger checks From 818375befc0dff125c8f364d377425d07816271a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 21:26:30 +0000 Subject: [PATCH 3/5] fix: revert String.Concat in Utils.fs (design-time code, minimal gain, netstandard2.0 risk) Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/1f6fb00f-2880-4601-9bc2-e05206c1878e Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> --- src/SwaggerProvider.DesignTime/Utils.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SwaggerProvider.DesignTime/Utils.fs b/src/SwaggerProvider.DesignTime/Utils.fs index 783a09cf..ac5f7d6b 100644 --- a/src/SwaggerProvider.DesignTime/Utils.fs +++ b/src/SwaggerProvider.DesignTime/Utils.fs @@ -382,7 +382,7 @@ module XmlDoc = | Some rd when not(String.IsNullOrWhiteSpace rd) -> $"{escapeXml rd}" | _ -> "" - String.Concat(summaryPart, remarksPart, paramParts, returnsPart) + summaryPart + remarksPart + paramParts + returnsPart type UniqueNameGenerator(?occupiedNames: string seq) = let hash = System.Collections.Generic.HashSet<_>() From e92fa61e91486a6153f08abf6ecd0cb8026ad85f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 21:32:50 +0000 Subject: [PATCH 4/5] fix: strip leading slash in createHttpRequest fast path + add tests Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/1a1e0084-4082-4409-b5db-938fe506ba2a Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> --- src/SwaggerProvider.Runtime/RuntimeHelpers.fs | 8 +++++-- .../RuntimeHelpersTests.fs | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index 1fac9192..194cf3dc 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -529,11 +529,15 @@ module RuntimeHelpers = | true, m -> m | false, _ -> HttpMethod(method.ToUpperInvariant()) - let createHttpRequest (httpMethod: string) address (queryParams: (string * string) list) = + let createHttpRequest (httpMethod: string) (address: string) (queryParams: (string * string) list) = let requestUrl = // Fast path: avoid UriBuilder + ParseQueryString allocation when there are no query params. + // TrimStart('/') mirrors the UriBuilder path's PathAndQuery.TrimStart('/') normalisation, + // which strips the leading slash from schema paths such as "/pets" → "pets". A leading- + // slash relative URI resolves from the host root and silently drops any base path, so + // normalisation must be applied on both branches. if List.isEmpty queryParams then - address + address.TrimStart('/') else let fakeHost = "http://fake-host/" let builder = UriBuilder(combineUrl fakeHost address) diff --git a/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs b/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs index 5a1bbb85..e36ee868 100644 --- a/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs +++ b/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs @@ -553,6 +553,27 @@ module CreateHttpRequestTests = use req = createHttpRequest "GET" "v1/pets/42" [] req.RequestUri.ToString() |> shouldContainText "v1/pets/42" + [] + let ``createHttpRequest strips leading slash from path (no query params)``() = + // OpenAPI schema path keys start with '/'; the leading slash must be removed so that + // the relative URI is resolved relative to the HttpClient.BaseAddress path rather + // than from the host root. + use req = createHttpRequest "GET" "/pets" [] + req.RequestUri.ToString() |> shouldEqual "pets" + + [] + let ``createHttpRequest strips leading slash from path (with query params)``() = + use req = createHttpRequest "GET" "/pets" [ ("status", "available") ] + let uri = req.RequestUri.ToString() + uri |> shouldNotContainText "/pets" + uri |> shouldContainText "pets" + uri |> shouldContainText "status=available" + + [] + let ``createHttpRequest strips leading slash from nested path (no query params)``() = + use req = createHttpRequest "GET" "/pets/42" [] + req.RequestUri.ToString() |> shouldEqual "pets/42" + module FillHeadersTests = From a60dc42bf90ef218572fa6c52eb77823744eeb58 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 07:23:58 +0000 Subject: [PATCH 5/5] fix: use seq in createHttpRequest to preserve original API compatibility Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/eef54ff5-7fe4-4f39-9258-82cc720dd70d Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> --- src/SwaggerProvider.Runtime/RuntimeHelpers.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index 194cf3dc..504c38f3 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -529,14 +529,14 @@ module RuntimeHelpers = | true, m -> m | false, _ -> HttpMethod(method.ToUpperInvariant()) - let createHttpRequest (httpMethod: string) (address: string) (queryParams: (string * string) list) = + let createHttpRequest (httpMethod: string) (address: string) (queryParams: seq) = let requestUrl = // Fast path: avoid UriBuilder + ParseQueryString allocation when there are no query params. // TrimStart('/') mirrors the UriBuilder path's PathAndQuery.TrimStart('/') normalisation, // which strips the leading slash from schema paths such as "/pets" → "pets". A leading- // slash relative URI resolves from the host root and silently drops any base path, so // normalisation must be applied on both branches. - if List.isEmpty queryParams then + if Seq.isEmpty queryParams then address.TrimStart('/') else let fakeHost = "http://fake-host/"