Skip to content

Commit edad98f

Browse files
committed
Refactor 'ObjectListFilter' to use 'System.Text.Json' coercion
Replaces custom value coercers with 'System.Text.Json'-based coercion in 'ObjectListFilter', supporting advanced scenarios like F# DUs and CLR enums via 'JsonSerializerOptions'. Updates 'ObjectListFilterLinqOptions' to accept 'JsonSerializerOptions'. Refactors 'TypeCoercion' module to use JSON serialization/deserialization for all type conversions. Updates filter application logic and expands the test suite with new files to cover a wide range of coercion scenarios. Updates documentation and usage examples accordingly.
1 parent d3e00fa commit edad98f

9 files changed

Lines changed: 662 additions & 176 deletions

File tree

src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ namespace FSharp.Data.GraphQL.Server.Middleware
22

33
open System
44
open System.Collections
5+
open System.Text.Json
56
open FSharp.Data.GraphQL
67

78
/// A filter definition for a field value.
@@ -46,25 +47,19 @@ type private CompareDiscriminatorExpression<'T, 'D> = Expression<Func<'T, 'D, bo
4647
type ObjectListFilterValidationException (message : string, [<Optional>] extensions : Dictionary<string, obj> | null) =
4748
inherit GQLMessageExceptionBase (ErrorKind.Validation, message, extensions)
4849

49-
/// <summary>
50-
/// Function signature for custom value coercion logic. Takes a target CLR type and a JSON
51-
/// primitive value, returns the coerced value or <see langword="ValueNone"/> if coercion is not supported.
52-
/// </summary>
53-
type FilterValueCoercer = Type -> obj -> obj voption
54-
5550
/// <summary>
5651
/// Optional configuration for LINQ translation including discriminator handling.
5752
/// </summary>
5853
type ObjectListFilterLinqOptions<'T, 'D>
5954
(
6055
[<Optional>] compareDiscriminator : CompareDiscriminatorExpression<'T, 'D> | null,
6156
[<Optional>] getDiscriminatorValue : (Type -> 'D) | null,
62-
[<Optional>] customCoercers : FilterValueCoercer list | null
57+
[<Optional>] jsonOptions : JsonSerializerOptions | null
6358
) =
6459

6560
member _.CompareDiscriminator = compareDiscriminator |> ValueOption.ofObj
6661
member _.GetDiscriminatorValue = getDiscriminatorValue |> ValueOption.ofObj
67-
member _.CustomCoercers = customCoercers |> Option.ofObj |> Option.defaultValue []
62+
member _.JsonOptions = jsonOptions |> ValueOption.ofObj
6863

6964
static member None = ObjectListFilterLinqOptions<'T, 'D> (null, null, null)
7065

@@ -82,10 +77,10 @@ type ObjectListFilterLinqOptions<'T, 'D>
8277
ObjectListFilterLinqOptions<'T, 'D> (null, getDiscriminatorValue, null)
8378
new (getDiscriminator : Expression<Func<'T, 'D>>, getDiscriminatorValue : Type -> 'D) =
8479
ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, getDiscriminatorValue, null)
85-
new (customCoercers : FilterValueCoercer list) =
86-
ObjectListFilterLinqOptions<'T, 'D> (null, null, customCoercers)
87-
new (getDiscriminator : Expression<Func<'T, 'D>>, customCoercers : FilterValueCoercer list) =
88-
ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, null, customCoercers)
89-
new (compareDiscriminator : CompareDiscriminatorExpression<'T, 'D>, customCoercers : FilterValueCoercer list) =
90-
ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator, null, customCoercers)
80+
new (jsonOptions : JsonSerializerOptions) =
81+
ObjectListFilterLinqOptions<'T, 'D> (null, null, jsonOptions)
82+
new (getDiscriminator : Expression<Func<'T, 'D>>, jsonOptions : JsonSerializerOptions) =
83+
ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, null, jsonOptions)
84+
new (compareDiscriminator : CompareDiscriminatorExpression<'T, 'D>, jsonOptions : JsonSerializerOptions) =
85+
ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator, null, jsonOptions)
9186

src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilterModule.fs

Lines changed: 18 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,10 @@ module ObjectListFilter =
196196
| NonEnumerableCast _ ->
197197
Expression.NotEqual (Expression.Convert (``member``, objectType), Expression.Convert ((Expression.Constant f.Value), objectType))
198198
| Enumerable ->
199-
let ``const`` = Expression.Constant (Values.normalizeOptional ``member``.Type f.Value)
200-
Expression.Not (Expression.Call (``const``, equalsMethod, ``member``))
199+
let normalized = Values.normalizeOptional ``member``.Type f.Value
200+
let ``const`` = Expression.Constant (normalized)
201+
let boxedArg = Expression.Convert (``member``, objectType)
202+
Expression.Not (Expression.Call (``const``, equalsMethod, boxedArg))
201203
| Not f -> f |> build |> Expression.Not :> Expression
202204
| And (f1, f2) -> Expression.AndAlso (build f1, build f2)
203205
| Or (f1, f2) -> Expression.OrElse (build f1, build f2)
@@ -215,8 +217,10 @@ module ObjectListFilter =
215217
| NonEnumerableCast _ ->
216218
Expression.Equal (Expression.Convert (``member``, objectType), Expression.Convert ((Expression.Constant f.Value), objectType))
217219
| Enumerable ->
218-
let ``const`` = Expression.Constant (Values.normalizeOptional ``member``.Type f.Value)
219-
Expression.Call (``const``, equalsMethod, ``member``)
220+
let normalized = Values.normalizeOptional ``member``.Type f.Value
221+
let ``const`` = Expression.Constant (normalized)
222+
let boxedArg = Expression.Convert (``member``, objectType)
223+
Expression.Call (``const``, equalsMethod, boxedArg)
220224
| GreaterThan f ->
221225
let ``member`` = Expression.PropertyOrField (param, f.FieldName)
222226
match f.Value with
@@ -362,59 +366,41 @@ module ObjectListFilterExtensions =
362366
/// <summary>
363367
/// Applies the filter to a queryable with automatic type coercion of JSON primitives to CLR types.
364368
/// Supports <see cref="Guid"/>, <see cref="DateTime"/>, <see cref="DateTimeOffset"/>, <see cref="DateOnly"/>, and F# discriminated unions.
365-
/// Pass custom coercers via <c>ObjectListFilterLinqOptions</c> constructor.
369+
/// Pass <see cref="JsonSerializerOptions"/> via <c>ObjectListFilterLinqOptions</c> constructor for custom serialization.
366370
/// </summary>
367371
/// <example>
368372
/// <code>
369373
/// // Basic usage - automatic coercion of string to Guid
370374
/// let filter = "id" === "550e8400-e29b-41d4-a716-446655440000"
371375
/// let users = filter.ApplyTo query
372376
///
373-
/// // With custom coercer for NodaTime.Instant
374-
/// let nodaCoercer : FilterValueCoercer = fun targetType value ->
375-
/// if targetType = typeof&lt;NodaTime.Instant&gt; then
376-
/// match value with
377-
/// | :? string as s ->
378-
/// let parsed = NodaTime.Text.InstantPattern.ExtendedIso.Parse s
379-
/// if parsed.Success then ValueSome (box parsed.Value)
380-
/// else ValueNone
381-
/// | _ -> ValueNone
382-
/// else ValueNone
383-
///
384-
/// let options = ObjectListFilterLinqOptions([nodaCoercer])
377+
/// // With custom JsonSerializerOptions
378+
/// let opts = JsonSerializerOptions(PropertyNameCaseInsensitive = true)
379+
/// let options = ObjectListFilterLinqOptions(opts)
385380
/// let events = filter.ApplyTo(query, options)
386381
/// </code>
387382
/// </example>
388383
member inline filter.ApplyTo<'T, 'D> (query : IQueryable<'T>, [<Optional>] options : ObjectListFilterLinqOptions<'T, 'D> | null) =
389384
let options = options |> ValueOption.ofObj |> ValueOption.defaultValue ObjectListFilterLinqOptions<'T, 'D>.None
390-
let filter = TypeCoercion.coerceFilter options.CustomCoercers typeof<'T> filter
385+
let filter = TypeCoercion.coerceFilter options.JsonOptions typeof<'T> filter
391386
apply options filter query
392387

393388
type IQueryable<'T> with
394389

395390
/// <summary>
396391
/// Applies the filter with automatic type coercion of JSON primitives to CLR types. Supports <see cref="Guid"/>, <see cref="DateTime"/>,
397-
/// <see cref="DateTimeOffset"/>, <see cref="DateOnly"/>, and F# discriminated unions. Pass custom coercers via <c>
398-
/// ObjectListFilterLinqOptions</c> constructor.
392+
/// <see cref="DateTimeOffset"/>, <see cref="DateOnly"/>, and F# discriminated unions. Pass <see cref="JsonSerializerOptions"/> via <c>
393+
/// ObjectListFilterLinqOptions</c> constructor for custom serialization.
399394
/// </summary>
400395
/// <example>
401396
/// <code>
402397
/// // Basic usage - automatic coercion of string to Guid
403398
/// let filter = "id" === "550e8400-e29b-41d4-a716-446655440000"
404399
/// let users = query.Apply filter
405400
///
406-
/// // With custom coercer for NodaTime.Instant
407-
/// let nodaCoercer : FilterValueCoercer = fun targetType value ->
408-
/// if targetType = typeof&lt;NodaTime.Instant&gt; then
409-
/// match value with
410-
/// | :? string as s ->
411-
/// let parsed = NodaTime.Text.InstantPattern.ExtendedIso.Parse s
412-
/// if parsed.Success then ValueSome (box parsed.Value)
413-
/// else ValueNone
414-
/// | _ -> ValueNone
415-
/// else ValueNone
416-
///
417-
/// let options = ObjectListFilterLinqOptions([nodaCoercer])
401+
/// // With custom JsonSerializerOptions
402+
/// let opts = JsonSerializerOptions(PropertyNameCaseInsensitive = true)
403+
/// let options = ObjectListFilterLinqOptions(opts)
418404
/// let events = query.Apply(filter, options)
419405
/// </code>
420406
/// </example>

0 commit comments

Comments
 (0)