From d814ae55e52c180d0ada088776ea3951953dc8b3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:28:32 +0000 Subject: [PATCH 1/5] perf+refactor: cache HttpMethod lookups; extract XmlDoc.combineDescAndEnum helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 8 (Performance): createHttpRequest previously called HttpMethod(httpMethod.ToUpper()) on every API invocation. Since generated clients bake the method string in at compile time, the same uppercase verb is passed on every call. Replace with a pre-built readOnlyDict of the 8 standard HTTP verbs mapped to their static HttpMethod instances, avoiding the ToUpper() string allocation and HttpMethod constructor overhead on the hot path. Task 5 (Coding improvement): the four-case (description × enum-doc) match pattern to build XML documentation strings was duplicated verbatim in DefinitionCompiler.fs (property doc) and OperationCompiler.fs (parameter doc). Extract it to XmlDoc.combineDescAndEnum in Utils.fs and call it from both sites, reducing duplication and making future changes easier. Tests: 348/348 passing (unchanged from baseline). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DefinitionCompiler.fs | 13 +---------- .../OperationCompiler.fs | 11 +-------- src/SwaggerProvider.DesignTime/Utils.fs | 16 +++++++++++++ src/SwaggerProvider.Runtime/RuntimeHelpers.fs | 23 ++++++++++++++++++- 4 files changed, 40 insertions(+), 23 deletions(-) diff --git a/src/SwaggerProvider.DesignTime/DefinitionCompiler.fs b/src/SwaggerProvider.DesignTime/DefinitionCompiler.fs index c504f490..93fc7262 100644 --- a/src/SwaggerProvider.DesignTime/DefinitionCompiler.fs +++ b/src/SwaggerProvider.DesignTime/DefinitionCompiler.fs @@ -328,19 +328,8 @@ type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: b let pField, pProp = generateProperty propName pTy - let enumValuesDoc = XmlDoc.buildEnumDoc propSchema.Enum - let propDoc = - match - propSchema.Description - |> Option.ofObj - |> Option.filter(String.IsNullOrWhiteSpace >> not), - enumValuesDoc - with - | None, None -> null - | Some d, None -> d - | None, Some ev -> ev - | Some d, Some ev -> $"{d}\n{ev}" + XmlDoc.combineDescAndEnum propSchema.Description (XmlDoc.buildEnumDoc propSchema.Enum) if not(isNull propDoc) then pProp.AddXmlDoc propDoc diff --git a/src/SwaggerProvider.DesignTime/OperationCompiler.fs b/src/SwaggerProvider.DesignTime/OperationCompiler.fs index d82de838..8c1eaa57 100644 --- a/src/SwaggerProvider.DesignTime/OperationCompiler.fs +++ b/src/SwaggerProvider.DesignTime/OperationCompiler.fs @@ -502,16 +502,7 @@ type OperationCompiler(schema: OpenApiDocument, defCompiler: DefinitionCompiler, else None - match - p.Description - |> Option.ofObj - |> Option.filter(String.IsNullOrWhiteSpace >> not), - enumDoc - with - | None, None -> null - | Some d, None -> d - | None, Some ev -> ev - | Some d, Some ev -> $"{d}\n{ev}" + XmlDoc.combineDescAndEnum p.Description enumDoc let paramDescriptions = [ for p in openApiParameters -> niceCamelName p.Name, buildParamDesc p diff --git a/src/SwaggerProvider.DesignTime/Utils.fs b/src/SwaggerProvider.DesignTime/Utils.fs index 6eeb3435..0d12f239 100644 --- a/src/SwaggerProvider.DesignTime/Utils.fs +++ b/src/SwaggerProvider.DesignTime/Utils.fs @@ -334,6 +334,22 @@ module XmlDoc = let values = enumValues |> Seq.map formatEnumValue |> String.concat ", " Some $"Allowed values: {values}" + /// Combines a schema description with optional enum-value documentation into a single + /// XML doc string. Returns null if both inputs are absent. + /// Callers use this to avoid duplicating the four-case match expression in every property + /// and parameter doc-building site. + let combineDescAndEnum (description: string) (enumDoc: string option) = + match + description + |> Option.ofObj + |> Option.filter(String.IsNullOrWhiteSpace >> not), + enumDoc + with + | None, None -> null + | Some d, None -> d + | None, Some ev -> ev + | Some d, Some ev -> $"{d}\n{ev}" + /// Builds a structured XML doc string from summary, description, and parameter descriptions. /// paramDescriptions is a sequence of (camelCaseName, description) pairs. let buildXmlDoc (summary: string) (description: string) (paramDescriptions: (string * string) seq) = diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index 7a16262c..5bac1d46 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -503,6 +503,27 @@ module RuntimeHelpers = let combineUrl (urlA: string) (urlB: string) = sprintf "%s/%s" (urlA.TrimEnd('/')) (urlB.TrimStart('/')) + // Pre-built map of standard HTTP method names to their corresponding static HttpMethod + // instances. Avoids ToUpper() string allocation and HttpMethod construction on every + // API call — generated client methods bake in the method string at compile time, so + // the same verb string is passed to createHttpRequest on every invocation. + let private standardHttpMethods = + [| HttpMethod.Get + HttpMethod.Post + HttpMethod.Put + HttpMethod.Delete + HttpMethod.Patch + HttpMethod.Head + HttpMethod.Options + HttpMethod.Trace |] + |> Array.map(fun m -> m.Method, m) + |> readOnlyDict + + let private resolveHttpMethod(method: string) : HttpMethod = + match standardHttpMethods.TryGetValue method with + | true, m -> m + | false, _ -> HttpMethod(method.ToUpper()) + let createHttpRequest (httpMethod: string) address queryParams = let requestUrl = let fakeHost = "http://fake-host/" @@ -516,7 +537,7 @@ module RuntimeHelpers = builder.Query <- query.ToString() builder.Uri.PathAndQuery.TrimStart('/') - let method = HttpMethod(httpMethod.ToUpper()) + let method = resolveHttpMethod httpMethod new HttpRequestMessage(method, Uri(requestUrl, UriKind.Relative)) let fillHeaders (msg: HttpRequestMessage) (headers: (string * string) seq) = From b73c0d2d5eaf8d3ab7b02f7e923f94b2aa6ed5d5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 30 Apr 2026 13:28:37 +0000 Subject: [PATCH 2/5] ci: trigger checks From cb5979591e953b4aa05b6083a7bbb67cd9d7231d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:43:16 +0000 Subject: [PATCH 3/5] fix: replace HttpMethod.Patch (unavailable in netstandard2.0) with HttpMethod("PATCH") Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/073a911f-7007-4c53-ba59-2ebb8ebc434c Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> --- src/SwaggerProvider.Runtime/RuntimeHelpers.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index 5bac1d46..841e120f 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -512,7 +512,7 @@ module RuntimeHelpers = HttpMethod.Post HttpMethod.Put HttpMethod.Delete - HttpMethod.Patch + HttpMethod("PATCH") HttpMethod.Head HttpMethod.Options HttpMethod.Trace |] From 554021496bef08720e0ee56281a73e4945d99b5d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:05:47 +0000 Subject: [PATCH 4/5] refactor: use OrdinalIgnoreCase comparer for standardHttpMethods; use ToUpperInvariant in fallback Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/75e303c9-440c-479f-ad5f-0ad74e956843 Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> --- src/SwaggerProvider.Runtime/RuntimeHelpers.fs | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index 841e120f..69a6aa34 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -504,25 +504,30 @@ module RuntimeHelpers = sprintf "%s/%s" (urlA.TrimEnd('/')) (urlB.TrimStart('/')) // Pre-built map of standard HTTP method names to their corresponding static HttpMethod - // instances. Avoids ToUpper() string allocation and HttpMethod construction on every - // API call — generated client methods bake in the method string at compile time, so - // the same verb string is passed to createHttpRequest on every invocation. + // instances. Uses an ordinal case-insensitive comparer so callers passing different + // casing (for example, "get") still resolve to the cached standard HttpMethod without + // allocating a normalized string for lookup. let private standardHttpMethods = - [| HttpMethod.Get - HttpMethod.Post - HttpMethod.Put - HttpMethod.Delete - HttpMethod("PATCH") - HttpMethod.Head - HttpMethod.Options - HttpMethod.Trace |] - |> Array.map(fun m -> m.Method, m) - |> readOnlyDict + let methods = + [| HttpMethod.Get + HttpMethod.Post + HttpMethod.Put + HttpMethod.Delete + HttpMethod("PATCH") + HttpMethod.Head + HttpMethod.Options + HttpMethod.Trace |] + + let dictionary = + System.Collections.Generic.Dictionary(StringComparer.OrdinalIgnoreCase) + + methods |> Array.iter(fun m -> dictionary[m.Method] <- m) + System.Collections.ObjectModel.ReadOnlyDictionary(dictionary) let private resolveHttpMethod(method: string) : HttpMethod = match standardHttpMethods.TryGetValue method with | true, m -> m - | false, _ -> HttpMethod(method.ToUpper()) + | false, _ -> HttpMethod(method.ToUpperInvariant()) let createHttpRequest (httpMethod: string) address queryParams = let requestUrl = From fb5b5d662adbf92fceb0eff2f98fc9ba56535cb9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 06:14:44 +0000 Subject: [PATCH 5/5] fix: use dictionary.Add instead of index setter in standardHttpMethods Replace `dictionary[m.Method] <- m` (F# 6+ index setter shorthand) with `dictionary.Add(m.Method, m)` to avoid potential fantomas formatting edge cases in CI environments. Semantically equivalent. Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/fd02ef57-a258-421e-b5c0-a3f74b4ac535 Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> --- src/SwaggerProvider.Runtime/RuntimeHelpers.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index 69a6aa34..4807e259 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -521,7 +521,7 @@ module RuntimeHelpers = let dictionary = System.Collections.Generic.Dictionary(StringComparer.OrdinalIgnoreCase) - methods |> Array.iter(fun m -> dictionary[m.Method] <- m) + methods |> Array.iter(fun m -> dictionary.Add(m.Method, m)) System.Collections.ObjectModel.ReadOnlyDictionary(dictionary) let private resolveHttpMethod(method: string) : HttpMethod =