Skip to content

Commit 9a18cf4

Browse files
perf: avoid FSharpValue.GetUnionFields in toParam via cached tag reader
Replace the FSharpValue.GetUnionFields call in toParam (used when unwrapping F# option values for query/form parameters) with a precomputed union-tag reader. GetUnionFields allocates a UnionCaseInfo object and an obj[] on every call. The new approach: - Caches FSharpValue.PreComputeUnionTagReader per option type (O(1) tag check with no allocation after first call) - Caches the Value PropertyInfo per option type (shared with unwrapFSharpOption) - Removes the now-redundant private optionValuePropCache, consolidating both caches in one place This matters for APIs that send many optional query/form parameters; the improvement is visible when profiling clients that call high- frequency endpoints with optional fields. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent deb9d9f commit 9a18cf4

1 file changed

Lines changed: 25 additions & 9 deletions

File tree

src/SwaggerProvider.Runtime/RuntimeHelpers.fs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,20 @@ module RuntimeHelpers =
111111
let private tryFormatTimeOnly(value: obj) =
112112
tryFormatViaMethods timeOnlyTypeName "HH:mm:ss.FFFFFFF" value
113113

114+
// Cache of precomputed union tag readers for F# option types. Avoids the overhead of
115+
// FSharpValue.GetUnionFields (which allocates UnionCaseInfo + obj[]) on each call.
116+
// Stores as (obj -> int) with an explicit wrapper to satisfy nullable annotations.
117+
let private optionTagReaderCache =
118+
Collections.Concurrent.ConcurrentDictionary<Type, obj -> int>()
119+
120+
let private makeOptionTagReader(t: Type) : obj -> int =
121+
let reader = Microsoft.FSharp.Reflection.FSharpValue.PreComputeUnionTagReader t
122+
fun (o: obj) -> reader o
123+
124+
// Cache of the 'Value' PropertyInfo per F# option type, shared with unwrapFSharpOption below.
125+
let private optionValueCache =
126+
Collections.Concurrent.ConcurrentDictionary<Type, Reflection.PropertyInfo>()
127+
114128
let rec toParam(obj: obj) =
115129
match obj with
116130
| :? DateTime as dt -> dt.ToString("O")
@@ -125,15 +139,20 @@ module RuntimeHelpers =
125139
| None ->
126140
let ty = obj.GetType()
127141

128-
// Unwrap F# Option<T>: Some(x) -> toParam(x), None -> null
142+
// Unwrap F# Option<T>: Some(x) -> toParam(x), None -> null.
143+
// Uses a precomputed tag reader (cached) to check Some/None without
144+
// allocating a UnionCaseInfo or obj[] on every call.
129145
if
130146
ty.IsGenericType
131147
&& ty.GetGenericTypeDefinition() = typedefof<option<_>>
132148
then
133-
let (case, values) = Microsoft.FSharp.Reflection.FSharpValue.GetUnionFields(obj, ty)
149+
let tagReader =
150+
optionTagReaderCache.GetOrAdd(ty, System.Func<Type, obj -> int>(makeOptionTagReader))
151+
152+
if tagReader obj = 1 then // 1 = Some
153+
let valueProp = optionValueCache.GetOrAdd(ty, fun t -> t.GetProperty("Value"))
134154

135-
if case.Name = "Some" && values.Length > 0 then
136-
toParam values.[0]
155+
toParam(valueProp.GetValue(obj))
137156
else
138157
null
139158
else
@@ -287,10 +306,7 @@ module RuntimeHelpers =
287306

288307
// Unwraps F# option values: returns the inner value for Some, null for None.
289308
// This prevents `Some(value)` from being sent as-is in form data.
290-
// The `Value` PropertyInfo is cached per concrete option type to avoid repeated reflection lookups.
291-
let private optionValuePropCache =
292-
Collections.Concurrent.ConcurrentDictionary<Type, Reflection.PropertyInfo>()
293-
309+
// Reuses optionValueCache defined alongside toParam above.
294310
let private unwrapFSharpOption(value: obj) : obj =
295311
if isNull value then
296312
null
@@ -301,7 +317,7 @@ module RuntimeHelpers =
301317
ty.IsGenericType
302318
&& ty.GetGenericTypeDefinition() = typedefof<option<_>>
303319
then
304-
let prop = optionValuePropCache.GetOrAdd(ty, fun t -> t.GetProperty("Value"))
320+
let prop = optionValueCache.GetOrAdd(ty, fun t -> t.GetProperty("Value"))
305321
prop.GetValue(value)
306322
else
307323
value

0 commit comments

Comments
 (0)