From 6083db9d41753e22d77480ec4c36861130653fe1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:35:57 +0000 Subject: [PATCH 1/3] perf: eliminate redundant GetType() calls in toParam Previously, toParam dispatched through tryFormatDateOnly and tryFormatTimeOnly, each of which called value.GetType() internally. For any non-DateTime/non-DateTimeOffset scalar type (string, int, Guid, bool, etc.), this resulted in up to 3 GetType() calls before falling through to obj.ToString(). Restructure toParam to call GetType() once at the top of the catch-all branch, then dispatch to a shared formatDateOrTimeValue helper or the option unwrapping path as needed. The three private helpers (tryFormatViaMethods, tryFormatDateOnly, tryFormatTimeOnly) are removed; formatDateOrTimeValue replaces them with an API that accepts the pre-computed Type, making the single GetType() call obvious at the call site. Behavior is identical; all 327 unit tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/SwaggerProvider.Runtime/RuntimeHelpers.fs | 87 ++++++++----------- 1 file changed, 38 insertions(+), 49 deletions(-) diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index 49beb3f..aaf02e7 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -86,30 +86,18 @@ module RuntimeHelpers = let private isTimeOnlyLikeType(t: Type) = isTimeOnlyType t || isOptionOfTimeOnlyType t - let private tryFormatViaMethods (typeName: string) (format: string) (value: obj) = - if isNull value then - None - else - let ty = value.GetType() - - if ty.FullName = typeName then - match value with - | :? IFormattable as formattable -> Some(formattable.ToString(format, Globalization.CultureInfo.InvariantCulture)) - | _ -> - match - ty.GetMethod("ToString", [| typeof; typeof |]) - |> Option.ofObj - with - | Some methodInfo -> Some(methodInfo.Invoke(value, [| box format; box Globalization.CultureInfo.InvariantCulture |]) :?> string) - | None -> None - else - None - - let private tryFormatDateOnly(value: obj) = - tryFormatViaMethods dateOnlyTypeName "yyyy-MM-dd" value - - let private tryFormatTimeOnly(value: obj) = - tryFormatViaMethods timeOnlyTypeName "HH:mm:ss.FFFFFFF" value + // Formats a DateOnly or TimeOnly value using the given format string. + // The caller has already verified ty.FullName matches the expected type name. + // DateOnly and TimeOnly implement IFormattable on .NET 6+; the GetMethod + // fallback is a defensive path for forward-compatibility only. + let private formatDateOrTimeValue (format: string) (ty: Type) (value: obj) : string = + match value with + | :? IFormattable as f -> f.ToString(format, Globalization.CultureInfo.InvariantCulture) + | _ -> + ty.GetMethod("ToString", [| typeof; typeof |]) + |> Option.ofObj + |> Option.map(fun mi -> mi.Invoke(value, [| box format; box Globalization.CultureInfo.InvariantCulture |]) :?> string) + |> Option.defaultValue(value.ToString()) // Cache of precomputed union tag readers for F# option types. Avoids the overhead of // FSharpValue.GetUnionFields (which allocates UnionCaseInfo + obj[]) on each call. @@ -139,31 +127,32 @@ module RuntimeHelpers = | :? DateTimeOffset as dto -> dto.ToString("O") | null -> null | _ -> - match tryFormatDateOnly obj with - | Some formatted -> formatted - | None -> - match tryFormatTimeOnly obj with - | Some formatted -> formatted - | None -> - let ty = obj.GetType() - - // Unwrap F# Option: Some(x) -> toParam(x), None -> null. - // Uses a precomputed tag reader (cached) to check Some/None without - // allocating a UnionCaseInfo or obj[] on every call. - if - ty.IsGenericType - && ty.GetGenericTypeDefinition() = typedefof> - then - let tagReader = optionTagReaderCache.GetOrAdd(ty, optionTagReaderFactory) - - if tagReader obj = 1 then // 1 = Some - let valueProp = optionValueCache.GetOrAdd(ty, optionValueFactory) - - toParam(valueProp.GetValue(obj)) - else - null - else - obj.ToString() + // Hoist GetType() once; previously tryFormatDateOnly and tryFormatTimeOnly + // each called GetType() internally, resulting in up to 3 GetType() calls for + // common scalar types such as string, int, Guid, or bool. + let ty = obj.GetType() + + if ty.FullName = dateOnlyTypeName then + formatDateOrTimeValue "yyyy-MM-dd" ty obj + elif ty.FullName = timeOnlyTypeName then + formatDateOrTimeValue "HH:mm:ss.FFFFFFF" ty obj + // Unwrap F# Option: Some(x) -> toParam(x), None -> null. + // Uses a precomputed tag reader (cached) to check Some/None without + // allocating a UnionCaseInfo or obj[] on every call. + elif + ty.IsGenericType + && ty.GetGenericTypeDefinition() = typedefof> + then + let tagReader = optionTagReaderCache.GetOrAdd(ty, optionTagReaderFactory) + + if tagReader obj = 1 then // 1 = Some + let valueProp = optionValueCache.GetOrAdd(ty, optionValueFactory) + + toParam(valueProp.GetValue(obj)) + else + null + else + obj.ToString() let toQueryParams (name: string) (obj: obj) (client: Swagger.ProvidedApiClientBase) = if isNull obj then From 7e6607e0c244eb8de64d490fa97d8b4de2b5d078 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Apr 2026 13:36:01 +0000 Subject: [PATCH 2/3] ci: trigger checks From 31ad67009e891893692bdadf06d201e9922fc3e7 Mon Sep 17 00:00:00 2001 From: Sergey Tihon Date: Tue, 28 Apr 2026 15:56:02 +0200 Subject: [PATCH 3/3] Update src/SwaggerProvider.Runtime/RuntimeHelpers.fs Co-authored-by: Copilot <175728472+Copilot@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 aaf02e7..1febdbe 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -97,7 +97,7 @@ module RuntimeHelpers = ty.GetMethod("ToString", [| typeof; typeof |]) |> Option.ofObj |> Option.map(fun mi -> mi.Invoke(value, [| box format; box Globalization.CultureInfo.InvariantCulture |]) :?> string) - |> Option.defaultValue(value.ToString()) + |> Option.defaultWith(fun () -> value.ToString()) // Cache of precomputed union tag readers for F# option types. Avoids the overhead of // FSharpValue.GetUnionFields (which allocates UnionCaseInfo + obj[]) on each call.