11namespace FSharp.Data.GraphQL.Server.Middleware
22
33open System
4+ open System.Collections
45open FSharp.Data .GraphQL
56
67/// A filter definition for a field value.
78type FieldFilter < 'Val > = { FieldName : string ; Value : 'Val }
89
10+ /// <summary >
911/// A filter definition for an object list.
12+ /// </summary >
13+ /// <remarks >
14+ /// String-based filters can carry a comparer. When the comparer is not provided by the default
15+ /// string operators, ` StartsWith ` , ` EndsWith ` , and string ` Contains ` preserve the existing
16+ /// case-sensitive ` StringComparison.CurrentCulture ` behavior.
17+ /// ` StringComparer.CurrentCultureIgnoreCase ` enables case-insensitive matching.
18+ /// When filters are provided through GraphQL input, lowercase string suffixes are interpreted
19+ /// as case-insensitive and capitalized suffixes are interpreted as case-sensitive.
20+ /// </remarks >
1021type ObjectListFilter =
1122 | And of ObjectListFilter * ObjectListFilter
1223 | Or of ObjectListFilter * ObjectListFilter
1324 | Not of ObjectListFilter
14- | Equals of FieldFilter < System. IComparable>
15- | GreaterThan of FieldFilter < System. IComparable>
16- | GreaterThanOrEqual of FieldFilter < System. IComparable>
17- | LessThan of FieldFilter < System. IComparable>
18- | LessThanOrEqual of FieldFilter < System. IComparable>
25+ | Equals of Filter : FieldFilter<IComparable> * Comparer : IComparer
26+ | GreaterThan of FieldFilter < IComparable >
27+ | GreaterThanOrEqual of FieldFilter < IComparable >
28+ | LessThan of FieldFilter < IComparable >
29+ | LessThanOrEqual of FieldFilter < IComparable >
1930 | In of FieldFilter < obj list >
20- | StartsWith of FieldFilter < string >
21- | EndsWith of FieldFilter < string >
22- | Contains of FieldFilter < System. IComparable>
31+ | StartsWith of Filter : FieldFilter<string> * Comparer : StringComparer
32+ | EndsWith of Filter : FieldFilter<string> * Comparer : StringComparer
33+ | Contains of Filter : FieldFilter<IComparable> * Comparer : IComparer
2334 | OfTypes of Type list
2435 | FilterField of FieldFilter < ObjectListFilter >
2536
@@ -95,7 +106,7 @@ module ObjectListFilter =
95106 let ( ||| ) x y = Or ( x, y)
96107
97108 /// Creates a new ObjectListFilter representing an EQUALS operation between two comparable values.
98- let ( = == ) fname value = Equals { FieldName = fname; Value = value }
109+ let ( = == ) fname value = Equals ( { FieldName = fname; Value = value }, null )
99110
100111 /// Creates a new ObjectListFilter representing a GREATER THAN operation of a comparable value.
101112 let ( >>> ) fname value = GreaterThan { FieldName = fname; Value = value }
@@ -110,23 +121,35 @@ module ObjectListFilter =
110121 let ( <== ) fname value = LessThanOrEqual { FieldName = fname ; Value = value }
111122
112123 /// Creates a new ObjectListFilter representing a STARTS WITH operation of a string value.
113- let ( =@@ ) fname value = StartsWith { FieldName = fname ; Value = value }
124+ let ( =@@ ) fname value = StartsWith ( { FieldName = fname ; Value = value }, null )
114125
115126 /// Creates a new ObjectListFilter representing an ENDS WITH operation of a string value.
116- let ( @@= ) fname value = EndsWith { FieldName = fname ; Value = value }
127+ let ( @@= ) fname value = EndsWith ( { FieldName = fname ; Value = value }, null )
117128
118129 /// Creates a new ObjectListFilter representing a CONTAINS operation.
119- let ( @=@ ) fname value = Contains { FieldName = fname ; Value = value }
130+ let ( @=@ ) fname value = Contains ( { FieldName = fname ; Value = value }, null )
120131
121132 /// Creates a new ObjectListFilter representing a IN operation.
122133 let ( = ~= ) fname value = In { FieldName = fname ; Value = value }
123134
124135 /// Creates a new ObjectListFilter representing a field sub comparison.
125136 let ( - -> ) fname filter = FilterField { FieldName = fname ; Value = filter }
126137
127- /// Creates a new ObjectListFilter representing a NOT opreation for the existing one.
138+ /// Creates a new ObjectListFilter representing a NOT operation for the existing one.
128139 let ( !!! ) filter = Not filter
129140
141+ /// Creates a new ObjectListFilter representing a case - insensitive EQUALS operation on a string value.
142+ let ( === ~ ) fname ( value : string ) = Equals ({ FieldName = fname ; Value = value }, StringComparer.CurrentCultureIgnoreCase )
143+
144+ /// Creates a new ObjectListFilter representing a case - insensitive STARTS WITH operation on a string value.
145+ let ( =@@ ~ ) fname ( value : string ) = StartsWith ({ FieldName = fname ; Value = value }, StringComparer.CurrentCultureIgnoreCase )
146+
147+ /// Creates a new ObjectListFilter representing a case - insensitive ENDS WITH operation on a string value.
148+ let ( @@= ~ ) fname ( value : string ) = EndsWith ({ FieldName = fname ; Value = value }, StringComparer.CurrentCultureIgnoreCase )
149+
150+ /// Creates a new ObjectListFilter representing a case - insensitive CONTAINS operation on a string value.
151+ let ( @=@ ~ ) fname ( value : string ) = Contains ({ FieldName = fname ; Value = value }, StringComparer.CurrentCultureIgnoreCase )
152+
130153 let private genericWhereMethod =
131154 typeof < Queryable > .GetMethods ()
132155 |> Seq.where ( fun m -> m.Name = " Where" )
@@ -144,9 +167,11 @@ module ObjectListFilter =
144167 let private stringType = typeof< string>
145168 let private genericIEnumerableType = typedefof< IEnumerable<_>>
146169
147- let private StringStartsWithMethod = stringType.GetMethod ( " StartsWith" , [| stringType |])
148- let private StringEndsWithMethod = stringType.GetMethod ( " EndsWith" , [| stringType |])
149- let private StringContainsMethod = stringType.GetMethod ( " Contains" , [| stringType |])
170+ let private stringComparisonType = typeof< StringComparison>
171+ let private StringStartsWithMethod = stringType.GetMethod ( " StartsWith" , [| stringType; stringComparisonType |])
172+ let private StringEndsWithMethod = stringType.GetMethod ( " EndsWith" , [| stringType; stringComparisonType |])
173+ let private StringContainsMethod = stringType.GetMethod ( " Contains" , [| stringType; stringComparisonType |])
174+ let private StringEqualsMethod = stringType.GetMethod ( " Equals" , [| stringType; stringComparisonType |])
150175 let private unwrapOptionMethod =
151176 FSharp.Data.GraphQL.Helpers.moduleType.GetMethod ( nameof Helpers.unwrap)
152177
@@ -205,6 +230,21 @@ module ObjectListFilter =
205230 |> Seq.where ( fun m -> m.Name = " Equals" )
206231 |> Seq.head
207232
233+ /// Maps an IComparer to a StringComparison value.
234+ /// Returns ValueNone only when the comparer is null or is not a recognized StringComparer.
235+ let private comparerToStringComparison ( comparer : IComparer ) =
236+ match comparer with
237+ | null -> ValueNone
238+ | :? StringComparer as sc ->
239+ if obj.ReferenceEquals ( sc, StringComparer.OrdinalIgnoreCase) then ValueSome StringComparison.OrdinalIgnoreCase
240+ elif obj.ReferenceEquals ( sc, StringComparer.InvariantCultureIgnoreCase) then ValueSome StringComparison.InvariantCultureIgnoreCase
241+ elif obj.ReferenceEquals ( sc, StringComparer.CurrentCultureIgnoreCase) then ValueSome StringComparison.CurrentCultureIgnoreCase
242+ elif obj.ReferenceEquals ( sc, StringComparer.Ordinal) then ValueSome StringComparison.Ordinal
243+ elif obj.ReferenceEquals ( sc, StringComparer.InvariantCulture) then ValueSome StringComparison.InvariantCulture
244+ elif obj.ReferenceEquals ( sc, StringComparer.CurrentCulture) then ValueSome StringComparison.CurrentCulture
245+ else ValueNone
246+ | _ -> ValueNone
247+
208248 let rec buildFilterExpr isEnumerableQuery ( param : SourceExpression ) buildTypeDiscriminatorCheck filter : Expression =
209249
210250 let build = buildFilterExpr isEnumerableQuery param buildTypeDiscriminatorCheck
@@ -224,31 +264,41 @@ module ObjectListFilter =
224264 | _ -> Expression.Convert ( `` member `` , stringType)
225265
226266 match filter with
227- | Not ( Equals f ) ->
267+ | Not ( Equals ( f , comparer ) ) ->
228268 let ``member`` = Expression.PropertyOrField ( param, f.FieldName)
229- let hasEqualityOperator = hasEqualityOperator `` member `` .Type
230- match f.Value with
231- | NoCast when hasEqualityOperator -> Expression.NotEqual ( `` member `` , Expression.Constant f.Value)
232- | NoCast
233- | NonEnumerableCast _ ->
234- Expression.NotEqual ( Expression.Convert ( `` member `` , objectType), Expression.Convert (( Expression.Constant f.Value), objectType))
235- | Enumerable ->
236- let ``const`` = Expression.Constant ( Values.normalizeOptional `` member `` .Type f.Value)
237- Expression.Not ( Expression.Call ( `` const `` , equalsMethod, `` member `` ))
269+ match comparerToStringComparison comparer with
270+ | ValueSome comparison ->
271+ let value = Helpers.unwrap ( box f.Value) :?> string
272+ Expression.Not ( Expression.Call ( normalizeStringMemberExpr `` member `` , StringEqualsMethod, Expression.Constant ( value, typeof< string>), Expression.Constant comparison)) :> Expression
273+ | ValueNone ->
274+ let hasEqualityOperator = hasEqualityOperator `` member `` .Type
275+ match f.Value with
276+ | NoCast when hasEqualityOperator -> Expression.NotEqual ( `` member `` , Expression.Constant f.Value)
277+ | NoCast
278+ | NonEnumerableCast _ ->
279+ Expression.NotEqual ( Expression.Convert ( `` member `` , objectType), Expression.Convert (( Expression.Constant f.Value), objectType))
280+ | Enumerable ->
281+ let ``const`` = Expression.Constant ( Values.normalizeOptional `` member `` .Type f.Value)
282+ Expression.Not ( Expression.Call ( `` const `` , equalsMethod, `` member `` ))
238283 | Not f -> f |> build |> Expression.Not :> Expression
239284 | And ( f1, f2) -> Expression.AndAlso ( build f1, build f2)
240285 | Or ( f1, f2) -> Expression.OrElse ( build f1, build f2)
241- | Equals f ->
286+ | Equals ( f , comparer ) ->
242287 let ``member`` = Expression.PropertyOrField ( param, f.FieldName)
243- let hasEqualityOperator = hasEqualityOperator `` member `` .Type
244- match f.Value with
245- | NoCast when hasEqualityOperator -> Expression.Equal ( `` member `` , Expression.Constant f.Value)
246- | NoCast
247- | NonEnumerableCast _ ->
248- Expression.Equal ( Expression.Convert ( `` member `` , objectType), Expression.Convert (( Expression.Constant f.Value), objectType))
249- | Enumerable ->
250- let ``const`` = Expression.Constant ( Values.normalizeOptional `` member `` .Type f.Value)
251- Expression.Call ( `` const `` , equalsMethod, `` member `` )
288+ match comparerToStringComparison comparer with
289+ | ValueSome comparison ->
290+ let value = Helpers.unwrap ( box f.Value) :?> string
291+ Expression.Call ( normalizeStringMemberExpr `` member `` , StringEqualsMethod, Expression.Constant ( value, typeof< string>), Expression.Constant comparison) :> Expression
292+ | ValueNone ->
293+ let hasEqualityOperator = hasEqualityOperator `` member `` .Type
294+ match f.Value with
295+ | NoCast when hasEqualityOperator -> Expression.Equal ( `` member `` , Expression.Constant f.Value)
296+ | NoCast
297+ | NonEnumerableCast _ ->
298+ Expression.Equal ( Expression.Convert ( `` member `` , objectType), Expression.Convert (( Expression.Constant f.Value), objectType))
299+ | Enumerable ->
300+ let ``const`` = Expression.Constant ( Values.normalizeOptional `` member `` .Type f.Value)
301+ Expression.Call ( `` const `` , equalsMethod, `` member `` )
252302 | GreaterThan f ->
253303 let ``member`` = Expression.PropertyOrField ( param, f.FieldName)
254304 match f.Value with
@@ -273,14 +323,16 @@ module ObjectListFilter =
273323 | NoCast -> Expression.LessThanOrEqual ( `` member `` , Expression.Constant f.Value)
274324 | Enumerable -> Expression.LessThanOrEqual ( `` member `` , Expression.Constant ( Values.normalizeOptional `` member `` .Type f.Value))
275325 | NonEnumerableCast `` type `` -> Expression.LessThanOrEqual (( unsafeConvertTo `` type `` `` member `` ), Expression.Constant f.Value)
276- | StartsWith f ->
326+ | StartsWith ( f , comparer ) ->
277327 let ``member`` = Expression.PropertyOrField ( param, f.FieldName)
278- Expression.Call ( normalizeStringMemberExpr `` member `` , StringStartsWithMethod, Expression.Constant f.Value)
279- | EndsWith f ->
328+ let comparison = comparerToStringComparison ( comparer :> IComparer) |> ValueOption.defaultValue StringComparison.CurrentCulture
329+ Expression.Call ( normalizeStringMemberExpr `` member `` , StringStartsWithMethod, Expression.Constant f.Value, Expression.Constant comparison)
330+ | EndsWith ( f, comparer) ->
280331 let ``member`` = Expression.PropertyOrField ( param, f.FieldName)
281- Expression.Call ( normalizeStringMemberExpr `` member `` , StringEndsWithMethod, Expression.Constant f.Value)
332+ let comparison = comparerToStringComparison ( comparer :> IComparer) |> ValueOption.defaultValue StringComparison.CurrentCulture
333+ Expression.Call ( normalizeStringMemberExpr `` member `` , StringEndsWithMethod, Expression.Constant f.Value, Expression.Constant comparison)
282334
283- | Contains f ->
335+ | Contains ( f , comparer ) ->
284336 let ``member`` = Expression.PropertyOrField ( param, f.FieldName)
285337 let isEnumerable ( memberType : Type ) =
286338 not ( Type.(=) ( memberType, stringType))
@@ -316,7 +368,8 @@ module ObjectListFilter =
316368 | :? FieldInfo as field when field.FieldType |> isEnumerable -> callContains field.FieldType
317369 | _ ->
318370 let unwrappedValue = Helpers.unwrap f.Value
319- Expression.Call ( normalizeStringMemberExpr `` member `` , StringContainsMethod, Expression.Constant unwrappedValue)
371+ let comparison = comparerToStringComparison comparer |> ValueOption.defaultValue StringComparison.CurrentCulture
372+ Expression.Call ( normalizeStringMemberExpr `` member `` , StringContainsMethod, Expression.Constant ( unwrappedValue :?> string, typeof< string>), Expression.Constant comparison)
320373 | In f when not ( f.Value.IsEmpty) ->
321374 let ``member`` = Expression.PropertyOrField ( param, f.FieldName)
322375 let enumerableContains = getEnumerableContainsMethod objectType
0 commit comments