Skip to content

Fixed Contains and In operators implementation for collections #525

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 41 additions & 39 deletions src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type ObjectListFilter =
| In of FieldFilter<System.IComparable list>
| StartsWith of FieldFilter<string>
| EndsWith of FieldFilter<string>
| Contains of FieldFilter<string>
| Contains of FieldFilter<System.IComparable>
| OfTypes of Type list
| FilterField of FieldFilter<ObjectListFilter>

Expand Down Expand Up @@ -142,25 +142,27 @@ module ObjectListFilter =
let private StringStartsWithMethod = stringType.GetMethod ("StartsWith", [| stringType |])
let private StringEndsWithMethod = stringType.GetMethod ("EndsWith", [| stringType |])
let private StringContainsMethod = stringType.GetMethod ("Contains", [| stringType |])
let private getEnumerableContainsMethod (memberType : Type) =
let private getCollectionInstanceContainsMethod (memberType : Type) =
memberType
.GetMethods(BindingFlags.Instance ||| BindingFlags.Public)
.FirstOrDefault (fun m -> m.Name = "Contains" && m.GetParameters().Length = 1)
|> ValueOption.ofObj
let private getEnumerableContainsMethod (itemType : Type) =
match
typeof<Enumerable>
.GetMethods(BindingFlags.Static ||| BindingFlags.Public)
.FirstOrDefault (fun m -> m.Name = "Contains" && m.GetParameters().Length = 2)
with
| null -> raise (MissingMemberException "Static 'Contains' method with 2 parameters not found on 'Enumerable' class")
| containsGenericStaticMethod ->
if
memberType.IsGenericType
&& memberType.GenericTypeArguments.Length = 1
then
containsGenericStaticMethod.MakeGenericMethod (memberType.GenericTypeArguments)
else
let ienumerable =
memberType
.GetInterfaces()
.First (fun i -> i.FullName.StartsWith "System.Collections.Generic.IEnumerable`1")
containsGenericStaticMethod.MakeGenericMethod ([| ienumerable.GenericTypeArguments[0] |])
| containsGenericStaticMethod -> containsGenericStaticMethod.MakeGenericMethod ([| itemType |])
let private getEnumerableCastMethod (itemType : Type) =
match
typeof<Enumerable>
.GetMethods(BindingFlags.Static ||| BindingFlags.Public)
.FirstOrDefault (fun m -> m.Name = "Cast" && m.GetParameters().Length = 1)
with
| null -> raise (MissingMemberException "Static 'Cast' method with 1 parameter not found on 'Enumerable' class")
| castGenericStaticMethod -> castGenericStaticMethod.MakeGenericMethod ([| itemType |])

let getField (param : ParameterExpression) fieldName = Expression.PropertyOrField (param, fieldName)

Expand Down Expand Up @@ -204,38 +206,38 @@ module ObjectListFilter =
&& memberType
.GetInterfaces()
.Any (fun i -> i.FullName.StartsWith "System.Collections.Generic.IEnumerable`1")
let callContains memberType =
let itemType =
if ``member``.Type.IsArray then ``member``.Type.GetElementType()
else ``member``.Type.GetGenericArguments()[0]
let valueType =
match f.Value with
| null -> itemType
| value -> value.GetType()
let castedMember =
if itemType = valueType then ``member`` :> Expression
else
let castMethod = getEnumerableCastMethod valueType
Expression.Call (castMethod, ``member``)
match getCollectionInstanceContainsMethod memberType with
| ValueNone ->
let enumerableContains = getEnumerableContainsMethod valueType
Expression.Call (enumerableContains, castedMember, Expression.Constant (f.Value))
| ValueSome instanceContainsMethod ->
Expression.Call (castedMember, instanceContainsMethod, Expression.Constant (f.Value))
match ``member``.Member with
| :? PropertyInfo as prop when prop.PropertyType |> isEnumerable ->
match
prop.PropertyType
.GetMethods(BindingFlags.Instance ||| BindingFlags.Public)
.FirstOrDefault (fun m -> m.Name = "Contains" && m.GetParameters().Length = 1)
with
| null ->
Expression.Call (
getEnumerableContainsMethod prop.PropertyType,
Expression.PropertyOrField (param, f.FieldName),
Expression.Constant (f.Value)
)
| instanceContainsMethod ->
Expression.Call (Expression.PropertyOrField (param, f.FieldName), instanceContainsMethod, Expression.Constant (f.Value))
| :? FieldInfo as field when field.FieldType |> isEnumerable ->
Expression.Call (
getEnumerableContainsMethod field.FieldType,
Expression.PropertyOrField (param, f.FieldName),
Expression.Constant (f.Value)
)
| :? PropertyInfo as prop when prop.PropertyType |> isEnumerable -> callContains prop.PropertyType
| :? FieldInfo as field when field.FieldType |> isEnumerable -> callContains field.FieldType
| _ ->
if ``member``.Type = stringType then
Expression.Call (``member``, StringContainsMethod, Expression.Constant (f.Value))
else
Expression.Call (Expression.Convert (``member``, stringType), StringContainsMethod, Expression.Constant (f.Value))
| In f ->
| In f when not (f.Value.IsEmpty) ->
let ``member`` = Expression.PropertyOrField (param, f.FieldName)
f.Value
|> Seq.map (fun v -> Expression.Equal (``member``, Expression.Constant (v)))
|> Seq.reduce (fun acc expr -> Expression.OrElse (acc, expr))
:> Expression
let enumerableContains = getEnumerableContainsMethod typeof<IComparable>
Expression.Call (enumerableContains, Expression.Constant (f.Value), Expression.Convert (``member``, typeof<IComparable>))
| In f -> Expression.Constant (true)
| OfTypes types ->
types
|> Seq.map (fun t -> buildTypeDiscriminatorCheck param t)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ type ValidIntObject =
type FakeEntity = {
ValidStringStruct : ValidStringStruct
ValidStringObject : ValidStringObject
ValidStringStructList : ValidStringStruct list
ValidStringObjectList : ValidStringObject list
string : string
ValidIntStruct : ValidIntStruct
ValidIntObject : ValidIntObject
Expand Down Expand Up @@ -125,6 +127,23 @@ let ``ObjectListFilter works with Contains operator for ValidStringStruct`` () =
let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery
equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE CONTAINS(root["validStringStruct"], "athan")"""

[<Fact (Skip = "Cast not supported in Cosmos LINQ")>]
let ``ObjectListFilter works with Contains operator for ValidStringStruct list`` () =
let queryable = container.GetItemLinqQueryable<FakeEntity> ()
let filter = Contains { FieldName = "validStringStructList"; Value = "athan" }
let filterQuery = queryable.Apply (filter, filterOptions)
let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery
equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE ARRAY_CONTAINS(root["validStringStructList"], "athan")"""

[<Fact>]
let ``ObjectListFilter works with In operator for ValidStringStruct list`` () =
let queryable = container.GetItemLinqQueryable<FakeEntity> ()
let filter = In { FieldName = "validStringStruct"; Value = [ "athan"; "gaja" ] }
let filterQuery = queryable.Apply (filter, filterOptions)
let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery
equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE ARRAY_CONTAINS([ "athan", "gaja" ], root["validStringStruct"])"""


[<Fact>]
let ``ObjectListFilter works with Equals operator for ValidStringObject`` () =
let filter = Equals { FieldName = "validStringObject"; Value = ValidStringObject "Jonathan" }
Expand All @@ -133,6 +152,7 @@ let ``ObjectListFilter works with Equals operator for ValidStringObject`` () =
let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery
equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["validStringObject"] = "Jonathan")"""


[<Fact>]
let ``ObjectListFilter works with GreaterThan operator for ValidIntStruct`` () =
let queryable = container.GetItemLinqQueryable<FakeEntity> ()
Expand Down
21 changes: 21 additions & 0 deletions tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,27 @@ let ``ObjectListFilter works with IN operator for int type field`` () =
result.Contact |> equals { Email = "[email protected]" }
result.Friends |> equals [ { Email = "[email protected]" }; { Email = "[email protected]" } ]

[<Fact>]
let ``ObjectListFilter works with Contains operator for array type field`` () =
let filter = Contains { FieldName = "friends"; Value = { Email = "[email protected]" } }
let queryable = data.AsQueryable ()
let filteredData = queryable.Apply (filter) |> Seq.toList
List.length filteredData |> equals 2
do
let result = List.head filteredData
result.ID |> equals 4
result.FirstName |> equals "Ben"
result.LastName |> equals "Adams"
result.Contact |> equals { Email = "[email protected]" }
result.Friends |> equals [ { Email = "[email protected]" }; { Email = "[email protected]" } ]
do
let result = List.last filteredData
result.ID |> equals 7
result.FirstName |> equals "Jeneffer"
result.LastName |> equals "Trif"
result.Contact |> equals { Email = "[email protected]" }
result.Friends |> equals [ { Email = "[email protected]" } ]

[<Fact>]
let ``ObjectListFilter works with FilterField operator`` () =
let filter =
Expand Down