Skip to content

Commit 0ca2f05

Browse files
Copilotxperiandri
andauthored
Redesign ObjectListFilter: add IComparer to Equals/StartsWith/EndsWith/Contains DU cases
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
1 parent 4a329e7 commit 0ca2f05

5 files changed

Lines changed: 236 additions & 145 deletions

File tree

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

Lines changed: 79 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,24 @@ type ObjectListFilter =
1111
| And of ObjectListFilter * ObjectListFilter
1212
| Or of ObjectListFilter * ObjectListFilter
1313
| Not of ObjectListFilter
14-
| Equals of FieldFilter<System.IComparable>
14+
| Equals of Filter : FieldFilter<System.IComparable> * Comparer : System.Collections.IComparer
1515
| GreaterThan of FieldFilter<System.IComparable>
1616
| GreaterThanOrEqual of FieldFilter<System.IComparable>
1717
| LessThan of FieldFilter<System.IComparable>
1818
| LessThanOrEqual of FieldFilter<System.IComparable>
1919
| In of FieldFilter<obj list>
20-
| StartsWith of FieldFilter<string>
21-
| EndsWith of FieldFilter<string>
22-
| Contains of FieldFilter<System.IComparable>
20+
| StartsWith of Filter : FieldFilter<string> * Comparer : System.Collections.IComparer
21+
| EndsWith of Filter : FieldFilter<string> * Comparer : System.Collections.IComparer
22+
| Contains of Filter : FieldFilter<System.IComparable> * Comparer : System.Collections.IComparer
2323
| OfTypes of Type list
2424
| FilterField of FieldFilter<ObjectListFilter>
25-
| EqualsCI of FieldFilter<string>
26-
| StartsWithCI of FieldFilter<string>
27-
| EndsWithCI of FieldFilter<string>
28-
| ContainsCI of FieldFilter<string>
2925

3026
open System.Linq
3127
open System.Linq.Expressions
3228
open System.Runtime.InteropServices
3329
open System.Reflection
3430
open System.Collections.Generic
31+
open System.Collections
3532

3633
type private CompareDiscriminatorExpression<'T, 'D> = Expression<Func<'T, 'D, bool>>
3734

@@ -99,7 +96,7 @@ module ObjectListFilter =
9996
let ( ||| ) x y = Or (x, y)
10097

10198
/// Creates a new ObjectListFilter representing an EQUALS operation between two comparable values.
102-
let ( === ) fname value = Equals { FieldName = fname; Value = value }
99+
let ( === ) fname value = Equals ({ FieldName = fname; Value = value }, null)
103100

104101
/// Creates a new ObjectListFilter representing a GREATER THAN operation of a comparable value.
105102
let ( >>> ) fname value = GreaterThan { FieldName = fname; Value = value }
@@ -114,13 +111,13 @@ module ObjectListFilter =
114111
let ( <== ) fname value = LessThanOrEqual { FieldName = fname; Value = value }
115112

116113
/// Creates a new ObjectListFilter representing a STARTS WITH operation of a string value.
117-
let ( =@@ ) fname value = StartsWith { FieldName = fname; Value = value }
114+
let ( =@@ ) fname value = StartsWith ({ FieldName = fname; Value = value }, null)
118115

119116
/// Creates a new ObjectListFilter representing an ENDS WITH operation of a string value.
120-
let ( @@= ) fname value = EndsWith { FieldName = fname; Value = value }
117+
let ( @@= ) fname value = EndsWith ({ FieldName = fname; Value = value }, null)
121118

122119
/// Creates a new ObjectListFilter representing a CONTAINS operation.
123-
let ( @=@ ) fname value = Contains { FieldName = fname; Value = value }
120+
let ( @=@ ) fname value = Contains ({ FieldName = fname; Value = value }, null)
124121

125122
/// Creates a new ObjectListFilter representing a IN operation.
126123
let ( =~= ) fname value = In { FieldName = fname; Value = value }
@@ -132,16 +129,16 @@ module ObjectListFilter =
132129
let ( !!! ) filter = Not filter
133130

134131
/// Creates a new ObjectListFilter representing a case-insensitive EQUALS operation on a string value.
135-
let ( ===~ ) fname value = EqualsCI { FieldName = fname; Value = value }
132+
let ( ===~ ) fname (value : string) = Equals ({ FieldName = fname; Value = value }, StringComparer.OrdinalIgnoreCase)
136133

137134
/// Creates a new ObjectListFilter representing a case-insensitive STARTS WITH operation on a string value.
138-
let ( =@@~ ) fname value = StartsWithCI { FieldName = fname; Value = value }
135+
let ( =@@~ ) fname value = StartsWith ({ FieldName = fname; Value = value }, StringComparer.OrdinalIgnoreCase)
139136

140137
/// Creates a new ObjectListFilter representing a case-insensitive ENDS WITH operation on a string value.
141-
let ( @@=~ ) fname value = EndsWithCI { FieldName = fname; Value = value }
138+
let ( @@=~ ) fname value = EndsWith ({ FieldName = fname; Value = value }, StringComparer.OrdinalIgnoreCase)
142139

143140
/// Creates a new ObjectListFilter representing a case-insensitive CONTAINS operation on a string value.
144-
let ( @=@~ ) fname value = ContainsCI { FieldName = fname; Value = value }
141+
let ( @=@~ ) fname (value : string) = Contains ({ FieldName = fname; Value = value }, StringComparer.OrdinalIgnoreCase)
145142

146143
let private genericWhereMethod =
147144
typeof<Queryable>.GetMethods ()
@@ -164,11 +161,10 @@ module ObjectListFilter =
164161
let private StringEndsWithMethod = stringType.GetMethod ("EndsWith", [| stringType |])
165162
let private StringContainsMethod = stringType.GetMethod ("Contains", [| stringType |])
166163
let private stringComparisonType = typeof<StringComparison>
167-
let private StringEqualsCIMethod = stringType.GetMethod ("Equals", [| stringType; stringComparisonType |])
168-
let private StringStartsWithCIMethod = stringType.GetMethod ("StartsWith", [| stringType; stringComparisonType |])
169-
let private StringEndsWithCIMethod = stringType.GetMethod ("EndsWith", [| stringType; stringComparisonType |])
170-
let private StringContainsCIMethod = stringType.GetMethod ("Contains", [| stringType; stringComparisonType |])
171-
let private OrdinalIgnoreCase = Expression.Constant (StringComparison.OrdinalIgnoreCase)
164+
let private StringStartsWithWithComparisonMethod = stringType.GetMethod ("StartsWith", [| stringType; stringComparisonType |])
165+
let private StringEndsWithWithComparisonMethod = stringType.GetMethod ("EndsWith", [| stringType; stringComparisonType |])
166+
let private StringContainsWithComparisonMethod = stringType.GetMethod ("Contains", [| stringType; stringComparisonType |])
167+
let private StringEqualsWithComparisonMethod = stringType.GetMethod ("Equals", [| stringType; stringComparisonType |])
172168
let private unwrapOptionMethod =
173169
FSharp.Data.GraphQL.Helpers.moduleType.GetMethod (nameof Helpers.unwrap)
174170

@@ -227,6 +223,20 @@ module ObjectListFilter =
227223
|> Seq.where (fun m -> m.Name = "Equals")
228224
|> Seq.head
229225

226+
/// Maps an IComparer to a StringComparison value.
227+
/// Returns ValueNone for null or Ordinal comparers (use default Expression.Equal path).
228+
let private comparerToStringComparison (comparer : IComparer) =
229+
match comparer with
230+
| null -> ValueNone
231+
| :? StringComparer as sc ->
232+
if obj.ReferenceEquals (sc, StringComparer.OrdinalIgnoreCase) then ValueSome StringComparison.OrdinalIgnoreCase
233+
elif obj.ReferenceEquals (sc, StringComparer.InvariantCultureIgnoreCase) then ValueSome StringComparison.InvariantCultureIgnoreCase
234+
elif obj.ReferenceEquals (sc, StringComparer.CurrentCultureIgnoreCase) then ValueSome StringComparison.CurrentCultureIgnoreCase
235+
elif obj.ReferenceEquals (sc, StringComparer.InvariantCulture) then ValueSome StringComparison.InvariantCulture
236+
elif obj.ReferenceEquals (sc, StringComparer.CurrentCulture) then ValueSome StringComparison.CurrentCulture
237+
else ValueNone
238+
| _ -> ValueNone
239+
230240
let rec buildFilterExpr isEnumerableQuery (param : SourceExpression) buildTypeDiscriminatorCheck filter : Expression =
231241

232242
let build = buildFilterExpr isEnumerableQuery param buildTypeDiscriminatorCheck
@@ -246,31 +256,41 @@ module ObjectListFilter =
246256
| _ -> Expression.Convert (``member``, stringType)
247257

248258
match filter with
249-
| Not (Equals f) ->
259+
| Not (Equals (f, comparer)) ->
250260
let ``member`` = Expression.PropertyOrField (param, f.FieldName)
251-
let hasEqualityOperator = hasEqualityOperator ``member``.Type
252-
match f.Value with
253-
| NoCast when hasEqualityOperator -> Expression.NotEqual (``member``, Expression.Constant f.Value)
254-
| NoCast
255-
| NonEnumerableCast _ ->
256-
Expression.NotEqual (Expression.Convert (``member``, objectType), Expression.Convert ((Expression.Constant f.Value), objectType))
257-
| Enumerable ->
258-
let ``const`` = Expression.Constant (Values.normalizeOptional ``member``.Type f.Value)
259-
Expression.Not (Expression.Call (``const``, equalsMethod, ``member``))
261+
match comparerToStringComparison comparer with
262+
| ValueSome comparison ->
263+
let value = Helpers.unwrap (box f.Value) :?> string
264+
Expression.Not (Expression.Call (normalizeStringMemberExpr ``member``, StringEqualsWithComparisonMethod, Expression.Constant (value, typeof<string>), Expression.Constant comparison)) :> Expression
265+
| ValueNone ->
266+
let hasEqualityOperator = hasEqualityOperator ``member``.Type
267+
match f.Value with
268+
| NoCast when hasEqualityOperator -> Expression.NotEqual (``member``, Expression.Constant f.Value)
269+
| NoCast
270+
| NonEnumerableCast _ ->
271+
Expression.NotEqual (Expression.Convert (``member``, objectType), Expression.Convert ((Expression.Constant f.Value), objectType))
272+
| Enumerable ->
273+
let ``const`` = Expression.Constant (Values.normalizeOptional ``member``.Type f.Value)
274+
Expression.Not (Expression.Call (``const``, equalsMethod, ``member``))
260275
| Not f -> f |> build |> Expression.Not :> Expression
261276
| And (f1, f2) -> Expression.AndAlso (build f1, build f2)
262277
| Or (f1, f2) -> Expression.OrElse (build f1, build f2)
263-
| Equals f ->
278+
| Equals (f, comparer) ->
264279
let ``member`` = Expression.PropertyOrField (param, f.FieldName)
265-
let hasEqualityOperator = hasEqualityOperator ``member``.Type
266-
match f.Value with
267-
| NoCast when hasEqualityOperator -> Expression.Equal (``member``, Expression.Constant f.Value)
268-
| NoCast
269-
| NonEnumerableCast _ ->
270-
Expression.Equal (Expression.Convert (``member``, objectType), Expression.Convert ((Expression.Constant f.Value), objectType))
271-
| Enumerable ->
272-
let ``const`` = Expression.Constant (Values.normalizeOptional ``member``.Type f.Value)
273-
Expression.Call (``const``, equalsMethod, ``member``)
280+
match comparerToStringComparison comparer with
281+
| ValueSome comparison ->
282+
let value = Helpers.unwrap (box f.Value) :?> string
283+
Expression.Call (normalizeStringMemberExpr ``member``, StringEqualsWithComparisonMethod, Expression.Constant (value, typeof<string>), Expression.Constant comparison) :> Expression
284+
| ValueNone ->
285+
let hasEqualityOperator = hasEqualityOperator ``member``.Type
286+
match f.Value with
287+
| NoCast when hasEqualityOperator -> Expression.Equal (``member``, Expression.Constant f.Value)
288+
| NoCast
289+
| NonEnumerableCast _ ->
290+
Expression.Equal (Expression.Convert (``member``, objectType), Expression.Convert ((Expression.Constant f.Value), objectType))
291+
| Enumerable ->
292+
let ``const`` = Expression.Constant (Values.normalizeOptional ``member``.Type f.Value)
293+
Expression.Call (``const``, equalsMethod, ``member``)
274294
| GreaterThan f ->
275295
let ``member`` = Expression.PropertyOrField (param, f.FieldName)
276296
match f.Value with
@@ -295,14 +315,22 @@ module ObjectListFilter =
295315
| NoCast -> Expression.LessThanOrEqual (``member``, Expression.Constant f.Value)
296316
| Enumerable -> Expression.LessThanOrEqual (``member``, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value))
297317
| NonEnumerableCast ``type`` -> Expression.LessThanOrEqual ((unsafeConvertTo ``type`` ``member``), Expression.Constant f.Value)
298-
| StartsWith f ->
318+
| StartsWith (f, comparer) ->
299319
let ``member`` = Expression.PropertyOrField (param, f.FieldName)
300-
Expression.Call (normalizeStringMemberExpr ``member``, StringStartsWithMethod, Expression.Constant f.Value)
301-
| EndsWith f ->
320+
match comparerToStringComparison comparer with
321+
| ValueSome comparison ->
322+
Expression.Call (normalizeStringMemberExpr ``member``, StringStartsWithWithComparisonMethod, Expression.Constant f.Value, Expression.Constant comparison)
323+
| ValueNone ->
324+
Expression.Call (normalizeStringMemberExpr ``member``, StringStartsWithMethod, Expression.Constant f.Value)
325+
| EndsWith (f, comparer) ->
302326
let ``member`` = Expression.PropertyOrField (param, f.FieldName)
303-
Expression.Call (normalizeStringMemberExpr ``member``, StringEndsWithMethod, Expression.Constant f.Value)
327+
match comparerToStringComparison comparer with
328+
| ValueSome comparison ->
329+
Expression.Call (normalizeStringMemberExpr ``member``, StringEndsWithWithComparisonMethod, Expression.Constant f.Value, Expression.Constant comparison)
330+
| ValueNone ->
331+
Expression.Call (normalizeStringMemberExpr ``member``, StringEndsWithMethod, Expression.Constant f.Value)
304332

305-
| Contains f ->
333+
| Contains (f, comparer) ->
306334
let ``member`` = Expression.PropertyOrField (param, f.FieldName)
307335
let isEnumerable (memberType : Type) =
308336
not (Type.(=) (memberType, stringType))
@@ -338,7 +366,11 @@ module ObjectListFilter =
338366
| :? FieldInfo as field when field.FieldType |> isEnumerable -> callContains field.FieldType
339367
| _ ->
340368
let unwrappedValue = Helpers.unwrap f.Value
341-
Expression.Call (normalizeStringMemberExpr ``member``, StringContainsMethod, Expression.Constant unwrappedValue)
369+
match comparerToStringComparison comparer with
370+
| ValueSome comparison ->
371+
Expression.Call (normalizeStringMemberExpr ``member``, StringContainsWithComparisonMethod, Expression.Constant (unwrappedValue :?> string, typeof<string>), Expression.Constant comparison)
372+
| ValueNone ->
373+
Expression.Call (normalizeStringMemberExpr ``member``, StringContainsMethod, Expression.Constant unwrappedValue)
342374
| In f when not (f.Value.IsEmpty) ->
343375
let ``member`` = Expression.PropertyOrField (param, f.FieldName)
344376
let enumerableContains = getEnumerableContainsMethod objectType
@@ -351,18 +383,6 @@ module ObjectListFilter =
351383
| FilterField f ->
352384
let paramExpr = Expression.PropertyOrField (param, f.FieldName)
353385
buildFilterExpr isEnumerableQuery (SourceExpression paramExpr) buildTypeDiscriminatorCheck f.Value
354-
| EqualsCI f ->
355-
let ``member`` = Expression.PropertyOrField (param, f.FieldName)
356-
Expression.Call (normalizeStringMemberExpr ``member``, StringEqualsCIMethod, Expression.Constant f.Value, OrdinalIgnoreCase)
357-
| StartsWithCI f ->
358-
let ``member`` = Expression.PropertyOrField (param, f.FieldName)
359-
Expression.Call (normalizeStringMemberExpr ``member``, StringStartsWithCIMethod, Expression.Constant f.Value, OrdinalIgnoreCase)
360-
| EndsWithCI f ->
361-
let ``member`` = Expression.PropertyOrField (param, f.FieldName)
362-
Expression.Call (normalizeStringMemberExpr ``member``, StringEndsWithCIMethod, Expression.Constant f.Value, OrdinalIgnoreCase)
363-
| ContainsCI f ->
364-
let ``member`` = Expression.PropertyOrField (param, f.FieldName)
365-
Expression.Call (normalizeStringMemberExpr ``member``, StringContainsCIMethod, Expression.Constant f.Value, OrdinalIgnoreCase)
366386

367387
type private CompareDiscriminatorExpressionVisitor<'T, 'D>
368388
(compareDiscriminator : CompareDiscriminatorExpression<'T, 'D>, param : SourceExpression, value : obj) =

0 commit comments

Comments
 (0)