From c4860f315c7a21da4b69dadcb0e4ded8d7d08c2f Mon Sep 17 00:00:00 2001 From: Viktor Tochonov Date: Fri, 7 Mar 2025 23:19:08 +0200 Subject: [PATCH 01/14] Implemented ObjectListFilter tests for following cases: And, Equals, GreaterThan, LessThan, StartsWith, EndsWith, Contains --- .../ObjectListFilter.fs | 278 +++++++++++------- .../FSharp.Data.GraphQL.Shared.fsproj | 3 + tests/FSharp.Data.GraphQL.Tests/LinqTests.fs | 129 ++++++++ 3 files changed, 297 insertions(+), 113 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs index e46edb7e0..472b60233 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs @@ -60,116 +60,168 @@ module ObjectListFilter = /// Creates a new ObjectListFilter representing a NOT opreation for the existing one. let ( !!! ) filter = Not filter -//[] -//module ObjectListFilterExtensions = - -// type ObjectListFilter with - -// member filter.Apply<'T, 'D>(query : IQueryable<'T>, -// compareDiscriminator : Expr<'T -> 'D -> 'D> | null, -// getDiscriminatorValue : (Type -> 'D) | null) = -// filter.Apply(query, compareDiscriminator, getDiscriminatorValue) - -// member filter.Apply<'T, 'D>(query : IQueryable<'T>, -// [] getDiscriminator : Expr<'T -> 'D> | null, -// [] getDiscriminatorValue : (Type -> 'D) | null) = -// // Helper to create parameter expression for the lambda -// let param = Expression.Parameter(typeof<'T>, "x") - -// // Helper to get property value -// let getPropertyExpr fieldName = -// Expression.PropertyOrField(param, fieldName) - -// // Helper to create lambda from body expression -// let makeLambda (body: Expression) = -// let delegateType = typedefof>.MakeGenericType([|typeof<'T>; body.Type|]) -// Expression.Lambda(delegateType, body, param) - -// // Helper to create Where expression -// let whereExpr predicate = -// let whereMethod = -// typeof.GetMethods() -// |> Seq.where (fun m -> m.Name = "Where") -// |> Seq.find (fun m -> -// let parameters = m.GetParameters() -// parameters.Length = 2 -// && parameters[1].ParameterType.GetGenericTypeDefinition() = typedefof>>) -// |> fun m -> m.MakeGenericMethod([|typeof<'T>|]) -// Expression.Call(whereMethod, [|query.Expression; makeLambda predicate|]) - -// // Helper for discriminator comparison -// let buildTypeDiscriminatorCheck (t: Type) = -// match getDiscriminator, getDiscriminatorValue with -// | null, _ | _, null -> None -// | discExpr, discValueFn -> -// let compiled = QuotationEvaluator.Eval(discExpr) -// let discriminatorValue = discValueFn t -// let discExpr = getPropertyExpr "__discriminator" // Assuming discriminator field name -// let valueExpr = Expression.Constant(discriminatorValue) -// Some(Expression.Equal(discExpr, valueExpr)) - -// // Main filter logic -// let rec buildFilterExpr filter = -// match filter with -// | NoFilter -> query.Expression -// | And (f1, f2) -> -// let q1 = buildFilterExpr f1 |> Expression.Lambda>>|> _.Compile().Invoke() -// buildFilterExpr f2 |> Expression.Lambda>> |> _.Compile().Invoke(q1).Expression -// | Or (f1, f2) -> -// let expr1 = buildFilterExpr f1 -// let expr2 = buildFilterExpr f2 -// let unionMethod = -// typeof.GetMethods() -// |> Array.find (fun m -> m.Name = "Union") -// |> fun m -> m.MakeGenericMethod([|typeof<'T>|]) -// Expression.Call(unionMethod, [|expr1; expr2|]) -// | Not f -> -// let exceptMethod = -// typeof.GetMethods() -// |> Array.find (fun m -> m.Name = "Except") -// |> fun m -> m.MakeGenericMethod([|typeof<'T>|]) -// Expression.Call(exceptMethod, [|query.Expression; buildFilterExpr f|]) -// | Equals f -> -// Expression.Equal(getPropertyExpr f.FieldName, Expression.Constant(f.Value)) |> whereExpr -// | GreaterThan f -> -// Expression.GreaterThan(getPropertyExpr f.FieldName, Expression.Constant(f.Value)) |> whereExpr -// | LessThan f -> -// Expression.LessThan(getPropertyExpr f.FieldName, Expression.Constant(f.Value)) |> whereExpr -// | StartsWith f -> -// let methodInfo = typeof.GetMethod("StartsWith", [|typeof|]) -// Expression.Call(getPropertyExpr f.FieldName, methodInfo, Expression.Constant(f.Value)) |> whereExpr -// | EndsWith f -> -// let methodInfo = typeof.GetMethod("EndsWith", [|typeof|]) -// Expression.Call(getPropertyExpr f.FieldName, methodInfo, Expression.Constant(f.Value)) |> whereExpr -// | Contains f -> -// let methodInfo = typeof.GetMethod("Contains", [|typeof|]) -// Expression.Call(getPropertyExpr f.FieldName, methodInfo, Expression.Constant(f.Value)) |> whereExpr -// | OfTypes types -> -// match types.Value with -// | [] -> query.Expression // No types specified, return original query -// | types -> -// let typeChecks = -// types -// |> List.choose buildTypeDiscriminatorCheck -// |> List.fold (fun acc expr -> -// match acc with -// | None -> Some expr -// | Some prevExpr -> Some(Expression.OrElse(prevExpr, expr))) None - -// match typeChecks with -// | None -> query.Expression -// | Some expr -> whereExpr expr -// | FilterField f -> -// let propExpr = getPropertyExpr f.FieldName -// match propExpr.Type.GetInterfaces() -// |> Array.tryFind (fun t -> -// t.IsGenericType && t.GetGenericTypeDefinition() = typedefof>) with -// | Some queryableType -> -// let elementType = queryableType.GetGenericArguments().[0] -// let subFilter = f.Value -// let subQuery = Expression.Convert(propExpr, queryableType) -// Expression.Call(typeof, "Any", [|elementType|], subQuery) |> whereExpr -// | None -> query.Expression - -// // Create and execute the final expression -// query.Provider.CreateQuery<'T>(buildFilterExpr filter) +[] +module ObjectListFilterExtensions = + + //// discriminator custom condition + //let a () = + // filter.Apply( + // queryable, + // <@ fun e d -> e.Discriminator.StartsWith d @>, + // function + // | t when Type.(=)(t, typeof) -> ResidentialPropertiesConstants.Discriminators.Complex + // | t when Type.(=)(t, typeof) -> ResidentialPropertiesConstants.Discriminators.Building + // ) + //// discriminator equals + //let b () = + // filter.Apply( + // queryable, + // <@ fun e -> e.Discriminator @>, + // function + // | t when Type.(=)(t, typeof) -> ResidentialPropertiesConstants.Discriminators.Complex + // | t when Type.(=)(t, typeof) -> ResidentialPropertiesConstants.Discriminators.Building + // ) + + // Helper to create parameter expression for the lambda + let param<'T> = Expression.Parameter(typeof<'T>, "x") + + // Helper to get property value + let getPropertyExpr<'T> (param: ParameterExpression) fieldName = + Expression.PropertyOrField(param, fieldName) + + // Helper to create lambda from body expression + let makeLambda<'T> (param: ParameterExpression) (body: Expression) = + let delegateType = typedefof>.MakeGenericType([|typeof<'T>; body.Type|]) + Expression.Lambda(delegateType, body, param) + + // Helper to create Where expression + let whereExpr<'T> (query : IQueryable<'T>) (param: ParameterExpression) predicate = + let whereMethod = + typeof.GetMethods() + |> Seq.where (fun m -> m.Name = "Where") + |> Seq.find (fun m -> + let parameters = m.GetParameters() + parameters.Length = 2 + && parameters[1].ParameterType.GetGenericTypeDefinition() = typedefof>>) + |> fun m -> m.MakeGenericMethod([|typeof<'T>|]) + Expression.Call(whereMethod, [|query.Expression; makeLambda<'T> param predicate|]) + + // Main filter logic + let rec buildFilterExpr<'T> (param: ParameterExpression) buildTypeDiscriminatorCheck filter (query : IQueryable<'T>) = + let buildFilterExpr = buildFilterExpr<'T> param buildTypeDiscriminatorCheck + match filter with + | NoFilter -> query.Expression + | And (f1, f2) -> + let q1 = buildFilterExpr f1 query |> Expression.Lambda>> |> _.Compile().Invoke() + let q2 = buildFilterExpr f2 q1 |> Expression.Lambda>> |> _.Compile().Invoke() + q2.Expression + //| Or (f1, f2) -> + // let expr1 = buildFilterExpr f1 query + // let expr2 = buildFilterExpr f2 query + // Expression.OrElse(expr1, expr2) |> whereExpr<'T> query param :> Expression + //| Not f -> + // let exceptMethod = + // typeof.GetMethods() + // |> Array.find (fun m -> m.Name = "Except") + // |> fun m -> m.MakeGenericMethod([|typeof<'T>|]) + // Expression.Call(exceptMethod, [|query.Expression; buildFilterExpr f|]) + //| OfTypes types -> + // match types.Value with + // | [] -> query.Expression // No types specified, return original query + // | types -> + // let typeChecks = + // types + // |> List.vchoose buildTypeDiscriminatorCheck + // |> List.fold (fun acc expr -> + // match acc with + // | ValueNone -> ValueSome expr + // | ValueSome prevExpr -> ValueSome (Expression.OrElse(prevExpr, expr))) ValueNone + + // match typeChecks with + // | ValueNone -> query.Expression + // | ValueSome expr -> whereExpr query expr :> Expression + | Equals f -> + Expression.Equal(getPropertyExpr<'T> param f.FieldName, Expression.Constant(f.Value)) |> whereExpr<'T> query param :> Expression + | GreaterThan f -> + Expression.GreaterThan(getPropertyExpr<'T> param f.FieldName, Expression.Constant(f.Value)) |> whereExpr<'T> query param :> Expression + | LessThan f -> + Expression.LessThan(getPropertyExpr<'T> param f.FieldName, Expression.Constant(f.Value)) |> whereExpr<'T> query param :> Expression + | StartsWith f -> + let methodInfo = typeof.GetMethod("StartsWith", [|typeof|]) + Expression.Call(getPropertyExpr<'T> param f.FieldName, methodInfo, Expression.Constant(f.Value)) |> whereExpr<'T> query param :> Expression + | EndsWith f -> + let methodInfo = typeof.GetMethod("EndsWith", [|typeof|]) + Expression.Call(getPropertyExpr<'T> param f.FieldName, methodInfo, Expression.Constant(f.Value)) |> whereExpr<'T> query param :> Expression + | Contains f -> + let methodInfo = typeof.GetMethod("Contains", [|typeof|]) + Expression.Call(getPropertyExpr<'T> param f.FieldName, methodInfo, Expression.Constant(f.Value)) |> whereExpr<'T> query param :> Expression + | FilterField f -> + let propExpr = getPropertyExpr<'T> param f.FieldName + match propExpr.Type.GetInterfaces() + |> Array.tryFind (fun t -> + t.IsGenericType && t.GetGenericTypeDefinition() = typedefof>) with + | Some queryableType -> + let elementType = queryableType.GetGenericArguments().[0] + let subFilter = f.Value + let subQuery = Expression.Convert(propExpr, queryableType) + Expression.Call(typeof, "Any", [|elementType|], subQuery) |> whereExpr<'T> query param :> Expression + | None -> query.Expression + +type ObjectListFilter with + + member filter.Apply<'T, 'D>(query : IQueryable<'T>, + compareDiscriminator : Expression>, + getDiscriminatorValue : (Type -> 'D)) = + + // Helper for discriminator comparison + let buildTypeDiscriminatorCheck (t: Type) = + match compareDiscriminator, getDiscriminatorValue with + | null, discValueFn when obj.Equals(discValueFn, null) -> + // use __typename from filter and do type.ToSting() for values + ValueNone + | discExpr, discValueFn when obj.Equals(discValueFn, null) -> + // use discriminator and do type.ToSting() for values + ValueNone + | null, discValueFn -> + // use __typename from filter and execute discValueFn for values + ValueNone + | discExpr, discValueFn -> + // use discriminator and execute discValueFn for values + + let discriminatorValue = discValueFn t + let param = Expression.Parameter(typeof<'T>, "x") + let discExpr = getPropertyExpr<'T> param "__discriminator" // Assuming discriminator field name + let valueExpr = Expression.Constant(discriminatorValue) + ValueSome(Expression.Equal(discExpr, valueExpr)) + + // Create and execute the final expression + let param = Expression.Parameter(typeof<'T>, "x") + query.Provider.CreateQuery<'T>(buildFilterExpr<'T> param buildTypeDiscriminatorCheck filter query) + + member filter.Apply<'T, 'D>(query : IQueryable<'T>, + [] getDiscriminator : Expression> | null, + [] getDiscriminatorValue : Type -> 'D) = + + // Helper for discriminator comparison + let buildTypeDiscriminatorCheck (t: Type) = + match getDiscriminator, getDiscriminatorValue with + | null, discValueFn when obj.Equals(discValueFn, null) -> + // use __typename from filter and do type.ToSting() for values + ValueNone + | discExpr, discValueFn when obj.Equals(discValueFn, null) -> + // use discriminator and do type.ToSting() for values + ValueNone + | null, discValueFn -> + // use __typename from filter and execute discValueFn for values + ValueNone + | discExpr, discValueFn -> + // use discriminator and execute discValueFn for values + let discriminatorValue = discValueFn t + let param = Expression.Parameter(typeof<'T>, "x") + let discExpr = getPropertyExpr<'T> param "__discriminator" // Assuming discriminator field name + let valueExpr = Expression.Constant(discriminatorValue) + ValueSome(Expression.Equal(discExpr, valueExpr)) + + // Create and execute the final expression + let param = Expression.Parameter(typeof<'T>, "x") + query.Provider.CreateQuery<'T>(buildFilterExpr<'T> param buildTypeDiscriminatorCheck filter query) diff --git a/src/FSharp.Data.GraphQL.Shared/FSharp.Data.GraphQL.Shared.fsproj b/src/FSharp.Data.GraphQL.Shared/FSharp.Data.GraphQL.Shared.fsproj index 8d270a060..39291f9de 100644 --- a/src/FSharp.Data.GraphQL.Shared/FSharp.Data.GraphQL.Shared.fsproj +++ b/src/FSharp.Data.GraphQL.Shared/FSharp.Data.GraphQL.Shared.fsproj @@ -17,6 +17,9 @@ <_Parameter1>FSharp.Data.GraphQL.Server + + <_Parameter1>FSharp.Data.GraphQL.Server.Middleware + <_Parameter1>FSharp.Data.GraphQL.Client diff --git a/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs b/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs index 0b1ee1dee..cdd4a47b9 100644 --- a/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs @@ -9,6 +9,9 @@ open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Types open FSharp.Data.GraphQL.Linq open FSharp.Data.GraphQL.Execution +open FSharp.Data.GraphQL.Server.Middleware +open FSharp.Data.GraphQL.Server.Middleware.ObjectListFilter +open FSharp.Data.GraphQL.Server.Middleware.ObjectListFilterExtensions type Contact = { Email : string } @@ -286,3 +289,129 @@ let ``LINQ interpreter works with orderByDesc arg``() = (7, "Jeneffer") (4, "Ben") ] +[] +let ``ObjectListFilter works with Equals operator``() = + let filter = Equals { FieldName = "firstName"; Value = "Jonathan" } // :> IComparable + let queryable = data.AsQueryable() + let filteredData = filter.Apply(queryable) |> Seq.toList + List.length filteredData |> equals 1 + let result = List.head filteredData + result.ID |> equals 2 + result.FirstName |> equals "Jonathan" + result.LastName |> equals "Abrams" + result.Contact |> equals { Email = "j.abrams@gmail.com" } + result.Friends |> equals [] + +[] +let ``ObjectListFilter works with GreaterThan operator``() = + let filter = GreaterThan { FieldName = "id"; Value = 4 } // :> IComparable + let queryable = data.AsQueryable() + let filteredData = filter.Apply(queryable) |> Seq.toList + List.length filteredData |> equals 1 + let result = List.head filteredData + result.ID |> equals 7 + result.FirstName |> equals "Jeneffer" + result.LastName |> equals "Trif" + result.Contact |> equals { Email = "j.trif@gmail.com" } + result.Friends |> equals [ { Email = "j.abrams@gmail.com" } ] + +[] +let ``ObjectListFilter works with LessThan operator``() = + let filter = LessThan { FieldName = "id"; Value = 4 } // :> IComparable + let queryable = data.AsQueryable() + let filteredData = filter.Apply(queryable) |> Seq.toList + List.length filteredData |> equals 1 + let result = List.head filteredData + result.ID |> equals 2 + result.FirstName |> equals "Jonathan" + result.LastName |> equals "Abrams" + result.Contact |> equals { Email = "j.abrams@gmail.com" } + result.Friends |> equals [] + +[] +let ``ObjectListFilter works with StartsWith operator``() = + let filter = StartsWith { FieldName = "firstName"; Value = "J" } + let queryable = data.AsQueryable() + let filteredData = filter.Apply(queryable) |> Seq.toList + List.length filteredData |> equals 2 + let result = List.head filteredData + result.ID |> equals 2 + result.FirstName |> equals "Jonathan" + result.LastName |> equals "Abrams" + result.Contact |> equals { Email = "j.abrams@gmail.com" } + result.Friends |> equals [] + +[] +let ``ObjectListFilter works with Contains operator``() = + let filter = Contains { FieldName = "firstName"; Value = "en" } + let queryable = data.AsQueryable() + let filteredData = filter.Apply(queryable) |> Seq.toList + List.length filteredData |> equals 2 + let result = List.head filteredData + result.ID |> equals 4 + result.FirstName |> equals "Ben" + result.LastName |> equals "Adams" + result.Contact |> equals { Email = "b.adams@gmail.com" } + result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] + +[] +let ``ObjectListFilter works with EndsWith operator``() = + let filter = EndsWith { FieldName = "lastName"; Value = "ams" } + let queryable = data.AsQueryable() + let filteredData = filter.Apply(queryable) |> Seq.toList + List.length filteredData |> equals 2 + let result = List.head filteredData + result.ID |> equals 4 + result.FirstName |> equals "Ben" + result.LastName |> equals "Adams" + result.Contact |> equals { Email = "b.adams@gmail.com" } + result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] + +[] +let ``ObjectListFilter works with AND operator``() = + let filter = + And ( + Contains { FieldName = "firstName"; Value = "en" }, + Equals { FieldName = "lastName"; Value = "Adams" } + ) + let queryable = data.AsQueryable() + let filteredData = filter.Apply(queryable) |> Seq.toList + List.length filteredData |> equals 1 + let result = List.head filteredData + result.ID |> equals 4 + result.FirstName |> equals "Ben" + result.LastName |> equals "Adams" + result.Contact |> equals { Email = "b.adams@gmail.com" } + result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] + +//[] +//let ``ObjectListFilter works with OR operator``() = +// let filter = +// Or ( +// GreaterThan { FieldName = "id"; Value = 4 }, +// Equals { FieldName = "lastName"; Value = "Adams" } +// ) +// let queryable = data.AsQueryable() +// let filteredData = filter.Apply(queryable) |> Seq.toList +// List.length filteredData |> equals 2 +// let result = List.head filteredData +// result.ID |> equals 4 +// result.FirstName |> equals "Ben" +// result.LastName |> equals "Adams" +// result.Contact |> equals { Email = "b.adams@gmail.com" } +// result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] + +//[] +//let ``ObjectListFilter works with FilterField operator``() = +// let filter = +// FilterField { FieldName = "Friends"; Value = Contains { FieldName = "Email"; Value = "l.trif@gmail.com" } } +// let queryable = data.AsQueryable() +// let filteredData = filter.Apply(queryable) |> Seq.toList +// List.length filteredData |> equals 1 +// let result = List.head filteredData +// result.ID |> equals 4 +// result.FirstName |> equals "Ben" +// result.LastName |> equals "Adams" +// result.Contact |> equals { Email = "b.adams@gmail.com" } +// result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] + From 51ef7f88e60adec2732913b6ef5f00ed9d4e2af6 Mon Sep 17 00:00:00 2001 From: Viktor Tochonov Date: Mon, 10 Mar 2025 17:02:33 +0200 Subject: [PATCH 02/14] Refactored ObjectListFilter and implemented Or and Not cases tests --- .../ObjectListFilter.fs | 248 ++++++++---------- tests/FSharp.Data.GraphQL.Tests/LinqTests.fs | 46 ++-- 2 files changed, 142 insertions(+), 152 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs index 472b60233..e386bcff8 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs @@ -4,12 +4,9 @@ open System open System.Linq open System.Linq.Expressions open System.Runtime.InteropServices -open Microsoft.FSharp.Quotations /// A filter definition for a field value. -type FieldFilter<'Val> = - { FieldName : string - Value : 'Val } +type FieldFilter<'Val> = { FieldName : string; Value : 'Val } /// A filter definition for an object list. type ObjectListFilter = @@ -82,146 +79,125 @@ module ObjectListFilterExtensions = // | t when Type.(=)(t, typeof) -> ResidentialPropertiesConstants.Discriminators.Building // ) - // Helper to create parameter expression for the lambda - let param<'T> = Expression.Parameter(typeof<'T>, "x") - - // Helper to get property value - let getPropertyExpr<'T> (param: ParameterExpression) fieldName = - Expression.PropertyOrField(param, fieldName) - // Helper to create lambda from body expression - let makeLambda<'T> (param: ParameterExpression) (body: Expression) = - let delegateType = typedefof>.MakeGenericType([|typeof<'T>; body.Type|]) - Expression.Lambda(delegateType, body, param) + let makeLambda<'T> (param : ParameterExpression) (body : Expression) = + let delegateType = typedefof>.MakeGenericType ([| typeof<'T>; body.Type |]) + Expression.Lambda (delegateType, body, param) + + let private genericWhereMethod = + typeof.GetMethods () + |> Seq.where (fun m -> m.Name = "Where") + |> Seq.find (fun m -> + let parameters = m.GetParameters () + parameters.Length = 2 + && parameters[1].ParameterType.GetGenericTypeDefinition () = typedefof>>) // Helper to create Where expression - let whereExpr<'T> (query : IQueryable<'T>) (param: ParameterExpression) predicate = - let whereMethod = - typeof.GetMethods() - |> Seq.where (fun m -> m.Name = "Where") - |> Seq.find (fun m -> - let parameters = m.GetParameters() - parameters.Length = 2 - && parameters[1].ParameterType.GetGenericTypeDefinition() = typedefof>>) - |> fun m -> m.MakeGenericMethod([|typeof<'T>|]) - Expression.Call(whereMethod, [|query.Expression; makeLambda<'T> param predicate|]) - - // Main filter logic - let rec buildFilterExpr<'T> (param: ParameterExpression) buildTypeDiscriminatorCheck filter (query : IQueryable<'T>) = - let buildFilterExpr = buildFilterExpr<'T> param buildTypeDiscriminatorCheck + let whereExpr<'T> (query : IQueryable<'T>) (param : ParameterExpression) predicate = + let whereMethod = genericWhereMethod.MakeGenericMethod ([| typeof<'T> |]) + Expression.Call (whereMethod, [| query.Expression; makeLambda<'T> param predicate |]) + + let private StringStartsWithMethod = typeof.GetMethod ("StartsWith", [| typeof |]) + let private StringEndsWithMethod = typeof.GetMethod ("EndsWith", [| typeof |]) + let private StringContainsMethod = typeof.GetMethod ("Contains", [| typeof |]) + + let getField (param : ParameterExpression) fieldName = Expression.PropertyOrField (param, fieldName) + + [] + type SourceExpression private (expression : Expression) = + new (parameter : ParameterExpression) = SourceExpression (parameter :> Expression) + new (``member`` : MemberExpression) = SourceExpression (``member`` :> Expression) + member _.Value = expression + static member op_Implicit (source : SourceExpression) = source.Value + static member op_Implicit (parameter : ParameterExpression) = SourceExpression (parameter :> Expression) + static member op_Implicit (``member`` : MemberExpression) = SourceExpression (``member`` :> Expression) + + let rec buildFilterExpr (param : SourceExpression) buildTypeDiscriminatorCheck filter : Expression = + let build = buildFilterExpr param buildTypeDiscriminatorCheck match filter with - | NoFilter -> query.Expression - | And (f1, f2) -> - let q1 = buildFilterExpr f1 query |> Expression.Lambda>> |> _.Compile().Invoke() - let q2 = buildFilterExpr f2 q1 |> Expression.Lambda>> |> _.Compile().Invoke() - q2.Expression - //| Or (f1, f2) -> - // let expr1 = buildFilterExpr f1 query - // let expr2 = buildFilterExpr f2 query - // Expression.OrElse(expr1, expr2) |> whereExpr<'T> query param :> Expression - //| Not f -> - // let exceptMethod = - // typeof.GetMethods() - // |> Array.find (fun m -> m.Name = "Except") - // |> fun m -> m.MakeGenericMethod([|typeof<'T>|]) - // Expression.Call(exceptMethod, [|query.Expression; buildFilterExpr f|]) - //| OfTypes types -> - // match types.Value with - // | [] -> query.Expression // No types specified, return original query - // | types -> - // let typeChecks = - // types - // |> List.vchoose buildTypeDiscriminatorCheck - // |> List.fold (fun acc expr -> - // match acc with - // | ValueNone -> ValueSome expr - // | ValueSome prevExpr -> ValueSome (Expression.OrElse(prevExpr, expr))) ValueNone - - // match typeChecks with - // | ValueNone -> query.Expression - // | ValueSome expr -> whereExpr query expr :> Expression - | Equals f -> - Expression.Equal(getPropertyExpr<'T> param f.FieldName, Expression.Constant(f.Value)) |> whereExpr<'T> query param :> Expression - | GreaterThan f -> - Expression.GreaterThan(getPropertyExpr<'T> param f.FieldName, Expression.Constant(f.Value)) |> whereExpr<'T> query param :> Expression - | LessThan f -> - Expression.LessThan(getPropertyExpr<'T> param f.FieldName, Expression.Constant(f.Value)) |> whereExpr<'T> query param :> Expression + | NoFilter -> Expression.Constant (true) + | Not f -> f |> build |> Expression.Not :> Expression + | And (f1, f2) -> Expression.AndAlso (build f1, build f2) + | Or (f1, f2) -> Expression.OrElse (build f1, build f2) + | Equals f -> Expression.Equal (Expression.PropertyOrField (param, f.FieldName), Expression.Constant (f.Value)) + | GreaterThan f -> Expression.GreaterThan (Expression.PropertyOrField (param, f.FieldName), Expression.Constant (f.Value)) + | LessThan f -> Expression.LessThan (Expression.PropertyOrField (param, f.FieldName), Expression.Constant (f.Value)) | StartsWith f -> - let methodInfo = typeof.GetMethod("StartsWith", [|typeof|]) - Expression.Call(getPropertyExpr<'T> param f.FieldName, methodInfo, Expression.Constant(f.Value)) |> whereExpr<'T> query param :> Expression + Expression.Call (Expression.PropertyOrField (param, f.FieldName), StringStartsWithMethod, Expression.Constant (f.Value)) | EndsWith f -> - let methodInfo = typeof.GetMethod("EndsWith", [|typeof|]) - Expression.Call(getPropertyExpr<'T> param f.FieldName, methodInfo, Expression.Constant(f.Value)) |> whereExpr<'T> query param :> Expression + Expression.Call (Expression.PropertyOrField (param, f.FieldName), StringEndsWithMethod, Expression.Constant (f.Value)) | Contains f -> - let methodInfo = typeof.GetMethod("Contains", [|typeof|]) - Expression.Call(getPropertyExpr<'T> param f.FieldName, methodInfo, Expression.Constant(f.Value)) |> whereExpr<'T> query param :> Expression + Expression.Call (Expression.PropertyOrField (param, f.FieldName), StringContainsMethod, Expression.Constant (f.Value)) + | OfTypes types -> + types.Value + |> Seq.map (fun t -> buildTypeDiscriminatorCheck param t) + |> Seq.reduce (fun acc expr -> Expression.Or (acc, expr)) | FilterField f -> - let propExpr = getPropertyExpr<'T> param f.FieldName - match propExpr.Type.GetInterfaces() - |> Array.tryFind (fun t -> - t.IsGenericType && t.GetGenericTypeDefinition() = typedefof>) with - | Some queryableType -> - let elementType = queryableType.GetGenericArguments().[0] - let subFilter = f.Value - let subQuery = Expression.Convert(propExpr, queryableType) - Expression.Call(typeof, "Any", [|elementType|], subQuery) |> whereExpr<'T> query param :> Expression - | None -> query.Expression + let paramExpr = Expression.PropertyOrField (param, f.FieldName) + buildFilterExpr (SourceExpression paramExpr) buildTypeDiscriminatorCheck f.Value type ObjectListFilter with - member filter.Apply<'T, 'D>(query : IQueryable<'T>, - compareDiscriminator : Expression>, - getDiscriminatorValue : (Type -> 'D)) = - - // Helper for discriminator comparison - let buildTypeDiscriminatorCheck (t: Type) = - match compareDiscriminator, getDiscriminatorValue with - | null, discValueFn when obj.Equals(discValueFn, null) -> - // use __typename from filter and do type.ToSting() for values - ValueNone - | discExpr, discValueFn when obj.Equals(discValueFn, null) -> - // use discriminator and do type.ToSting() for values - ValueNone - | null, discValueFn -> - // use __typename from filter and execute discValueFn for values - ValueNone - | discExpr, discValueFn -> - // use discriminator and execute discValueFn for values - - let discriminatorValue = discValueFn t - let param = Expression.Parameter(typeof<'T>, "x") - let discExpr = getPropertyExpr<'T> param "__discriminator" // Assuming discriminator field name - let valueExpr = Expression.Constant(discriminatorValue) - ValueSome(Expression.Equal(discExpr, valueExpr)) - - // Create and execute the final expression - let param = Expression.Parameter(typeof<'T>, "x") - query.Provider.CreateQuery<'T>(buildFilterExpr<'T> param buildTypeDiscriminatorCheck filter query) - - member filter.Apply<'T, 'D>(query : IQueryable<'T>, - [] getDiscriminator : Expression> | null, - [] getDiscriminatorValue : Type -> 'D) = - - // Helper for discriminator comparison - let buildTypeDiscriminatorCheck (t: Type) = - match getDiscriminator, getDiscriminatorValue with - | null, discValueFn when obj.Equals(discValueFn, null) -> - // use __typename from filter and do type.ToSting() for values - ValueNone - | discExpr, discValueFn when obj.Equals(discValueFn, null) -> - // use discriminator and do type.ToSting() for values - ValueNone - | null, discValueFn -> - // use __typename from filter and execute discValueFn for values - ValueNone - | discExpr, discValueFn -> - // use discriminator and execute discValueFn for values - let discriminatorValue = discValueFn t - let param = Expression.Parameter(typeof<'T>, "x") - let discExpr = getPropertyExpr<'T> param "__discriminator" // Assuming discriminator field name - let valueExpr = Expression.Constant(discriminatorValue) - ValueSome(Expression.Equal(discExpr, valueExpr)) - - // Create and execute the final expression - let param = Expression.Parameter(typeof<'T>, "x") - query.Provider.CreateQuery<'T>(buildFilterExpr<'T> param buildTypeDiscriminatorCheck filter query) + member filter.Apply<'T, 'D> + (query : IQueryable<'T>, compareDiscriminator : Expression>, getDiscriminatorValue : (Type -> 'D)) + = + + match filter with + | NoFilter -> query + | _ -> + + // Helper for discriminator comparison + let buildTypeDiscriminatorCheck (param : SourceExpression) (t : Type) = + match compareDiscriminator, getDiscriminatorValue with + | null, discValueFn when obj.Equals (discValueFn, null) -> + // use __typename from filter and do type.ToSting() for values + Unchecked.defaultof + | discExpr, discValueFn when obj.Equals (discValueFn, null) -> + // use discriminator and do type.ToSting() for values + Unchecked.defaultof + | null, discValueFn -> + // use __typename from filter and execute discValueFn for values + Unchecked.defaultof + | discExpr, discValueFn -> + // use discriminator and execute discValueFn for values + + let discriminatorValue = discValueFn t + Expression.Equal (Expression.PropertyOrField (param, "__discriminator"), Expression.Constant (discriminatorValue)) + + let queryExpr = + let param = Expression.Parameter (typeof<'T>, "x") + let body = buildFilterExpr (SourceExpression param) buildTypeDiscriminatorCheck filter + whereExpr<'T> query param body + // Create and execute the final expression + query.Provider.CreateQuery<'T> (queryExpr) + + member filter.Apply<'T, 'D> + (query : IQueryable<'T>, [] getDiscriminator : Expression> | null, [] getDiscriminatorValue : Type -> 'D) + = + + match filter with + | NoFilter -> query + | _ -> + // Helper for discriminator comparison + let buildTypeDiscriminatorCheck (param : SourceExpression) (t : Type) = + match getDiscriminator, getDiscriminatorValue with + | null, discValueFn when obj.Equals (discValueFn, null) -> + // use __typename from filter and do type.ToSting() for values + Unchecked.defaultof + | discExpr, discValueFn when obj.Equals (discValueFn, null) -> + // use discriminator and do type.ToSting() for values + Unchecked.defaultof + | null, discValueFn -> + // use __typename from filter and execute discValueFn for values + Unchecked.defaultof + | discExpr, discValueFn -> + // use discriminator and execute discValueFn for values + let discriminatorValue = discValueFn t + Expression.Equal (Expression.PropertyOrField (param, "__discriminator"), Expression.Constant (discriminatorValue)) + + let queryExpr = + let param = Expression.Parameter (typeof<'T>, "x") + let body = buildFilterExpr (SourceExpression param) buildTypeDiscriminatorCheck filter + whereExpr<'T> query param body + // Create and execute the final expression + query.Provider.CreateQuery<'T> (queryExpr) diff --git a/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs b/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs index cdd4a47b9..f6d9684fd 100644 --- a/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs @@ -384,22 +384,22 @@ let ``ObjectListFilter works with AND operator``() = result.Contact |> equals { Email = "b.adams@gmail.com" } result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] -//[] -//let ``ObjectListFilter works with OR operator``() = -// let filter = -// Or ( -// GreaterThan { FieldName = "id"; Value = 4 }, -// Equals { FieldName = "lastName"; Value = "Adams" } -// ) -// let queryable = data.AsQueryable() -// let filteredData = filter.Apply(queryable) |> Seq.toList -// List.length filteredData |> equals 2 -// let result = List.head filteredData -// result.ID |> equals 4 -// result.FirstName |> equals "Ben" -// result.LastName |> equals "Adams" -// result.Contact |> equals { Email = "b.adams@gmail.com" } -// result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] +[] +let ``ObjectListFilter works with OR operator``() = + let filter = + Or ( + GreaterThan { FieldName = "id"; Value = 4 }, + Equals { FieldName = "lastName"; Value = "Adams" } + ) + let queryable = data.AsQueryable() + let filteredData = filter.Apply(queryable) |> Seq.toList + List.length filteredData |> equals 2 + let result = List.head filteredData + result.ID |> equals 4 + result.FirstName |> equals "Ben" + result.LastName |> equals "Adams" + result.Contact |> equals { Email = "b.adams@gmail.com" } + result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] //[] //let ``ObjectListFilter works with FilterField operator``() = @@ -415,3 +415,17 @@ let ``ObjectListFilter works with AND operator``() = // result.Contact |> equals { Email = "b.adams@gmail.com" } // result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] +[] +let ``ObjectListFilter works with NOT operator``() = + let filter = + Not (Equals { FieldName = "lastName"; Value = "Adams" }) + let queryable = data.AsQueryable() + let filteredData = filter.Apply(queryable) |> Seq.toList + List.length filteredData |> equals 2 + let result1 = List.head filteredData + result1.ID |> equals 2 + result1.FirstName |> equals "Jonathan" + result1.LastName |> equals "Abrams" + result1.Contact |> equals { Email = "j.abrams@gmail.com" } + result1.Friends |> equals [] + From 0fa9d2a11ef83347686005d882ef3eb1b63c89ae Mon Sep 17 00:00:00 2001 From: Viktor Tochonov Date: Tue, 11 Mar 2025 14:18:45 +0200 Subject: [PATCH 03/14] Implemented new tests for different cases of buildTypeDiscriminatorCheck --- .../ObjectListFilter.fs | 53 ++++-- .../TypeSystemExtensions.fs | 2 +- tests/FSharp.Data.GraphQL.Tests/LinqTests.fs | 151 +++++++++++++++++- 3 files changed, 188 insertions(+), 18 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs index e386bcff8..a05f50a6b 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs @@ -1,9 +1,6 @@ namespace FSharp.Data.GraphQL.Server.Middleware open System -open System.Linq -open System.Linq.Expressions -open System.Runtime.InteropServices /// A filter definition for a field value. type FieldFilter<'Val> = { FieldName : string; Value : 'Val } @@ -19,7 +16,7 @@ type ObjectListFilter = | StartsWith of FieldFilter | EndsWith of FieldFilter | Contains of FieldFilter - | OfTypes of FieldFilter + | OfTypes of Type list | FilterField of FieldFilter | NoFilter @@ -57,9 +54,15 @@ module ObjectListFilter = /// Creates a new ObjectListFilter representing a NOT opreation for the existing one. let ( !!! ) filter = Not filter +open System.Linq +open System.Linq.Expressions +open System.Runtime.InteropServices +open System.Reflection + [] module ObjectListFilterExtensions = + //// discriminator custom condition //let a () = // filter.Apply( @@ -79,6 +82,31 @@ module ObjectListFilterExtensions = // | t when Type.(=)(t, typeof) -> ResidentialPropertiesConstants.Discriminators.Building // ) + type DiscriminatorExpression<'T, 'D> = + | GetDiscriminatorValue of ('T -> 'D) + | CompareDiscriminator of Expression> + + [] + type ObjectListFilterLinqOptions<'T, 'D> ( + discriminatorExpression : DiscriminatorExpression<'T, 'D> | null, + [] getDiscriminatorValue: (Type -> 'D) | null, + [] serializeMemberName: (MemberInfo -> string) | null) = + + member _.DiscriminatorExpression = discriminatorExpression |> ValueOption.ofObj + member _.GetDiscriminatorValue = getDiscriminatorValue |> ValueOption.ofObj + member _.SerializeMemberName = serializeMemberName |> ValueOption.ofObj + + static member None = ObjectListFilterLinqOptions<'T, 'D> (null, null, null) + + new (getDiscriminatorValue : 'T -> 'D) = ObjectListFilterLinqOptions<'T, 'D> (GetDiscriminatorValue getDiscriminatorValue, null, null) + new (compareDiscriminator : Expression>) = ObjectListFilterLinqOptions<'T, 'D> (CompareDiscriminator compareDiscriminator, null, null) + new (getDiscriminatorValue : Type -> 'D) = ObjectListFilterLinqOptions<'T, 'D> (null, getDiscriminatorValue, null) + new (serializeMemberName : MemberInfo -> string) = ObjectListFilterLinqOptions<'T, 'D> (null, null, serializeMemberName) + + new (getDiscriminatorValue : 'T -> 'D, serializeMemberName : MemberInfo -> string) = ObjectListFilterLinqOptions<'T, 'D> (GetDiscriminatorValue getDiscriminatorValue, null, serializeMemberName) + new (compareDiscriminator : Expression>, serializeMemberName : MemberInfo -> string) = ObjectListFilterLinqOptions<'T, 'D> (CompareDiscriminator compareDiscriminator, null, serializeMemberName) + new (getDiscriminatorValue : Type -> 'D, serializeMemberName : MemberInfo -> string) = ObjectListFilterLinqOptions<'T, 'D> (null, getDiscriminatorValue, serializeMemberName) + // Helper to create lambda from body expression let makeLambda<'T> (param : ParameterExpression) (body : Expression) = let delegateType = typedefof>.MakeGenericType ([| typeof<'T>; body.Type |]) @@ -129,7 +157,7 @@ module ObjectListFilterExtensions = | Contains f -> Expression.Call (Expression.PropertyOrField (param, f.FieldName), StringContainsMethod, Expression.Constant (f.Value)) | OfTypes types -> - types.Value + types |> Seq.map (fun t -> buildTypeDiscriminatorCheck param t) |> Seq.reduce (fun acc expr -> Expression.Or (acc, expr)) | FilterField f -> @@ -181,19 +209,22 @@ type ObjectListFilter with // Helper for discriminator comparison let buildTypeDiscriminatorCheck (param : SourceExpression) (t : Type) = match getDiscriminator, getDiscriminatorValue with - | null, discValueFn when obj.Equals (discValueFn, null) -> + | null, discValueFn when obj.Equals(discValueFn, null) -> // use __typename from filter and do type.ToSting() for values - Unchecked.defaultof - | discExpr, discValueFn when obj.Equals (discValueFn, null) -> + let typename = t.FullName + Expression.Equal(Expression.PropertyOrField(param, "__typename"), Expression.Constant(typename)) :> Expression + | discExpr, discValueFn when obj.Equals(discValueFn, null) -> // use discriminator and do type.ToSting() for values - Unchecked.defaultof + let typename = t.FullName + Expression.Equal(Expression.Invoke(discExpr, param), Expression.Constant(typename)) :> Expression | null, discValueFn -> // use __typename from filter and execute discValueFn for values - Unchecked.defaultof + let discriminatorValue = discValueFn t + Expression.Equal(Expression.PropertyOrField(param, "__typename"), Expression.Constant(discriminatorValue)) :> Expression | discExpr, discValueFn -> // use discriminator and execute discValueFn for values let discriminatorValue = discValueFn t - Expression.Equal (Expression.PropertyOrField (param, "__discriminator"), Expression.Constant (discriminatorValue)) + Expression.Equal (Expression.Invoke(discExpr, param), Expression.Constant (discriminatorValue)) let queryExpr = let param = Expression.Parameter (typeof<'T>, "x") diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs b/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs index 27fae4438..fae77fe24 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs @@ -40,7 +40,7 @@ module TypeSystemExtensions = | ValueNone -> raise (MalformedGQLQueryException ($"Type '{name}' not found in schema.")) match typeFields.Keys |> Seq.map getType |> Seq.toList with | [] -> ValueNone - | filters -> ValueSome (f &&& (OfTypes { FieldName = "__typename"; Value = filters })) + | filters -> ValueSome (f &&& (OfTypes filters)) | _ -> ValueSome f | false, _ -> ValueNone | true, _ -> raise (InvalidOperationException "Invalid filter argument type.") diff --git a/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs b/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs index f6d9684fd..8ab66fa34 100644 --- a/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs @@ -12,6 +12,7 @@ open FSharp.Data.GraphQL.Execution open FSharp.Data.GraphQL.Server.Middleware open FSharp.Data.GraphQL.Server.Middleware.ObjectListFilter open FSharp.Data.GraphQL.Server.Middleware.ObjectListFilterExtensions +open System.Linq.Expressions type Contact = { Email : string } @@ -54,6 +55,7 @@ let data = Contact = { Email = "j.trif@gmail.com" } Friends = [ { Email = "j.abrams@gmail.com" } ] } ] + let internal undefined<'t> = Unchecked.defaultof<'t> let resolveRoot ctx () = @@ -352,7 +354,7 @@ let ``ObjectListFilter works with Contains operator``() = result.FirstName |> equals "Ben" result.LastName |> equals "Adams" result.Contact |> equals { Email = "b.adams@gmail.com" } - result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] + result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] [] let ``ObjectListFilter works with EndsWith operator``() = @@ -365,11 +367,11 @@ let ``ObjectListFilter works with EndsWith operator``() = result.FirstName |> equals "Ben" result.LastName |> equals "Adams" result.Contact |> equals { Email = "b.adams@gmail.com" } - result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] + result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] [] let ``ObjectListFilter works with AND operator``() = - let filter = + let filter = And ( Contains { FieldName = "firstName"; Value = "en" }, Equals { FieldName = "lastName"; Value = "Adams" } @@ -386,7 +388,7 @@ let ``ObjectListFilter works with AND operator``() = [] let ``ObjectListFilter works with OR operator``() = - let filter = + let filter = Or ( GreaterThan { FieldName = "id"; Value = 4 }, Equals { FieldName = "lastName"; Value = "Adams" } @@ -403,7 +405,7 @@ let ``ObjectListFilter works with OR operator``() = //[] //let ``ObjectListFilter works with FilterField operator``() = -// let filter = +// let filter = // FilterField { FieldName = "Friends"; Value = Contains { FieldName = "Email"; Value = "l.trif@gmail.com" } } // let queryable = data.AsQueryable() // let filteredData = filter.Apply(queryable) |> Seq.toList @@ -417,7 +419,7 @@ let ``ObjectListFilter works with OR operator``() = [] let ``ObjectListFilter works with NOT operator``() = - let filter = + let filter = Not (Equals { FieldName = "lastName"; Value = "Adams" }) let queryable = data.AsQueryable() let filteredData = filter.Apply(queryable) |> Seq.toList @@ -429,3 +431,140 @@ let ``ObjectListFilter works with NOT operator``() = result1.Contact |> equals { Email = "j.abrams@gmail.com" } result1.Friends |> equals [] +type Complex = + { ID : int + Name : string + Discriminator : string } + +type Building = + { ID : int + Name : string + Discriminator : string } + +type Community = + { ID : int + Name : string + Discriminator : string + Complexes : int list + Buildings : int list } + +type Property = + | Complex of Complex + | Building of Building + | Community of Community + +[] +let ``ObjectListFilter works with getDiscriminator for Complex``() = + let propertyData: Property list = + [ + Complex { ID = 1; Name = "Complex A"; Discriminator = typeof.FullName} + Building { ID = 2; Name = "Building B"; Discriminator = typeof.FullName } + Community { ID = 3; Name = "Community C"; Discriminator = typeof.FullName; Complexes = [1]; Buildings = [2] } + Complex { ID = 4; Name = "Complex AA"; Discriminator = typeof.FullName } + Building { ID = 5; Name = "Building BB"; Discriminator = typeof.FullName } + Community { ID = 6; Name = "Community CC"; Discriminator = typeof.FullName; Complexes = [4]; Buildings = [5] } + ] + let queryable = propertyData.AsQueryable() + let filter = OfTypes [typeof] + let filteredData = + filter.Apply( + queryable, + getDiscriminator = + fun p -> + match p with + | Complex c -> c.Discriminator + | Building b -> b.Discriminator + | Community c -> c.Discriminator + ) + |> Seq.toList + List.length filteredData |> equals 2 + let result1 = List.head filteredData + match result1 with + | Complex c -> + c.ID |> equals 1 + c.Name |> equals "Complex A" + | _ -> failwith "Expected Complex" + let result2 = List.last filteredData + match result2 with + | Complex c -> + c.ID |> equals 4 + c.Name |> equals "Complex AA" + | _ -> failwith "Expected Complex" + + +[] +let ``ObjectListFilter works with getDiscriminator and getDiscriminatorValue for Complex``() = + let propertyData: Property list = + [ + Complex { ID = 1; Name = "Complex A"; Discriminator = "Complex" } + Building { ID = 2; Name = "Building B"; Discriminator = "Building" } + Community { ID = 3; Name = "Community C"; Discriminator = "Community"; Complexes = [1]; Buildings = [2] } + Complex { ID = 4; Name = "Complex AA"; Discriminator = "Complex" } + Building { ID = 5; Name = "Building BB"; Discriminator = "Building" } + Community { ID = 6; Name = "Community CC"; Discriminator = "Community"; Complexes = [4]; Buildings = [5] } + ] + let queryable = propertyData.AsQueryable() + let filter = OfTypes [typeof] + let filteredData = + filter.Apply( + queryable, + (fun p -> + match p with + | Complex c -> c.Discriminator + | Building b -> b.Discriminator + | Community c -> c.Discriminator), + (function + | t when t = typeof -> "Complex" + | t when t = typeof -> "Building" + | t when t = typeof -> "Community" + | _ -> raise (NotSupportedException "Type not supported")) + ) + |> Seq.toList + List.length filteredData |> equals 2 + let result1 = List.head filteredData + match result1 with + | Complex c -> + c.ID |> equals 1 + c.Name |> equals "Complex A" + | _ -> failwith "Expected Complex" + let result2 = List.last filteredData + match result2 with + | Complex c -> + c.ID |> equals 4 + c.Name |> equals "Complex AA" + | _ -> failwith "Expected Complex" + + + +[] +let ``ObjectListFilter works with getDiscriminatorValue for Complex``() = + let propertyData: Property list = + [ + Complex { ID = 1; Name = "Complex A"; Discriminator = typeof.FullName} + Building { ID = 2; Name = "Building B"; Discriminator = typeof.FullName } + Community { ID = 3; Name = "Community C"; Discriminator = typeof.FullName; Complexes = [1]; Buildings = [2] } + Complex { ID = 4; Name = "Complex AA"; Discriminator = typeof.FullName } + Building { ID = 5; Name = "Building BB"; Discriminator = typeof.FullName } + Community { ID = 6; Name = "Community CC"; Discriminator = typeof.FullName; Complexes = [4]; Buildings = [5] } + ] + let queryable = propertyData.AsQueryable() + let filter = OfTypes [typeof] + let filteredData = + filter.Apply( + queryable, + getDiscriminatorValue = (fun t -> t.FullName) + ) + |> Seq.toList + List.length filteredData |> equals 2 + let result1 = List.head filteredData + match result1 with + | Complex c -> + c.ID |> equals 1 + c.Name |> equals "Complex A" + | _ -> failwith "Expected Complex" + let result2 = List.last filteredData + match result2 with + | Complex c -> + c.ID |> equals 4 + c.Name |> equals "Complex AA" + | _ -> failwith "Expected Complex" From 6fe1ed12805b8ff6fb51277a686084cd113a8e60 Mon Sep 17 00:00:00 2001 From: Viktor Tochonov Date: Wed, 12 Mar 2025 13:20:49 +0200 Subject: [PATCH 04/14] Implemented tests for different discriminator cases --- .../ObjectListFilter.fs | 104 ++++++----------- .../TypeSystemExtensions.fs | 28 +++-- tests/FSharp.Data.GraphQL.Tests/LinqTests.fs | 109 +++++++++--------- 3 files changed, 108 insertions(+), 133 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs index a05f50a6b..0920bcb5c 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs @@ -82,36 +82,6 @@ module ObjectListFilterExtensions = // | t when Type.(=)(t, typeof) -> ResidentialPropertiesConstants.Discriminators.Building // ) - type DiscriminatorExpression<'T, 'D> = - | GetDiscriminatorValue of ('T -> 'D) - | CompareDiscriminator of Expression> - - [] - type ObjectListFilterLinqOptions<'T, 'D> ( - discriminatorExpression : DiscriminatorExpression<'T, 'D> | null, - [] getDiscriminatorValue: (Type -> 'D) | null, - [] serializeMemberName: (MemberInfo -> string) | null) = - - member _.DiscriminatorExpression = discriminatorExpression |> ValueOption.ofObj - member _.GetDiscriminatorValue = getDiscriminatorValue |> ValueOption.ofObj - member _.SerializeMemberName = serializeMemberName |> ValueOption.ofObj - - static member None = ObjectListFilterLinqOptions<'T, 'D> (null, null, null) - - new (getDiscriminatorValue : 'T -> 'D) = ObjectListFilterLinqOptions<'T, 'D> (GetDiscriminatorValue getDiscriminatorValue, null, null) - new (compareDiscriminator : Expression>) = ObjectListFilterLinqOptions<'T, 'D> (CompareDiscriminator compareDiscriminator, null, null) - new (getDiscriminatorValue : Type -> 'D) = ObjectListFilterLinqOptions<'T, 'D> (null, getDiscriminatorValue, null) - new (serializeMemberName : MemberInfo -> string) = ObjectListFilterLinqOptions<'T, 'D> (null, null, serializeMemberName) - - new (getDiscriminatorValue : 'T -> 'D, serializeMemberName : MemberInfo -> string) = ObjectListFilterLinqOptions<'T, 'D> (GetDiscriminatorValue getDiscriminatorValue, null, serializeMemberName) - new (compareDiscriminator : Expression>, serializeMemberName : MemberInfo -> string) = ObjectListFilterLinqOptions<'T, 'D> (CompareDiscriminator compareDiscriminator, null, serializeMemberName) - new (getDiscriminatorValue : Type -> 'D, serializeMemberName : MemberInfo -> string) = ObjectListFilterLinqOptions<'T, 'D> (null, getDiscriminatorValue, serializeMemberName) - - // Helper to create lambda from body expression - let makeLambda<'T> (param : ParameterExpression) (body : Expression) = - let delegateType = typedefof>.MakeGenericType ([| typeof<'T>; body.Type |]) - Expression.Lambda (delegateType, body, param) - let private genericWhereMethod = typeof.GetMethods () |> Seq.where (fun m -> m.Name = "Where") @@ -123,7 +93,7 @@ module ObjectListFilterExtensions = // Helper to create Where expression let whereExpr<'T> (query : IQueryable<'T>) (param : ParameterExpression) predicate = let whereMethod = genericWhereMethod.MakeGenericMethod ([| typeof<'T> |]) - Expression.Call (whereMethod, [| query.Expression; makeLambda<'T> param predicate |]) + Expression.Call (whereMethod, [| query.Expression; Expression.Lambda> (predicate, param) |]) let private StringStartsWithMethod = typeof.GetMethod ("StartsWith", [| typeof |]) let private StringEndsWithMethod = typeof.GetMethod ("EndsWith", [| typeof |]) @@ -164,67 +134,61 @@ module ObjectListFilterExtensions = let paramExpr = Expression.PropertyOrField (param, f.FieldName) buildFilterExpr (SourceExpression paramExpr) buildTypeDiscriminatorCheck f.Value -type ObjectListFilter with + [] + type ObjectListFilterLinqOptions<'T, 'D> ( + [] compareDiscriminator : Expression> | null, + [] getDiscriminatorValue : (Type -> 'D) | null, + [] serializeMemberName : (MemberInfo -> string) | null) = - member filter.Apply<'T, 'D> - (query : IQueryable<'T>, compareDiscriminator : Expression>, getDiscriminatorValue : (Type -> 'D)) - = + member _.CompareDiscriminator = compareDiscriminator |> ValueOption.ofObj + member _.GetDiscriminatorValue = getDiscriminatorValue |> ValueOption.ofObj + //member _.SerializeMemberName = serializeMemberName |> ValueOption.ofObj - match filter with - | NoFilter -> query - | _ -> + static member None = ObjectListFilterLinqOptions<'T, 'D> (null, null, null) - // Helper for discriminator comparison - let buildTypeDiscriminatorCheck (param : SourceExpression) (t : Type) = - match compareDiscriminator, getDiscriminatorValue with - | null, discValueFn when obj.Equals (discValueFn, null) -> - // use __typename from filter and do type.ToSting() for values - Unchecked.defaultof - | discExpr, discValueFn when obj.Equals (discValueFn, null) -> - // use discriminator and do type.ToSting() for values - Unchecked.defaultof - | null, discValueFn -> - // use __typename from filter and execute discValueFn for values - Unchecked.defaultof - | discExpr, discValueFn -> - // use discriminator and execute discValueFn for values + static member GetCompareDiscriminator (getDiscriminatorValue : Expression>) = + let tParam = Expression.Parameter (typeof<'T>, "x") + let dParam = Expression.Parameter (typeof<'D>, "d") + let body = Expression.Equal(Expression.Invoke(getDiscriminatorValue, tParam), dParam) + Expression.Lambda> (body, tParam, dParam) - let discriminatorValue = discValueFn t - Expression.Equal (Expression.PropertyOrField (param, "__discriminator"), Expression.Constant (discriminatorValue)) + new (getDiscriminator : Expression>) = ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, null, null) + new (compareDiscriminator : Expression>) = ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator, null, null) + new (getDiscriminatorValue : Type -> 'D) = ObjectListFilterLinqOptions<'T, 'D> (null, getDiscriminatorValue, null) + //new (serializeMemberName : MemberInfo -> string) = ObjectListFilterLinqOptions<'T, 'D> (null, null, serializeMemberName) - let queryExpr = - let param = Expression.Parameter (typeof<'T>, "x") - let body = buildFilterExpr (SourceExpression param) buildTypeDiscriminatorCheck filter - whereExpr<'T> query param body - // Create and execute the final expression - query.Provider.CreateQuery<'T> (queryExpr) + new (getDiscriminator : Expression>, getDiscriminatorValue : Type -> 'D) = ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, getDiscriminatorValue, null) + + //new (getDiscriminator : Expression>, serializeMemberName : MemberInfo -> string) = ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, null, serializeMemberName) + //new (compareDiscriminator : Expression>, serializeMemberName : MemberInfo -> string) = ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator, null, serializeMemberName) + //new (getDiscriminatorValue : Type -> 'D, serializeMemberName : MemberInfo -> string) = ObjectListFilterLinqOptions<'T, 'D> (null, getDiscriminatorValue, serializeMemberName) + +type ObjectListFilter with - member filter.Apply<'T, 'D> - (query : IQueryable<'T>, [] getDiscriminator : Expression> | null, [] getDiscriminatorValue : Type -> 'D) - = + member filter.Apply<'T, 'D> (query : IQueryable<'T>, [] options : ObjectListFilterLinqOptions<'T, 'D>) = match filter with | NoFilter -> query | _ -> // Helper for discriminator comparison let buildTypeDiscriminatorCheck (param : SourceExpression) (t : Type) = - match getDiscriminator, getDiscriminatorValue with - | null, discValueFn when obj.Equals(discValueFn, null) -> + match options.CompareDiscriminator, options.GetDiscriminatorValue with + | ValueNone, ValueNone -> // use __typename from filter and do type.ToSting() for values let typename = t.FullName Expression.Equal(Expression.PropertyOrField(param, "__typename"), Expression.Constant(typename)) :> Expression - | discExpr, discValueFn when obj.Equals(discValueFn, null) -> + | ValueSome discExpr, ValueNone -> // use discriminator and do type.ToSting() for values let typename = t.FullName - Expression.Equal(Expression.Invoke(discExpr, param), Expression.Constant(typename)) :> Expression - | null, discValueFn -> + Expression.Invoke(discExpr, param, Expression.Constant(typename)) :> Expression + | ValueNone, ValueSome discValueFn -> // use __typename from filter and execute discValueFn for values let discriminatorValue = discValueFn t Expression.Equal(Expression.PropertyOrField(param, "__typename"), Expression.Constant(discriminatorValue)) :> Expression - | discExpr, discValueFn -> + | ValueSome discExpr, ValueSome discValueFn -> // use discriminator and execute discValueFn for values let discriminatorValue = discValueFn t - Expression.Equal (Expression.Invoke(discExpr, param), Expression.Constant (discriminatorValue)) + Expression.Invoke(discExpr, param, Expression.Constant (discriminatorValue)) let queryExpr = let param = Expression.Parameter (typeof<'T>, "x") diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs b/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs index fae77fe24..209966af6 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs @@ -23,6 +23,21 @@ module TypeSystemExtensions = open ObjectListFilter.Operators + type ExecutionInfo with + + member this.ResolveAbstractionFilter (typeMap : TypeMap) = + match this.Kind with + | ResolveAbstraction typeFields -> + let getType name = + match typeMap.TryFind name with + | ValueSome tdef -> tdef.Type + | ValueNone -> raise (MalformedGQLQueryException ($"Type '{name}' not found in schema.")) + match typeFields.Keys |> Seq.map getType |> Seq.toList with + | [] -> ValueNone + | filters -> ValueSome (OfTypes filters) + | _ -> ValueNone + + type ResolveFieldContext with /// @@ -32,16 +47,9 @@ module TypeSystemExtensions = member this.Filter = match this.Args.TryGetValue "filter" with | true, (:? ObjectListFilter as f) -> - match this.ExecutionInfo.Kind with - | ResolveAbstraction typeFields -> - let getType name = - match this.Context.Schema.TypeMap.TryFind name with - | ValueSome tdef -> tdef.Type - | ValueNone -> raise (MalformedGQLQueryException ($"Type '{name}' not found in schema.")) - match typeFields.Keys |> Seq.map getType |> Seq.toList with - | [] -> ValueNone - | filters -> ValueSome (f &&& (OfTypes filters)) - | _ -> ValueSome f + match this.ExecutionInfo.ResolveAbstractionFilter (this.Context.Schema.TypeMap) with + | ValueSome ofTypes -> ValueSome (ofTypes &&& f) + | ValueNone -> ValueSome f | false, _ -> ValueNone | true, _ -> raise (InvalidOperationException "Invalid filter argument type.") diff --git a/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs b/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs index 8ab66fa34..829f31192 100644 --- a/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs @@ -453,11 +453,12 @@ type Property = | Building of Building | Community of Community + [] let ``ObjectListFilter works with getDiscriminator for Complex``() = let propertyData: Property list = [ - Complex { ID = 1; Name = "Complex A"; Discriminator = typeof.FullName} + Complex { ID = 1; Name = "Complex A"; Discriminator = typeof.FullName } Building { ID = 2; Name = "Building B"; Discriminator = typeof.FullName } Community { ID = 3; Name = "Community C"; Discriminator = typeof.FullName; Complexes = [1]; Buildings = [2] } Complex { ID = 4; Name = "Complex AA"; Discriminator = typeof.FullName } @@ -466,17 +467,13 @@ let ``ObjectListFilter works with getDiscriminator for Complex``() = ] let queryable = propertyData.AsQueryable() let filter = OfTypes [typeof] - let filteredData = - filter.Apply( - queryable, - getDiscriminator = - fun p -> - match p with - | Complex c -> c.Discriminator - | Building b -> b.Discriminator - | Community c -> c.Discriminator - ) - |> Seq.toList + let options = + ObjectListFilterLinqOptions( + (function + | Complex c -> c.Discriminator + | Building b -> b.Discriminator + | Community c -> c.Discriminator)) + let filteredData = filter.Apply(queryable,options) |> Seq.toList List.length filteredData |> equals 2 let result1 = List.head filteredData match result1 with @@ -496,30 +493,28 @@ let ``ObjectListFilter works with getDiscriminator for Complex``() = let ``ObjectListFilter works with getDiscriminator and getDiscriminatorValue for Complex``() = let propertyData: Property list = [ - Complex { ID = 1; Name = "Complex A"; Discriminator = "Complex" } - Building { ID = 2; Name = "Building B"; Discriminator = "Building" } - Community { ID = 3; Name = "Community C"; Discriminator = "Community"; Complexes = [1]; Buildings = [2] } - Complex { ID = 4; Name = "Complex AA"; Discriminator = "Complex" } - Building { ID = 5; Name = "Building BB"; Discriminator = "Building" } - Community { ID = 6; Name = "Community CC"; Discriminator = "Community"; Complexes = [4]; Buildings = [5] } + Complex { ID = 1; Name = "Complex A"; Discriminator = typeof.Name} + Building { ID = 2; Name = "Building B"; Discriminator = typeof.Name } + Community { ID = 3; Name = "Community C"; Discriminator = typeof.Name; Complexes = [1]; Buildings = [2] } + Complex { ID = 4; Name = "Complex AA"; Discriminator = typeof.Name } + Building { ID = 5; Name = "Building BB"; Discriminator = typeof.Name } + Community { ID = 6; Name = "Community CC"; Discriminator = typeof.Name; Complexes = [4]; Buildings = [5] } ] let queryable = propertyData.AsQueryable() let filter = OfTypes [typeof] - let filteredData = - filter.Apply( - queryable, - (fun p -> - match p with - | Complex c -> c.Discriminator - | Building b -> b.Discriminator - | Community c -> c.Discriminator), + let options = + ObjectListFilterLinqOptions( + (function + | Complex c -> c.Discriminator + | Building b -> b.Discriminator + | Community c -> c.Discriminator), (function | t when t = typeof -> "Complex" | t when t = typeof -> "Building" | t when t = typeof -> "Community" | _ -> raise (NotSupportedException "Type not supported")) ) - |> Seq.toList + let filteredData = filter.Apply(queryable,options) |> Seq.toList List.length filteredData |> equals 2 let result1 = List.head filteredData match result1 with @@ -534,37 +529,45 @@ let ``ObjectListFilter works with getDiscriminator and getDiscriminatorValue for c.Name |> equals "Complex AA" | _ -> failwith "Expected Complex" +type Cow = + { ID : int + Name : string + __typename : string } +type Horse = + { ID : int + Name : string + __typename : string } + +let animalData = + [ + { ID = 1; Name = "Cow A"; __typename = typeof.Name } + { ID = 2; Name = "Horse B"; __typename = typeof.Name } + { ID = 3; Name = "Cow C"; __typename = typeof.Name } + { ID = 4; Name = "Horse D"; __typename = typeof.Name } + ] [] -let ``ObjectListFilter works with getDiscriminatorValue for Complex``() = - let propertyData: Property list = - [ - Complex { ID = 1; Name = "Complex A"; Discriminator = typeof.FullName} - Building { ID = 2; Name = "Building B"; Discriminator = typeof.FullName } - Community { ID = 3; Name = "Community C"; Discriminator = typeof.FullName; Complexes = [1]; Buildings = [2] } - Complex { ID = 4; Name = "Complex AA"; Discriminator = typeof.FullName } - Building { ID = 5; Name = "Building BB"; Discriminator = typeof.FullName } - Community { ID = 6; Name = "Community CC"; Discriminator = typeof.FullName; Complexes = [4]; Buildings = [5] } - ] - let queryable = propertyData.AsQueryable() - let filter = OfTypes [typeof] - let filteredData = - filter.Apply( - queryable, - getDiscriminatorValue = (fun t -> t.FullName) - ) - |> Seq.toList +let ``ObjectListFilter works with getDiscriminatorValue for Horse``() = + let queryable = animalData.AsQueryable() + let filter = OfTypes [typeof] + let options = + ObjectListFilterLinqOptions( + getDiscriminatorValue = (function + | t when t = typeof -> t.Name + | t when t = typeof -> t.Name + | _ -> raise (NotSupportedException "Type not supported")) + ) + let filteredData = filter.Apply(queryable, options) |> Seq.toList List.length filteredData |> equals 2 let result1 = List.head filteredData match result1 with - | Complex c -> - c.ID |> equals 1 - c.Name |> equals "Complex A" - | _ -> failwith "Expected Complex" + | h -> + h.ID |> equals 2 + h.Name |> equals "Horse B" + | _ -> failwith "Expected Horse" let result2 = List.last filteredData match result2 with - | Complex c -> - c.ID |> equals 4 - c.Name |> equals "Complex AA" - | _ -> failwith "Expected Complex" + | h -> + h.ID |> equals 4 + h.Name |> equals "Horse D" From 506110c84b1a2bab9bc11c7ed404a002c2a1b4c6 Mon Sep 17 00:00:00 2001 From: Viktor Tochonov Date: Wed, 12 Mar 2025 14:36:13 +0200 Subject: [PATCH 05/14] Remove unmatching rule --- tests/FSharp.Data.GraphQL.Tests/LinqTests.fs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs b/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs index 829f31192..51dd2df35 100644 --- a/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs @@ -565,7 +565,6 @@ let ``ObjectListFilter works with getDiscriminatorValue for Horse``() = | h -> h.ID |> equals 2 h.Name |> equals "Horse B" - | _ -> failwith "Expected Horse" let result2 = List.last filteredData match result2 with | h -> From 3c0dc436369eee94dce70cc35f97c8b15d9324e2 Mon Sep 17 00:00:00 2001 From: Viktor Tochonov Date: Fri, 14 Mar 2025 14:44:22 +0200 Subject: [PATCH 06/14] Splitted LinqTests and ObjectListFilterLinqTests into separated files --- .../TypeSystemExtensions.fs | 1 - .../FSharp.Data.GraphQL.Tests.fsproj | 3 +- tests/FSharp.Data.GraphQL.Tests/LinqTests.fs | 572 ------------------ .../ObjectListFilterLinqTests.fs | 363 +++++++++++ .../SelectLinqTests.fs | 288 +++++++++ 5 files changed, 653 insertions(+), 574 deletions(-) delete mode 100644 tests/FSharp.Data.GraphQL.Tests/LinqTests.fs create mode 100644 tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs create mode 100644 tests/FSharp.Data.GraphQL.Tests/SelectLinqTests.fs diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs b/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs index 209966af6..fdf3554da 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs @@ -37,7 +37,6 @@ module TypeSystemExtensions = | filters -> ValueSome (OfTypes filters) | _ -> ValueNone - type ResolveFieldContext with /// diff --git a/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj b/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj index 10ef9d6ca..f4352578b 100644 --- a/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj +++ b/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj @@ -63,7 +63,8 @@ - + + diff --git a/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs b/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs deleted file mode 100644 index 51dd2df35..000000000 --- a/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs +++ /dev/null @@ -1,572 +0,0 @@ -// The MIT License (MIT) -// Copyright (c) 2016 Bazinga Technologies Inc -module FSharp.Data.GraphQL.Tests.LinqTests - -open Xunit -open System -open System.Linq -open FSharp.Data.GraphQL -open FSharp.Data.GraphQL.Types -open FSharp.Data.GraphQL.Linq -open FSharp.Data.GraphQL.Execution -open FSharp.Data.GraphQL.Server.Middleware -open FSharp.Data.GraphQL.Server.Middleware.ObjectListFilter -open FSharp.Data.GraphQL.Server.Middleware.ObjectListFilterExtensions -open System.Linq.Expressions - -type Contact = - { Email : string } - -type Person = - { ID : int - FirstName : string - LastName : string - Contact : Contact - Friends : Contact list } - -let Contact = Define.Object("Contact", [ Define.Field("email", StringType, fun _ x -> x.Email) ]) - -let Person = - Define.Object("Person", - [ Define.Field("id", IDType, fun _ x -> string x.ID) - Define.AutoField("firstName", StringType) - Define.Field("lastName", StringType, fun _ x -> x.LastName) - Define.Field("fullName", StringType, fun _ x -> x.FirstName + " " + x.LastName) - Define.Field("contact", Contact, fun _ x -> x.Contact) - Define.Field("email", StringType, fun _ x -> x.Contact.Email) - Define.Field("friends", ListOf Contact, fun _ x -> x.Friends) ]) - -let data = - [ { ID = 4 - FirstName = "Ben" - LastName = "Adams" - Contact = { Email = "b.adams@gmail.com" } - Friends = - [ { Email = "j.abrams@gmail.com" } - { Email = "l.trif@gmail.com" } ] } - { ID = 2 - FirstName = "Jonathan" - LastName = "Abrams" - Contact = { Email = "j.abrams@gmail.com" } - Friends = [] } - { ID = 7 - FirstName = "Jeneffer" - LastName = "Trif" - Contact = { Email = "j.trif@gmail.com" } - Friends = [ { Email = "j.abrams@gmail.com" } ] } ] - - -let internal undefined<'t> = Unchecked.defaultof<'t> - -let resolveRoot ctx () = - let info = ctx.ExecutionInfo - let queryable = data.AsQueryable() - let result = queryable.Apply(info) |> Seq.toList - result - -let linqArgs = - [ Define.Input("id", Nullable IDType) - Define.Input("skip", Nullable IntType) - Define.Input("take", Nullable IntType) - Define.Input("orderBy", Nullable StringType) - Define.Input("orderByDesc", Nullable StringType) - Define.Input("first", Nullable IntType) - Define.Input("last", Nullable IntType) - Define.Input("before", Nullable StringType) - Define.Input("after", Nullable StringType) ] - -let schema = - Schema(Define.Object("RootQuery", - [ Define.Field("people", ListOf Person, "", linqArgs, - fun ctx () -> - let info = ctx.ExecutionInfo - let queryable = data.AsQueryable() - let result = queryable.Apply(info) |> Seq.toList - result) ])) - -let schemaProcessor = Executor(schema) - -[] -let ``LINQ interpreter works with auto-fields``() = - let plan = schemaProcessor.CreateExecutionPlanOrFail """ - query Example { - people { - firstName - } - } - """ - let info = plan.["people"] - let people = data.AsQueryable().Apply(info) |> Seq.toList - List.length people |> equals 3 - let result = List.head people - result.FirstName |> equals "Ben" - result.LastName |> equals undefined - result.Contact |> equals undefined - result.Friends |> equals undefined - -[] -let ``LINQ interpreter works with fields with defined resolvers``() = - let plan = schemaProcessor.CreateExecutionPlanOrFail """ - query Example { - people { - lastName - } - } - """ - let info = plan.["people"] - let people = data.AsQueryable().Apply(info) |> Seq.toList - List.length people |> equals 3 - let result = List.head people - result.FirstName |> equals undefined - result.LastName |> equals "Adams" - result.Contact |> equals undefined - result.Friends |> equals undefined - -[] -let ``LINQ interpreter works with fields referring to nested property resolver``() = - let plan = schemaProcessor.CreateExecutionPlanOrFail """ - query Example { - people { - contact { email } - } - } - """ - let info = plan.["people"] - let people = data.AsQueryable().Apply(info) |> Seq.toList - List.length people |> equals 3 - let result = List.head people - result.FirstName |> equals undefined - result.LastName |> equals undefined - result.Contact |> equals { Email = "b.adams@gmail.com" } - result.Friends |> equals undefined - -[] -let ``LINQ interpreter works with nested collections``() = - let plan = schemaProcessor.CreateExecutionPlanOrFail """ - query Example { - people { - friends { email } - } - } - """ - let info = plan.["people"] - let people = data.AsQueryable().Apply(info) |> Seq.toList - List.length people |> equals 3 - let result = List.head people - result.FirstName |> equals undefined - result.LastName |> equals undefined - result.Contact |> equals undefined - result.Friends |> equals [ { Email = "j.abrams@gmail.com" } - { Email = "l.trif@gmail.com" } ] - -[] -let ``LINQ interpreter works with nested property getters in resolve function``() = - let plan = schemaProcessor.CreateExecutionPlanOrFail """ - query Example { - people { - email - } - } - """ - let info = plan.["people"] - let people = data.AsQueryable().Apply(info) |> Seq.toList - List.length people |> equals 3 - let result = List.head people - result.FirstName |> equals undefined - result.LastName |> equals undefined - // this should be resolved, because email resolver is: fun _ x -> x.Contact.Email - result.Contact |> equals { Email = "b.adams@gmail.com" } - result.Friends |> equals undefined - -[] -let ``LINQ interpreter resolves multiple properties from complex resolvers``() = - let plan = schemaProcessor.CreateExecutionPlanOrFail """ - query Example { - people { - fullName - } - } - """ - let info = plan.["people"] - let people = data.AsQueryable().Apply(info) |> Seq.toList - List.length people |> equals 3 - let result = List.head people - // both FirstName and LastName should be resolved, because - // they are accessed from within fullName function resolver - result.FirstName |> equals "Ben" - result.LastName |> equals "Adams" - result.Contact |> equals undefined - result.Friends |> equals undefined - -[] -let ``LINQ interpreter works with id arg``() = - let plan = schemaProcessor.CreateExecutionPlanOrFail """ - query Example { - people(id: 2) { - id - firstName - } - } - """ - let info = plan.["people"] - let people = data.AsQueryable().Apply(info) |> Seq.toList - List.length people |> equals 1 - let result = List.head people - result.ID |> equals 2 - result.FirstName |> equals "Jonathan" - result.LastName |> equals undefined - result.Contact |> equals undefined - result.Friends |> equals undefined - -[] -let ``LINQ interpreter works with skip arg``() = - let plan = schemaProcessor.CreateExecutionPlanOrFail """ - query Example { - people(skip: 2) { - id - firstName - } - } - """ - let info = plan.["people"] - let people = data.AsQueryable().Apply(info) |> Seq.toList - List.length people |> equals 1 - let result = List.head people - result.ID |> equals 7 - result.FirstName |> equals "Jeneffer" - result.LastName |> equals undefined - result.Contact |> equals undefined - result.Friends |> equals undefined - -[] -let ``LINQ interpreter works with take arg``() = - let plan = schemaProcessor.CreateExecutionPlanOrFail """ - query Example { - people(take: 2) { - id - firstName - } - } - """ - let info = plan.["people"] - let people = data.AsQueryable().Apply(info) |> Seq.toList - List.length people |> equals 2 - let result = people |> List.map (fun p -> (p.ID, p.FirstName)) - result |> equals [ (4, "Ben") - (2, "Jonathan") ] - -[] -let ``LINQ interpreter works with orderBy arg``() = - let plan = schemaProcessor.CreateExecutionPlanOrFail """ - query Example { - people(orderBy: "firstName") { - id - firstName - } - } - """ - let info = plan.["people"] - let people = data.AsQueryable().Apply(info) |> Seq.toList - List.length people |> equals 3 - let result = people |> List.map (fun p -> (p.ID, p.FirstName)) - result |> equals [ (4, "Ben") - (7, "Jeneffer") - (2, "Jonathan") ] - -[] -let ``LINQ interpreter works with orderByDesc arg``() = - let plan = schemaProcessor.CreateExecutionPlanOrFail """ - query Example { - people(orderByDesc: "firstName") { - id - firstName - } - } - """ - let info = plan.["people"] - let people = data.AsQueryable().Apply(info) |> Seq.toList - List.length people |> equals 3 - let result = people |> List.map (fun p -> (p.ID, p.FirstName)) - result |> equals [ (2, "Jonathan") - (7, "Jeneffer") - (4, "Ben") ] - -[] -let ``ObjectListFilter works with Equals operator``() = - let filter = Equals { FieldName = "firstName"; Value = "Jonathan" } // :> IComparable - let queryable = data.AsQueryable() - let filteredData = filter.Apply(queryable) |> Seq.toList - List.length filteredData |> equals 1 - let result = List.head filteredData - result.ID |> equals 2 - result.FirstName |> equals "Jonathan" - result.LastName |> equals "Abrams" - result.Contact |> equals { Email = "j.abrams@gmail.com" } - result.Friends |> equals [] - -[] -let ``ObjectListFilter works with GreaterThan operator``() = - let filter = GreaterThan { FieldName = "id"; Value = 4 } // :> IComparable - let queryable = data.AsQueryable() - let filteredData = filter.Apply(queryable) |> Seq.toList - List.length filteredData |> equals 1 - let result = List.head filteredData - result.ID |> equals 7 - result.FirstName |> equals "Jeneffer" - result.LastName |> equals "Trif" - result.Contact |> equals { Email = "j.trif@gmail.com" } - result.Friends |> equals [ { Email = "j.abrams@gmail.com" } ] - -[] -let ``ObjectListFilter works with LessThan operator``() = - let filter = LessThan { FieldName = "id"; Value = 4 } // :> IComparable - let queryable = data.AsQueryable() - let filteredData = filter.Apply(queryable) |> Seq.toList - List.length filteredData |> equals 1 - let result = List.head filteredData - result.ID |> equals 2 - result.FirstName |> equals "Jonathan" - result.LastName |> equals "Abrams" - result.Contact |> equals { Email = "j.abrams@gmail.com" } - result.Friends |> equals [] - -[] -let ``ObjectListFilter works with StartsWith operator``() = - let filter = StartsWith { FieldName = "firstName"; Value = "J" } - let queryable = data.AsQueryable() - let filteredData = filter.Apply(queryable) |> Seq.toList - List.length filteredData |> equals 2 - let result = List.head filteredData - result.ID |> equals 2 - result.FirstName |> equals "Jonathan" - result.LastName |> equals "Abrams" - result.Contact |> equals { Email = "j.abrams@gmail.com" } - result.Friends |> equals [] - -[] -let ``ObjectListFilter works with Contains operator``() = - let filter = Contains { FieldName = "firstName"; Value = "en" } - let queryable = data.AsQueryable() - let filteredData = filter.Apply(queryable) |> Seq.toList - List.length filteredData |> equals 2 - let result = List.head filteredData - result.ID |> equals 4 - result.FirstName |> equals "Ben" - result.LastName |> equals "Adams" - result.Contact |> equals { Email = "b.adams@gmail.com" } - result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] - -[] -let ``ObjectListFilter works with EndsWith operator``() = - let filter = EndsWith { FieldName = "lastName"; Value = "ams" } - let queryable = data.AsQueryable() - let filteredData = filter.Apply(queryable) |> Seq.toList - List.length filteredData |> equals 2 - let result = List.head filteredData - result.ID |> equals 4 - result.FirstName |> equals "Ben" - result.LastName |> equals "Adams" - result.Contact |> equals { Email = "b.adams@gmail.com" } - result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] - -[] -let ``ObjectListFilter works with AND operator``() = - let filter = - And ( - Contains { FieldName = "firstName"; Value = "en" }, - Equals { FieldName = "lastName"; Value = "Adams" } - ) - let queryable = data.AsQueryable() - let filteredData = filter.Apply(queryable) |> Seq.toList - List.length filteredData |> equals 1 - let result = List.head filteredData - result.ID |> equals 4 - result.FirstName |> equals "Ben" - result.LastName |> equals "Adams" - result.Contact |> equals { Email = "b.adams@gmail.com" } - result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] - -[] -let ``ObjectListFilter works with OR operator``() = - let filter = - Or ( - GreaterThan { FieldName = "id"; Value = 4 }, - Equals { FieldName = "lastName"; Value = "Adams" } - ) - let queryable = data.AsQueryable() - let filteredData = filter.Apply(queryable) |> Seq.toList - List.length filteredData |> equals 2 - let result = List.head filteredData - result.ID |> equals 4 - result.FirstName |> equals "Ben" - result.LastName |> equals "Adams" - result.Contact |> equals { Email = "b.adams@gmail.com" } - result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] - -//[] -//let ``ObjectListFilter works with FilterField operator``() = -// let filter = -// FilterField { FieldName = "Friends"; Value = Contains { FieldName = "Email"; Value = "l.trif@gmail.com" } } -// let queryable = data.AsQueryable() -// let filteredData = filter.Apply(queryable) |> Seq.toList -// List.length filteredData |> equals 1 -// let result = List.head filteredData -// result.ID |> equals 4 -// result.FirstName |> equals "Ben" -// result.LastName |> equals "Adams" -// result.Contact |> equals { Email = "b.adams@gmail.com" } -// result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] - -[] -let ``ObjectListFilter works with NOT operator``() = - let filter = - Not (Equals { FieldName = "lastName"; Value = "Adams" }) - let queryable = data.AsQueryable() - let filteredData = filter.Apply(queryable) |> Seq.toList - List.length filteredData |> equals 2 - let result1 = List.head filteredData - result1.ID |> equals 2 - result1.FirstName |> equals "Jonathan" - result1.LastName |> equals "Abrams" - result1.Contact |> equals { Email = "j.abrams@gmail.com" } - result1.Friends |> equals [] - -type Complex = - { ID : int - Name : string - Discriminator : string } - -type Building = - { ID : int - Name : string - Discriminator : string } - -type Community = - { ID : int - Name : string - Discriminator : string - Complexes : int list - Buildings : int list } - -type Property = - | Complex of Complex - | Building of Building - | Community of Community - - -[] -let ``ObjectListFilter works with getDiscriminator for Complex``() = - let propertyData: Property list = - [ - Complex { ID = 1; Name = "Complex A"; Discriminator = typeof.FullName } - Building { ID = 2; Name = "Building B"; Discriminator = typeof.FullName } - Community { ID = 3; Name = "Community C"; Discriminator = typeof.FullName; Complexes = [1]; Buildings = [2] } - Complex { ID = 4; Name = "Complex AA"; Discriminator = typeof.FullName } - Building { ID = 5; Name = "Building BB"; Discriminator = typeof.FullName } - Community { ID = 6; Name = "Community CC"; Discriminator = typeof.FullName; Complexes = [4]; Buildings = [5] } - ] - let queryable = propertyData.AsQueryable() - let filter = OfTypes [typeof] - let options = - ObjectListFilterLinqOptions( - (function - | Complex c -> c.Discriminator - | Building b -> b.Discriminator - | Community c -> c.Discriminator)) - let filteredData = filter.Apply(queryable,options) |> Seq.toList - List.length filteredData |> equals 2 - let result1 = List.head filteredData - match result1 with - | Complex c -> - c.ID |> equals 1 - c.Name |> equals "Complex A" - | _ -> failwith "Expected Complex" - let result2 = List.last filteredData - match result2 with - | Complex c -> - c.ID |> equals 4 - c.Name |> equals "Complex AA" - | _ -> failwith "Expected Complex" - - -[] -let ``ObjectListFilter works with getDiscriminator and getDiscriminatorValue for Complex``() = - let propertyData: Property list = - [ - Complex { ID = 1; Name = "Complex A"; Discriminator = typeof.Name} - Building { ID = 2; Name = "Building B"; Discriminator = typeof.Name } - Community { ID = 3; Name = "Community C"; Discriminator = typeof.Name; Complexes = [1]; Buildings = [2] } - Complex { ID = 4; Name = "Complex AA"; Discriminator = typeof.Name } - Building { ID = 5; Name = "Building BB"; Discriminator = typeof.Name } - Community { ID = 6; Name = "Community CC"; Discriminator = typeof.Name; Complexes = [4]; Buildings = [5] } - ] - let queryable = propertyData.AsQueryable() - let filter = OfTypes [typeof] - let options = - ObjectListFilterLinqOptions( - (function - | Complex c -> c.Discriminator - | Building b -> b.Discriminator - | Community c -> c.Discriminator), - (function - | t when t = typeof -> "Complex" - | t when t = typeof -> "Building" - | t when t = typeof -> "Community" - | _ -> raise (NotSupportedException "Type not supported")) - ) - let filteredData = filter.Apply(queryable,options) |> Seq.toList - List.length filteredData |> equals 2 - let result1 = List.head filteredData - match result1 with - | Complex c -> - c.ID |> equals 1 - c.Name |> equals "Complex A" - | _ -> failwith "Expected Complex" - let result2 = List.last filteredData - match result2 with - | Complex c -> - c.ID |> equals 4 - c.Name |> equals "Complex AA" - | _ -> failwith "Expected Complex" - -type Cow = - { ID : int - Name : string - __typename : string } - -type Horse = - { ID : int - Name : string - __typename : string } - -let animalData = - [ - { ID = 1; Name = "Cow A"; __typename = typeof.Name } - { ID = 2; Name = "Horse B"; __typename = typeof.Name } - { ID = 3; Name = "Cow C"; __typename = typeof.Name } - { ID = 4; Name = "Horse D"; __typename = typeof.Name } - ] - -[] -let ``ObjectListFilter works with getDiscriminatorValue for Horse``() = - let queryable = animalData.AsQueryable() - let filter = OfTypes [typeof] - let options = - ObjectListFilterLinqOptions( - getDiscriminatorValue = (function - | t when t = typeof -> t.Name - | t when t = typeof -> t.Name - | _ -> raise (NotSupportedException "Type not supported")) - ) - let filteredData = filter.Apply(queryable, options) |> Seq.toList - List.length filteredData |> equals 2 - let result1 = List.head filteredData - match result1 with - | h -> - h.ID |> equals 2 - h.Name |> equals "Horse B" - let result2 = List.last filteredData - match result2 with - | h -> - h.ID |> equals 4 - h.Name |> equals "Horse D" diff --git a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs new file mode 100644 index 000000000..06c1f157f --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs @@ -0,0 +1,363 @@ +module FSharp.Data.GraphQL.Tests.ObjectListFilterLinqTests + +open Xunit +open System +open System.Linq +open FSharp.Data.GraphQL.Types +open FSharp.Data.GraphQL.Server.Middleware +open FSharp.Data.GraphQL.Tests.LinqTests + +[] +let ``ObjectListFilter works with Equals operator``() = + let filter = Equals { FieldName = "firstName"; Value = "Jonathan" } // :> IComparable + let queryable = data.AsQueryable() + let filteredData = queryable.Apply(filter) |> Seq.toList + List.length filteredData |> equals 1 + let result = List.head filteredData + result.ID |> equals 2 + result.FirstName |> equals "Jonathan" + result.LastName |> equals "Abrams" + result.Contact |> equals { Email = "j.abrams@gmail.com" } + result.Friends |> equals [] + +[] +let ``ObjectListFilter works with GreaterThan operator``() = + let filter = GreaterThan { FieldName = "id"; Value = 4 } // :> IComparable + let queryable = data.AsQueryable() + let filteredData = queryable.Apply(filter) |> Seq.toList + List.length filteredData |> equals 1 + let result = List.head filteredData + result.ID |> equals 7 + result.FirstName |> equals "Jeneffer" + result.LastName |> equals "Trif" + result.Contact |> equals { Email = "j.trif@gmail.com" } + result.Friends |> equals [ { Email = "j.abrams@gmail.com" } ] + +[] +let ``ObjectListFilter works with LessThan operator``() = + let filter = LessThan { FieldName = "id"; Value = 4 } // :> IComparable + let queryable = data.AsQueryable() + let filteredData = queryable.Apply(filter) |> Seq.toList + List.length filteredData |> equals 1 + let result = List.head filteredData + result.ID |> equals 2 + result.FirstName |> equals "Jonathan" + result.LastName |> equals "Abrams" + result.Contact |> equals { Email = "j.abrams@gmail.com" } + result.Friends |> equals [] + +[] +let ``ObjectListFilter works with StartsWith operator``() = + let filter = StartsWith { FieldName = "firstName"; Value = "J" } + let queryable = data.AsQueryable() + let filteredData = queryable.Apply(filter) |> Seq.toList + List.length filteredData |> equals 2 + let result = List.head filteredData + result.ID |> equals 2 + result.FirstName |> equals "Jonathan" + result.LastName |> equals "Abrams" + result.Contact |> equals { Email = "j.abrams@gmail.com" } + result.Friends |> equals [] + +[] +let ``ObjectListFilter works with Contains operator``() = + let filter = Contains { FieldName = "firstName"; Value = "en" } + let queryable = data.AsQueryable() + let filteredData = queryable.Apply(filter) |> Seq.toList + List.length filteredData |> equals 2 + let result = List.head filteredData + result.ID |> equals 4 + result.FirstName |> equals "Ben" + result.LastName |> equals "Adams" + result.Contact |> equals { Email = "b.adams@gmail.com" } + result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] + +[] +let ``ObjectListFilter works with EndsWith operator``() = + let filter = EndsWith { FieldName = "lastName"; Value = "ams" } + let queryable = data.AsQueryable() + let filteredData = queryable.Apply(filter) |> Seq.toList + List.length filteredData |> equals 2 + let result = List.head filteredData + result.ID |> equals 4 + result.FirstName |> equals "Ben" + result.LastName |> equals "Adams" + result.Contact |> equals { Email = "b.adams@gmail.com" } + result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] + +[] +let ``ObjectListFilter works with AND operator``() = + let filter = + And ( + Contains { FieldName = "firstName"; Value = "en" }, + Equals { FieldName = "lastName"; Value = "Adams" } + ) + let queryable = data.AsQueryable() + let filteredData = queryable.Apply(filter) |> Seq.toList + List.length filteredData |> equals 1 + let result = List.head filteredData + result.ID |> equals 4 + result.FirstName |> equals "Ben" + result.LastName |> equals "Adams" + result.Contact |> equals { Email = "b.adams@gmail.com" } + result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] + +[] +let ``ObjectListFilter works with OR operator``() = + let filter = + Or ( + GreaterThan { FieldName = "id"; Value = 4 }, + Equals { FieldName = "lastName"; Value = "Adams" } + ) + let queryable = data.AsQueryable() + let filteredData = queryable.Apply(filter) |> Seq.toList + List.length filteredData |> equals 2 + let result = List.head filteredData + result.ID |> equals 4 + result.FirstName |> equals "Ben" + result.LastName |> equals "Adams" + result.Contact |> equals { Email = "b.adams@gmail.com" } + result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] + +[] +let ``ObjectListFilter works with FilterField operator``() = + let filter = + FilterField { FieldName = "Friends"; Value = Contains { FieldName = "Email"; Value = "l.trif@gmail.com" } } + let queryable = data.AsQueryable() + let filteredData = queryable.Apply(filter) |> Seq.toList + List.length filteredData |> equals 1 + let result = List.head filteredData + result.ID |> equals 4 + result.FirstName |> equals "Ben" + result.LastName |> equals "Adams" + result.Contact |> equals { Email = "b.adams@gmail.com" } + result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] + +[] +let ``ObjectListFilter works with NOT operator``() = + let filter = + Not (Equals { FieldName = "lastName"; Value = "Adams" }) + let queryable = data.AsQueryable() + let filteredData = queryable.Apply(filter) |> Seq.toList + List.length filteredData |> equals 2 + let result1 = List.head filteredData + result1.ID |> equals 2 + result1.FirstName |> equals "Jonathan" + result1.LastName |> equals "Abrams" + result1.Contact |> equals { Email = "j.abrams@gmail.com" } + result1.Friends |> equals [] + +type Complex = + { ID : int + Name : string + Discriminator : string } + +type Building = + { ID : int + Name : string + Discriminator : string } + +type Community = + { ID : int + Name : string + Discriminator : string + Complexes : int list + Buildings : int list } + +type Property = + | Complex of Complex + | Building of Building + | Community of Community + + +[] +let ``ObjectListFilter works with getDiscriminator for Complex``() = + let propertyData: Property list = + [ + Complex { ID = 1; Name = "Complex A"; Discriminator = typeof.FullName } + Building { ID = 2; Name = "Building B"; Discriminator = typeof.FullName } + Community { ID = 3; Name = "Community C"; Discriminator = typeof.FullName; Complexes = [1]; Buildings = [2] } + Complex { ID = 4; Name = "Complex AA"; Discriminator = typeof.FullName } + Building { ID = 5; Name = "Building BB"; Discriminator = typeof.FullName } + Community { ID = 6; Name = "Community CC"; Discriminator = typeof.FullName; Complexes = [4]; Buildings = [5] } + ] + let queryable = propertyData.AsQueryable() + let filter = OfTypes [typeof] + let options = + ObjectListFilterLinqOptions( + (function + | Complex c -> c.Discriminator + | Building b -> b.Discriminator + | Community c -> c.Discriminator)) + let filteredData = queryable.Apply(filter, options) |> Seq.toList + List.length filteredData |> equals 2 + let result1 = List.head filteredData + match result1 with + | Complex c -> + c.ID |> equals 1 + c.Name |> equals "Complex A" + | _ -> failwith "Expected Complex" + let result2 = List.last filteredData + match result2 with + | Complex c -> + c.ID |> equals 4 + c.Name |> equals "Complex AA" + | _ -> failwith "Expected Complex" + + +[] +let ``ObjectListFilter works with getDiscriminator and getDiscriminatorValue for Complex``() = + let propertyData: Property list = + [ + Complex { ID = 1; Name = "Complex A"; Discriminator = typeof.Name} + Building { ID = 2; Name = "Building B"; Discriminator = typeof.Name } + Community { ID = 3; Name = "Community C"; Discriminator = typeof.Name; Complexes = [1]; Buildings = [2] } + Complex { ID = 4; Name = "Complex AA"; Discriminator = typeof.Name } + Building { ID = 5; Name = "Building BB"; Discriminator = typeof.Name } + Community { ID = 6; Name = "Community CC"; Discriminator = typeof.Name; Complexes = [4]; Buildings = [5] } + ] + let queryable = propertyData.AsQueryable() + let filter = OfTypes [typeof] + let options = + ObjectListFilterLinqOptions( + (function + | Complex c -> c.Discriminator + | Building b -> b.Discriminator + | Community c -> c.Discriminator), + (function + | t when t = typeof -> "Complex" + | t when t = typeof -> "Building" + | t when t = typeof -> "Community" + | _ -> raise (NotSupportedException "Type not supported")) + ) + let filteredData = queryable.Apply(filter, options) |> Seq.toList + List.length filteredData |> equals 2 + let result1 = List.head filteredData + match result1 with + | Complex c -> + c.ID |> equals 1 + c.Name |> equals "Complex A" + | _ -> failwith "Expected Complex" + let result2 = List.last filteredData + match result2 with + | Complex c -> + c.ID |> equals 4 + c.Name |> equals "Complex AA" + | _ -> failwith "Expected Complex" + +type Cow = + { ID : int + Name : string + Discriminator : string + __typename : string } + +type Horse = + { ID : int + Name : string + Discriminator : string + __typename : string } + +type Hamster = + { ID : int + Name : string + Discriminator : string + __typename : string } + +let animalData = + [ + { ID = 1; Discriminator="Cow"; Name = "Cow A"; __typename = typeof.Name } + { ID = 2; Discriminator="Horse"; Name = "Horse B"; __typename = typeof.Name } + { ID = 3; Discriminator="Cow"; Name = "Cow C"; __typename = typeof.Name } + { ID = 4; Discriminator="Horse"; Name = "Horse D"; __typename = typeof.Name } + { ID = 5; Discriminator="Hamster"; Name = "Hamster E"; __typename = typeof.Name } + ] + +[] +let ``ObjectListFilter works with getDiscriminatorValue for Horse``() = + let queryable = animalData.AsQueryable() + let filter = OfTypes [typeof] + let options = + ObjectListFilterLinqOptions( + getDiscriminatorValue = (function + | t when t = typeof -> t.Name + | t when t = typeof -> t.Name + | _ -> raise (NotSupportedException "Type not supported")) + ) + let filteredData = queryable.Apply(filter, options) |> Seq.toList + List.length filteredData |> equals 2 + let result1 = List.head filteredData + match result1 with + | h -> + h.ID |> equals 2 + h.Name |> equals "Horse B" + let result2 = List.last filteredData + match result2 with + | h -> + h.ID |> equals 4 + h.Name |> equals "Horse D" + +[] +let ``ObjectListFilter works with getDiscriminatorValue startsWith for Horse and Hamster``() = + + let queryable = animalData.AsQueryable() + let filter = StartsWith { FieldName = "Discriminator"; Value = "H" } + let options = + ObjectListFilterLinqOptions ( + (fun entity (discriminator: string) -> + entity.Discriminator.StartsWith discriminator), + getDiscriminatorValue = (function + | t when t = typeof -> t.Name + | t when t = typeof -> t.Name + | t when t = typeof -> t.Name + | _ -> raise (NotSupportedException "Type not supported")) + ) + let filteredData = queryable.Apply(filter, options) |> Seq.toList + List.length filteredData |> equals 3 + let result1 = List.head filteredData + match result1 with + | c -> + c.ID |> equals 2 + c.Name |> equals "Horse B" + let result2 = List.last filteredData + match result2 with + | c -> + c.ID |> equals 5 + c.Name |> equals "Hamster E" + +type Product = { + Name : string + Tags : string list +} + +let productList = + [ + { Name = "Product A"; Tags = ["Tag1"; "Tag2"] } + { Name = "Product B"; Tags = ["Tag2"; "Tag3"] } + { Name = "Product C"; Tags = ["Tag3"; "Tag4"] } + { Name = "Product D"; Tags = ["Tag4"; "Tag5"] } + ] +let productArray = productList.ToArray() + +[] +let ``ObjectListFilter works with Contains operator collection type properties``() = + let queryable = productList.AsQueryable() + let filter = Contains { FieldName = "Tags"; Value = "Tag3" } + let filteredData = queryable.Apply(filter) |> Seq.toList + List.length filteredData |> equals 2 + let result1 = List.head filteredData + result1.Name |> equals "Product B" + let result2 = List.last filteredData + result2.Name |> equals "Product C" + + +[] +let ``ObjectListFilter works with Contains operator collection type properties with array``() = + let queryable = productArray.AsQueryable() + let filter = Contains { FieldName = "Tags"; Value = "Tag3" } + let filteredData = queryable.Apply(filter) |> Seq.toList + List.length filteredData |> equals 2 + let result1 = List.head filteredData + result1.Name |> equals "Product B" + let result2 = List.last filteredData + result2.Name |> equals "Product C" + diff --git a/tests/FSharp.Data.GraphQL.Tests/SelectLinqTests.fs b/tests/FSharp.Data.GraphQL.Tests/SelectLinqTests.fs new file mode 100644 index 000000000..4b47580fa --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/SelectLinqTests.fs @@ -0,0 +1,288 @@ +// The MIT License (MIT) +// Copyright (c) 2016 Bazinga Technologies Inc +module FSharp.Data.GraphQL.Tests.LinqTests + +open Xunit +open System +open System.Linq +open FSharp.Data.GraphQL +open FSharp.Data.GraphQL.Linq +open FSharp.Data.GraphQL.Types + +type Contact = + { Email : string } + +type Person = + { ID : int + FirstName : string + LastName : string + Contact : Contact + Friends : Contact list } + +let Contact = Define.Object("Contact", [ Define.Field("email", StringType, fun _ x -> x.Email) ]) + +let Person = + Define.Object("Person", + [ Define.Field("id", IDType, fun _ x -> string x.ID) + Define.AutoField("firstName", StringType) + Define.Field("lastName", StringType, fun _ x -> x.LastName) + Define.Field("fullName", StringType, fun _ x -> x.FirstName + " " + x.LastName) + Define.Field("contact", Contact, fun _ x -> x.Contact) + Define.Field("email", StringType, fun _ x -> x.Contact.Email) + Define.Field("friends", ListOf Contact, fun _ x -> x.Friends) ]) + +let data = + [ { ID = 4 + FirstName = "Ben" + LastName = "Adams" + Contact = { Email = "b.adams@gmail.com" } + Friends = + [ { Email = "j.abrams@gmail.com" } + { Email = "l.trif@gmail.com" } ] } + { ID = 2 + FirstName = "Jonathan" + LastName = "Abrams" + Contact = { Email = "j.abrams@gmail.com" } + Friends = [] } + { ID = 7 + FirstName = "Jeneffer" + LastName = "Trif" + Contact = { Email = "j.trif@gmail.com" } + Friends = [ { Email = "j.abrams@gmail.com" } ] } ] + + +let internal undefined<'t> = Unchecked.defaultof<'t> + +let resolveRoot ctx () = + let info = ctx.ExecutionInfo + let queryable = data.AsQueryable() + let result = queryable.Apply(info) |> Seq.toList + result + +let linqArgs = + [ Define.Input("id", Nullable IDType) + Define.Input("skip", Nullable IntType) + Define.Input("take", Nullable IntType) + Define.Input("orderBy", Nullable StringType) + Define.Input("orderByDesc", Nullable StringType) + Define.Input("first", Nullable IntType) + Define.Input("last", Nullable IntType) + Define.Input("before", Nullable StringType) + Define.Input("after", Nullable StringType) ] + +let schema = + Schema(Define.Object("RootQuery", + [ Define.Field("people", ListOf Person, "", linqArgs, + fun ctx () -> + let info = ctx.ExecutionInfo + let queryable = data.AsQueryable() + let result = queryable.Apply(info) |> Seq.toList + result) ])) + +let schemaProcessor = Executor(schema) + +[] +let ``LINQ interpreter works with auto-fields``() = + let plan = schemaProcessor.CreateExecutionPlanOrFail """ + query Example { + people { + firstName + } + } + """ + let info = plan.["people"] + let people = data.AsQueryable().Apply(info) |> Seq.toList + List.length people |> equals 3 + let result = List.head people + result.FirstName |> equals "Ben" + result.LastName |> equals undefined + result.Contact |> equals undefined + result.Friends |> equals undefined + +[] +let ``LINQ interpreter works with fields with defined resolvers``() = + let plan = schemaProcessor.CreateExecutionPlanOrFail """ + query Example { + people { + lastName + } + } + """ + let info = plan.["people"] + let people = data.AsQueryable().Apply(info) |> Seq.toList + List.length people |> equals 3 + let result = List.head people + result.FirstName |> equals undefined + result.LastName |> equals "Adams" + result.Contact |> equals undefined + result.Friends |> equals undefined + +[] +let ``LINQ interpreter works with fields referring to nested property resolver``() = + let plan = schemaProcessor.CreateExecutionPlanOrFail """ + query Example { + people { + contact { email } + } + } + """ + let info = plan.["people"] + let people = data.AsQueryable().Apply(info) |> Seq.toList + List.length people |> equals 3 + let result = List.head people + result.FirstName |> equals undefined + result.LastName |> equals undefined + result.Contact |> equals { Email = "b.adams@gmail.com" } + result.Friends |> equals undefined + +[] +let ``LINQ interpreter works with nested collections``() = + let plan = schemaProcessor.CreateExecutionPlanOrFail """ + query Example { + people { + friends { email } + } + } + """ + let info = plan.["people"] + let people = data.AsQueryable().Apply(info) |> Seq.toList + List.length people |> equals 3 + let result = List.head people + result.FirstName |> equals undefined + result.LastName |> equals undefined + result.Contact |> equals undefined + result.Friends |> equals [ { Email = "j.abrams@gmail.com" } + { Email = "l.trif@gmail.com" } ] + +[] +let ``LINQ interpreter works with nested property getters in resolve function``() = + let plan = schemaProcessor.CreateExecutionPlanOrFail """ + query Example { + people { + email + } + } + """ + let info = plan.["people"] + let people = data.AsQueryable().Apply(info) |> Seq.toList + List.length people |> equals 3 + let result = List.head people + result.FirstName |> equals undefined + result.LastName |> equals undefined + // this should be resolved, because email resolver is: fun _ x -> x.Contact.Email + result.Contact |> equals { Email = "b.adams@gmail.com" } + result.Friends |> equals undefined + +[] +let ``LINQ interpreter resolves multiple properties from complex resolvers``() = + let plan = schemaProcessor.CreateExecutionPlanOrFail """ + query Example { + people { + fullName + } + } + """ + let info = plan.["people"] + let people = data.AsQueryable().Apply(info) |> Seq.toList + List.length people |> equals 3 + let result = List.head people + // both FirstName and LastName should be resolved, because + // they are accessed from within fullName function resolver + result.FirstName |> equals "Ben" + result.LastName |> equals "Adams" + result.Contact |> equals undefined + result.Friends |> equals undefined + +[] +let ``LINQ interpreter works with id arg``() = + let plan = schemaProcessor.CreateExecutionPlanOrFail """ + query Example { + people(id: 2) { + id + firstName + } + } + """ + let info = plan.["people"] + let people = data.AsQueryable().Apply(info) |> Seq.toList + List.length people |> equals 1 + let result = List.head people + result.ID |> equals 2 + result.FirstName |> equals "Jonathan" + result.LastName |> equals undefined + result.Contact |> equals undefined + result.Friends |> equals undefined + +[] +let ``LINQ interpreter works with skip arg``() = + let plan = schemaProcessor.CreateExecutionPlanOrFail """ + query Example { + people(skip: 2) { + id + firstName + } + } + """ + let info = plan.["people"] + let people = data.AsQueryable().Apply(info) |> Seq.toList + List.length people |> equals 1 + let result = List.head people + result.ID |> equals 7 + result.FirstName |> equals "Jeneffer" + result.LastName |> equals undefined + result.Contact |> equals undefined + result.Friends |> equals undefined + +[] +let ``LINQ interpreter works with take arg``() = + let plan = schemaProcessor.CreateExecutionPlanOrFail """ + query Example { + people(take: 2) { + id + firstName + } + } + """ + let info = plan.["people"] + let people = data.AsQueryable().Apply(info) |> Seq.toList + List.length people |> equals 2 + let result = people |> List.map (fun p -> (p.ID, p.FirstName)) + result |> equals [ (4, "Ben") + (2, "Jonathan") ] + +[] +let ``LINQ interpreter works with orderBy arg``() = + let plan = schemaProcessor.CreateExecutionPlanOrFail """ + query Example { + people(orderBy: "firstName") { + id + firstName + } + } + """ + let info = plan.["people"] + let people = data.AsQueryable().Apply(info) |> Seq.toList + List.length people |> equals 3 + let result = people |> List.map (fun p -> (p.ID, p.FirstName)) + result |> equals [ (4, "Ben") + (7, "Jeneffer") + (2, "Jonathan") ] + +[] +let ``LINQ interpreter works with orderByDesc arg``() = + let plan = schemaProcessor.CreateExecutionPlanOrFail """ + query Example { + people(orderByDesc: "firstName") { + id + firstName + } + } + """ + let info = plan.["people"] + let people = data.AsQueryable().Apply(info) |> Seq.toList + List.length people |> equals 3 + let result = people |> List.map (fun p -> (p.ID, p.FirstName)) + result |> equals [ (2, "Jonathan") + (7, "Jeneffer") + (4, "Ben") ] + From c9dd83ea43e0a26f14f8f42f19d49ef82686885c Mon Sep 17 00:00:00 2001 From: Viktor Tochonov Date: Fri, 14 Mar 2025 14:47:34 +0200 Subject: [PATCH 07/14] Implemented logic for different MemberInfo types in ObjectListFilter `Contains` case --- .../ObjectListFilter.fs | 171 +++++++++++------- 1 file changed, 109 insertions(+), 62 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs index 0920bcb5c..7f1a6b54a 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs @@ -20,6 +20,75 @@ type ObjectListFilter = | FilterField of FieldFilter | NoFilter +open System.Linq +open System.Linq.Expressions +open System.Runtime.InteropServices +open System.Reflection + +/// +/// Represents a value that can be either None or Some. +/// + +/// +/// Allows to specify discriminator comparison or discriminator getter +/// and a function that return discriminator value depending on entity type +/// +/// +/// // discriminator custom condition +/// let result () = +/// queryable.Apply( +/// filter, +/// ObjectListFilterLinqOptions ( +/// (fun entity discriminator -> entity.Discriminator.StartsWith discriminator), +/// (function +/// | t when Type.(=)(t, typeof) -> "cat+v1" +/// | t when Type.(=)(t, typeof) -> "dog+v1") +/// ) +/// ) +/// +/// +/// // discriminator equals +/// let result () = +/// queryable.Apply( +/// filter, +/// ObjectListFilterLinqOptions ( +/// (fun entity -> entity.Discriminator), +/// (function +/// | t when Type.(=)(t, typeof) -> "cat" +/// | t when Type.(=)(t, typeof) -> "dog") +/// ) +/// ) +/// +[] +type ObjectListFilterLinqOptions<'T, 'D> ( + [] compareDiscriminator : Expression> | null, + [] getDiscriminatorValue : (Type -> 'D) | null, + [] serializeMemberName : (MemberInfo -> string) | null +) = + + member _.CompareDiscriminator = compareDiscriminator |> ValueOption.ofObj + member _.GetDiscriminatorValue = getDiscriminatorValue |> ValueOption.ofObj + //member _.SerializeMemberName = serializeMemberName |> ValueOption.ofObj + + static member None = ObjectListFilterLinqOptions<'T, 'D> (null, null, null) + + static member GetCompareDiscriminator (getDiscriminatorValue : Expression>) = + let tParam = Expression.Parameter (typeof<'T>, "x") + let dParam = Expression.Parameter (typeof<'D>, "d") + let body = Expression.Equal(Expression.Invoke(getDiscriminatorValue, tParam), dParam) + Expression.Lambda> (body, tParam, dParam) + + new (getDiscriminator : Expression>) = ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, null, null) + new (compareDiscriminator : Expression>) = ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator, null, null) + new (getDiscriminatorValue : Type -> 'D) = ObjectListFilterLinqOptions<'T, 'D> (null, getDiscriminatorValue, null) + //new (serializeMemberName : MemberInfo -> string) = ObjectListFilterLinqOptions<'T, 'D> (null, null, serializeMemberName) + + new (getDiscriminator : Expression>, getDiscriminatorValue : Type -> 'D) = ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, getDiscriminatorValue, null) + + //new (getDiscriminator : Expression>, serializeMemberName : MemberInfo -> string) = ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, null, serializeMemberName) + //new (compareDiscriminator : Expression>, serializeMemberName : MemberInfo -> string) = ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator, null, serializeMemberName) + //new (getDiscriminatorValue : Type -> 'D, serializeMemberName : MemberInfo -> string) = ObjectListFilterLinqOptions<'T, 'D> (null, getDiscriminatorValue, serializeMemberName) + /// Contains tooling for working with ObjectListFilter. module ObjectListFilter = /// Contains operators for building and comparing ObjectListFilter values. @@ -54,34 +123,6 @@ module ObjectListFilter = /// Creates a new ObjectListFilter representing a NOT opreation for the existing one. let ( !!! ) filter = Not filter -open System.Linq -open System.Linq.Expressions -open System.Runtime.InteropServices -open System.Reflection - -[] -module ObjectListFilterExtensions = - - - //// discriminator custom condition - //let a () = - // filter.Apply( - // queryable, - // <@ fun e d -> e.Discriminator.StartsWith d @>, - // function - // | t when Type.(=)(t, typeof) -> ResidentialPropertiesConstants.Discriminators.Complex - // | t when Type.(=)(t, typeof) -> ResidentialPropertiesConstants.Discriminators.Building - // ) - //// discriminator equals - //let b () = - // filter.Apply( - // queryable, - // <@ fun e -> e.Discriminator @>, - // function - // | t when Type.(=)(t, typeof) -> ResidentialPropertiesConstants.Discriminators.Complex - // | t when Type.(=)(t, typeof) -> ResidentialPropertiesConstants.Discriminators.Building - // ) - let private genericWhereMethod = typeof.GetMethods () |> Seq.where (fun m -> m.Name = "Where") @@ -98,6 +139,18 @@ module ObjectListFilterExtensions = let private StringStartsWithMethod = typeof.GetMethod ("StartsWith", [| typeof |]) let private StringEndsWithMethod = typeof.GetMethod ("EndsWith", [| typeof |]) let private StringContainsMethod = typeof.GetMethod ("Contains", [| typeof |]) + let private getEnumerableContainsMethod (memberType : Type) = + match memberType.GetMethods(BindingFlags.Instance &&& BindingFlags.Public).FirstOrDefault(fun m -> m.Name = "Contains" && m.GetParameters().Length = 2) with + | null -> + match typeof.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.GetType().GetInterfaces().First(fun i -> i.FullName.StartsWith "System.Collections.Generic.IEnumerable`1") + containsGenericStaticMethod.MakeGenericMethod([| ienumerable.GenericTypeArguments[0] |]) + | instanceContainsMethod -> instanceContainsMethod let getField (param : ParameterExpression) fieldName = Expression.PropertyOrField (param, fieldName) @@ -125,7 +178,18 @@ module ObjectListFilterExtensions = | EndsWith f -> Expression.Call (Expression.PropertyOrField (param, f.FieldName), StringEndsWithMethod, Expression.Constant (f.Value)) | Contains f -> - Expression.Call (Expression.PropertyOrField (param, f.FieldName), StringContainsMethod, Expression.Constant (f.Value)) + let ``member`` = Expression.PropertyOrField (param, f.FieldName) + let isEnumerable (memberType: Type) = + not (Type.(=)(memberType, typeof)) + && typeof.IsAssignableFrom(memberType) + && memberType.GetInterfaces().Any(fun i -> i.FullName.StartsWith "System.Collections.Generic.IEnumerable`1") + match ``member``.Member with + | :? PropertyInfo as prop when prop.PropertyType |> isEnumerable -> + Expression.Call (getEnumerableContainsMethod prop.PropertyType, Expression.PropertyOrField (param, f.FieldName), 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)) + | _ -> + Expression.Call (``member``, StringContainsMethod, Expression.Constant (f.Value)) | OfTypes types -> types |> Seq.map (fun t -> buildTypeDiscriminatorCheck param t) @@ -134,39 +198,7 @@ module ObjectListFilterExtensions = let paramExpr = Expression.PropertyOrField (param, f.FieldName) buildFilterExpr (SourceExpression paramExpr) buildTypeDiscriminatorCheck f.Value - [] - type ObjectListFilterLinqOptions<'T, 'D> ( - [] compareDiscriminator : Expression> | null, - [] getDiscriminatorValue : (Type -> 'D) | null, - [] serializeMemberName : (MemberInfo -> string) | null) = - - member _.CompareDiscriminator = compareDiscriminator |> ValueOption.ofObj - member _.GetDiscriminatorValue = getDiscriminatorValue |> ValueOption.ofObj - //member _.SerializeMemberName = serializeMemberName |> ValueOption.ofObj - - static member None = ObjectListFilterLinqOptions<'T, 'D> (null, null, null) - - static member GetCompareDiscriminator (getDiscriminatorValue : Expression>) = - let tParam = Expression.Parameter (typeof<'T>, "x") - let dParam = Expression.Parameter (typeof<'D>, "d") - let body = Expression.Equal(Expression.Invoke(getDiscriminatorValue, tParam), dParam) - Expression.Lambda> (body, tParam, dParam) - - new (getDiscriminator : Expression>) = ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, null, null) - new (compareDiscriminator : Expression>) = ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator, null, null) - new (getDiscriminatorValue : Type -> 'D) = ObjectListFilterLinqOptions<'T, 'D> (null, getDiscriminatorValue, null) - //new (serializeMemberName : MemberInfo -> string) = ObjectListFilterLinqOptions<'T, 'D> (null, null, serializeMemberName) - - new (getDiscriminator : Expression>, getDiscriminatorValue : Type -> 'D) = ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, getDiscriminatorValue, null) - - //new (getDiscriminator : Expression>, serializeMemberName : MemberInfo -> string) = ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, null, serializeMemberName) - //new (compareDiscriminator : Expression>, serializeMemberName : MemberInfo -> string) = ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator, null, serializeMemberName) - //new (getDiscriminatorValue : Type -> 'D, serializeMemberName : MemberInfo -> string) = ObjectListFilterLinqOptions<'T, 'D> (null, getDiscriminatorValue, serializeMemberName) - -type ObjectListFilter with - - member filter.Apply<'T, 'D> (query : IQueryable<'T>, [] options : ObjectListFilterLinqOptions<'T, 'D>) = - + let apply (options : ObjectListFilterLinqOptions<'T, 'D>) (filter : ObjectListFilter) (query : IQueryable<'T>) = match filter with | NoFilter -> query | _ -> @@ -196,3 +228,18 @@ type ObjectListFilter with whereExpr<'T> query param body // Create and execute the final expression query.Provider.CreateQuery<'T> (queryExpr) + +[] +module ObjectListFilterExtensions = + + open ObjectListFilter + + type ObjectListFilter with + + member inline filter.ApplyTo<'T, 'D> (query : IQueryable<'T>, [] options : ObjectListFilterLinqOptions<'T, 'D>) = + apply options filter query + + type IQueryable<'T> with + + member inline query.Apply (filter : ObjectListFilter, [] options : ObjectListFilterLinqOptions<'T, 'D>) = + apply options filter query From 2dfff1626c79d972de29a510e23cf1b0f7823f3d Mon Sep 17 00:00:00 2001 From: Viktor Tochonov Date: Fri, 14 Mar 2025 18:10:28 +0200 Subject: [PATCH 08/14] Implemented IN case for ObjectListFilter and formatted files --- .../ObjectListFilter.fs | 37 ++++++++----------- .../ObjectListFilterLinqTests.fs | 34 +++++++++++++++++ .../SelectLinqTests.fs | 25 +++++++++---- 3 files changed, 68 insertions(+), 28 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs index 7f1a6b54a..58c43e273 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs @@ -13,6 +13,7 @@ type ObjectListFilter = | Equals of FieldFilter | GreaterThan of FieldFilter | LessThan of FieldFilter + | In of FieldFilter | StartsWith of FieldFilter | EndsWith of FieldFilter | Contains of FieldFilter @@ -25,10 +26,6 @@ open System.Linq.Expressions open System.Runtime.InteropServices open System.Reflection -/// -/// Represents a value that can be either None or Some. -/// - /// /// Allows to specify discriminator comparison or discriminator getter /// and a function that return discriminator value depending on entity type @@ -62,15 +59,13 @@ open System.Reflection [] type ObjectListFilterLinqOptions<'T, 'D> ( [] compareDiscriminator : Expression> | null, - [] getDiscriminatorValue : (Type -> 'D) | null, - [] serializeMemberName : (MemberInfo -> string) | null + [] getDiscriminatorValue : (Type -> 'D) | null ) = member _.CompareDiscriminator = compareDiscriminator |> ValueOption.ofObj member _.GetDiscriminatorValue = getDiscriminatorValue |> ValueOption.ofObj - //member _.SerializeMemberName = serializeMemberName |> ValueOption.ofObj - static member None = ObjectListFilterLinqOptions<'T, 'D> (null, null, null) + static member None = ObjectListFilterLinqOptions<'T, 'D> (null, null) static member GetCompareDiscriminator (getDiscriminatorValue : Expression>) = let tParam = Expression.Parameter (typeof<'T>, "x") @@ -78,16 +73,11 @@ type ObjectListFilterLinqOptions<'T, 'D> ( let body = Expression.Equal(Expression.Invoke(getDiscriminatorValue, tParam), dParam) Expression.Lambda> (body, tParam, dParam) - new (getDiscriminator : Expression>) = ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, null, null) - new (compareDiscriminator : Expression>) = ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator, null, null) - new (getDiscriminatorValue : Type -> 'D) = ObjectListFilterLinqOptions<'T, 'D> (null, getDiscriminatorValue, null) - //new (serializeMemberName : MemberInfo -> string) = ObjectListFilterLinqOptions<'T, 'D> (null, null, serializeMemberName) - - new (getDiscriminator : Expression>, getDiscriminatorValue : Type -> 'D) = ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, getDiscriminatorValue, null) + new (getDiscriminator : Expression>) = ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, null) + new (compareDiscriminator : Expression>) = ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator, null) + new (getDiscriminatorValue : Type -> 'D) = ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator = null , getDiscriminatorValue = getDiscriminatorValue) - //new (getDiscriminator : Expression>, serializeMemberName : MemberInfo -> string) = ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, null, serializeMemberName) - //new (compareDiscriminator : Expression>, serializeMemberName : MemberInfo -> string) = ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator, null, serializeMemberName) - //new (getDiscriminatorValue : Type -> 'D, serializeMemberName : MemberInfo -> string) = ObjectListFilterLinqOptions<'T, 'D> (null, getDiscriminatorValue, serializeMemberName) + new (getDiscriminator : Expression>, getDiscriminatorValue : Type -> 'D) = ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, getDiscriminatorValue) /// Contains tooling for working with ObjectListFilter. module ObjectListFilter = @@ -190,6 +180,11 @@ module ObjectListFilter = Expression.Call (getEnumerableContainsMethod field.FieldType, Expression.PropertyOrField (param, f.FieldName), Expression.Constant (f.Value)) | _ -> Expression.Call (``member``, StringContainsMethod, Expression.Constant (f.Value)) + | In f -> + let ``member`` = Expression.PropertyOrField (param, f.FieldName) + let values = f.Value |> List.map (fun v -> Expression.Equal(``member``, Expression.Constant(v))) + (values |> List.reduce (fun acc expr -> Expression.OrElse(acc, expr))) :> Expression + | OfTypes types -> types |> Seq.map (fun t -> buildTypeDiscriminatorCheck param t) @@ -208,19 +203,19 @@ module ObjectListFilter = | ValueNone, ValueNone -> // use __typename from filter and do type.ToSting() for values let typename = t.FullName - Expression.Equal(Expression.PropertyOrField(param, "__typename"), Expression.Constant(typename)) :> Expression + Expression.Equal (Expression.PropertyOrField (param, "__typename"), Expression.Constant (typename)) :> Expression | ValueSome discExpr, ValueNone -> // use discriminator and do type.ToSting() for values let typename = t.FullName - Expression.Invoke(discExpr, param, Expression.Constant(typename)) :> Expression + Expression.Invoke (discExpr, param, Expression.Constant (typename)) :> Expression | ValueNone, ValueSome discValueFn -> // use __typename from filter and execute discValueFn for values let discriminatorValue = discValueFn t - Expression.Equal(Expression.PropertyOrField(param, "__typename"), Expression.Constant(discriminatorValue)) :> Expression + Expression.Equal (Expression.PropertyOrField (param, "__typename"), Expression.Constant (discriminatorValue)) :> Expression | ValueSome discExpr, ValueSome discValueFn -> // use discriminator and execute discValueFn for values let discriminatorValue = discValueFn t - Expression.Invoke(discExpr, param, Expression.Constant (discriminatorValue)) + Expression.Invoke (discExpr, param, Expression.Constant (discriminatorValue)) let queryExpr = let param = Expression.Parameter (typeof<'T>, "x") diff --git a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs index 06c1f157f..6ad37113c 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs @@ -119,6 +119,40 @@ let ``ObjectListFilter works with OR operator``() = result.Contact |> equals { Email = "b.adams@gmail.com" } result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] +[] +let ``ObjectListFilter works with IN operator for string type field``() = + let filter = + In { FieldName = "firstName"; Value = ["Jeneffer"; "Ben"] } + let queryable = data.AsQueryable() + let filteredData = queryable.Apply(filter) |> Seq.toList + List.length filteredData |> equals 2 + let result = List.head filteredData + result.ID |> equals 4 + result.FirstName |> equals "Ben" + result.LastName |> equals "Adams" + result.Contact |> equals { Email = "b.adams@gmail.com" } + result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] + let result2 = List.last filteredData + result2.ID |> equals 7 + result2.FirstName |> equals "Jeneffer" + result2.LastName |> equals "Trif" + result2.Contact |> equals { Email = "j.trif@gmail.com" } + result2.Friends |> equals [ { Email = "j.abrams@gmail.com" } ] + +[] +let ``ObjectListFilter works with IN operator for int type field``() = + let filter = + In { FieldName = "id"; Value = [4; 2; 7] } + let queryable = data.AsQueryable() + let filteredData = queryable.Apply(filter) |> Seq.toList + List.length filteredData |> equals 3 + let result = List.head filteredData + result.ID |> equals 4 + result.FirstName |> equals "Ben" + result.LastName |> equals "Adams" + result.Contact |> equals { Email = "b.adams@gmail.com" } + result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] + [] let ``ObjectListFilter works with FilterField operator``() = let filter = diff --git a/tests/FSharp.Data.GraphQL.Tests/SelectLinqTests.fs b/tests/FSharp.Data.GraphQL.Tests/SelectLinqTests.fs index 4b47580fa..02a8b9ad8 100644 --- a/tests/FSharp.Data.GraphQL.Tests/SelectLinqTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/SelectLinqTests.fs @@ -71,13 +71,24 @@ let linqArgs = Define.Input("after", Nullable StringType) ] let schema = - Schema(Define.Object("RootQuery", - [ Define.Field("people", ListOf Person, "", linqArgs, - fun ctx () -> - let info = ctx.ExecutionInfo - let queryable = data.AsQueryable() - let result = queryable.Apply(info) |> Seq.toList - result) ])) + Schema ( + Define.Object ( + "RootQuery", + [ + Define.Field ( + "people", + ListOf Person, + "", + linqArgs, + fun ctx () -> + let info = ctx.ExecutionInfo + let queryable = data.AsQueryable () + let result = queryable.Apply (info) |> Seq.toList + result + ) + ] + ) + ) let schemaProcessor = Executor(schema) From da759abe635dfe997152f56675d8081db84f5612 Mon Sep 17 00:00:00 2001 From: Viktor Tochonov Date: Fri, 14 Mar 2025 18:16:16 +0200 Subject: [PATCH 09/14] Revert Implemented IN case --- .../ObjectListFilter.fs | 6 ---- .../ObjectListFilterLinqTests.fs | 34 ------------------- 2 files changed, 40 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs index 58c43e273..f97af6af7 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs @@ -13,7 +13,6 @@ type ObjectListFilter = | Equals of FieldFilter | GreaterThan of FieldFilter | LessThan of FieldFilter - | In of FieldFilter | StartsWith of FieldFilter | EndsWith of FieldFilter | Contains of FieldFilter @@ -180,11 +179,6 @@ module ObjectListFilter = Expression.Call (getEnumerableContainsMethod field.FieldType, Expression.PropertyOrField (param, f.FieldName), Expression.Constant (f.Value)) | _ -> Expression.Call (``member``, StringContainsMethod, Expression.Constant (f.Value)) - | In f -> - let ``member`` = Expression.PropertyOrField (param, f.FieldName) - let values = f.Value |> List.map (fun v -> Expression.Equal(``member``, Expression.Constant(v))) - (values |> List.reduce (fun acc expr -> Expression.OrElse(acc, expr))) :> Expression - | OfTypes types -> types |> Seq.map (fun t -> buildTypeDiscriminatorCheck param t) diff --git a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs index 6ad37113c..06c1f157f 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs @@ -119,40 +119,6 @@ let ``ObjectListFilter works with OR operator``() = result.Contact |> equals { Email = "b.adams@gmail.com" } result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] -[] -let ``ObjectListFilter works with IN operator for string type field``() = - let filter = - In { FieldName = "firstName"; Value = ["Jeneffer"; "Ben"] } - let queryable = data.AsQueryable() - let filteredData = queryable.Apply(filter) |> Seq.toList - List.length filteredData |> equals 2 - let result = List.head filteredData - result.ID |> equals 4 - result.FirstName |> equals "Ben" - result.LastName |> equals "Adams" - result.Contact |> equals { Email = "b.adams@gmail.com" } - result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] - let result2 = List.last filteredData - result2.ID |> equals 7 - result2.FirstName |> equals "Jeneffer" - result2.LastName |> equals "Trif" - result2.Contact |> equals { Email = "j.trif@gmail.com" } - result2.Friends |> equals [ { Email = "j.abrams@gmail.com" } ] - -[] -let ``ObjectListFilter works with IN operator for int type field``() = - let filter = - In { FieldName = "id"; Value = [4; 2; 7] } - let queryable = data.AsQueryable() - let filteredData = queryable.Apply(filter) |> Seq.toList - List.length filteredData |> equals 3 - let result = List.head filteredData - result.ID |> equals 4 - result.FirstName |> equals "Ben" - result.LastName |> equals "Adams" - result.Contact |> equals { Email = "b.adams@gmail.com" } - result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] - [] let ``ObjectListFilter works with FilterField operator``() = let filter = From 76071c5081fdf335aae5a57f19c0610aaec98d63 Mon Sep 17 00:00:00 2001 From: Viktor Tochonov Date: Fri, 14 Mar 2025 18:45:56 +0200 Subject: [PATCH 10/14] Fixed test ObjectListFilter works with FilterField operator --- .../ObjectListFilterLinqTests.fs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs index 06c1f157f..0391cff34 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs @@ -122,16 +122,16 @@ let ``ObjectListFilter works with OR operator``() = [] let ``ObjectListFilter works with FilterField operator``() = let filter = - FilterField { FieldName = "Friends"; Value = Contains { FieldName = "Email"; Value = "l.trif@gmail.com" } } + FilterField { FieldName = "Contact"; Value = Contains { FieldName = "Email"; Value = "j.trif@gmail.com" } } let queryable = data.AsQueryable() let filteredData = queryable.Apply(filter) |> Seq.toList List.length filteredData |> equals 1 let result = List.head filteredData - result.ID |> equals 4 - result.FirstName |> equals "Ben" - result.LastName |> equals "Adams" - result.Contact |> equals { Email = "b.adams@gmail.com" } - result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] + result.ID |> equals 7 + result.FirstName |> equals "Jeneffer" + result.LastName |> equals "Trif" + result.Contact |> equals { Email = "j.trif@gmail.com" } + result.Friends |> equals [ { Email = "j.abrams@gmail.com" } ] [] let ``ObjectListFilter works with NOT operator``() = @@ -360,4 +360,3 @@ let ``ObjectListFilter works with Contains operator collection type properties w result1.Name |> equals "Product B" let result2 = List.last filteredData result2.Name |> equals "Product C" - From 1d0f912e7d08355d55cae59aba97abf83403343b Mon Sep 17 00:00:00 2001 From: Viktor Tochonov Date: Fri, 14 Mar 2025 20:22:11 +0200 Subject: [PATCH 11/14] Fixed tests for Contains operator on different collection types --- .../ObjectListFilter.fs | 24 ++++---- .../ObjectListFilterLinqTests.fs | 58 +++++++++++++++---- 2 files changed, 58 insertions(+), 24 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs index f97af6af7..96a6a256d 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs @@ -129,17 +129,15 @@ module ObjectListFilter = let private StringEndsWithMethod = typeof.GetMethod ("EndsWith", [| typeof |]) let private StringContainsMethod = typeof.GetMethod ("Contains", [| typeof |]) let private getEnumerableContainsMethod (memberType : Type) = - match memberType.GetMethods(BindingFlags.Instance &&& BindingFlags.Public).FirstOrDefault(fun m -> m.Name = "Contains" && m.GetParameters().Length = 2) with - | null -> - match typeof.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.GetType().GetInterfaces().First(fun i -> i.FullName.StartsWith "System.Collections.Generic.IEnumerable`1") - containsGenericStaticMethod.MakeGenericMethod([| ienumerable.GenericTypeArguments[0] |]) - | instanceContainsMethod -> instanceContainsMethod + match typeof.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] |]) + let getField (param : ParameterExpression) fieldName = Expression.PropertyOrField (param, fieldName) @@ -174,7 +172,9 @@ module ObjectListFilter = && memberType.GetInterfaces().Any(fun i -> i.FullName.StartsWith "System.Collections.Generic.IEnumerable`1") match ``member``.Member with | :? PropertyInfo as prop when prop.PropertyType |> isEnumerable -> - Expression.Call (getEnumerableContainsMethod prop.PropertyType, Expression.PropertyOrField (param, f.FieldName), Expression.Constant (f.Value)) + 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)) | _ -> diff --git a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs index 0391cff34..86091b3e1 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs @@ -324,22 +324,20 @@ let ``ObjectListFilter works with getDiscriminatorValue startsWith for Horse and c.ID |> equals 5 c.Name |> equals "Hamster E" -type Product = { +type ListTagsProduct = { Name : string Tags : string list } -let productList = - [ - { Name = "Product A"; Tags = ["Tag1"; "Tag2"] } - { Name = "Product B"; Tags = ["Tag2"; "Tag3"] } - { Name = "Product C"; Tags = ["Tag3"; "Tag4"] } - { Name = "Product D"; Tags = ["Tag4"; "Tag5"] } - ] -let productArray = productList.ToArray() - [] -let ``ObjectListFilter works with Contains operator collection type properties``() = +let ``ObjectListFilter works with Contains operator on list collection properties``() = + let productList = + [ + { Name = "Product A"; Tags = ["Tag1"; "Tag2"] } + { Name = "Product B"; Tags = ["Tag2"; "Tag3"] } + { Name = "Product C"; Tags = ["Tag3"; "Tag4"] } + { Name = "Product D"; Tags = ["Tag4"; "Tag5"] } + ] let queryable = productList.AsQueryable() let filter = Contains { FieldName = "Tags"; Value = "Tag3" } let filteredData = queryable.Apply(filter) |> Seq.toList @@ -349,9 +347,45 @@ let ``ObjectListFilter works with Contains operator collection type properties`` let result2 = List.last filteredData result2.Name |> equals "Product C" +type ArrayTagsProduct = { + Name : string + Tags : string array +} + +[] +let ``ObjectListFilter works with Contains operator on array collection properties``() = + + let productArray = + [ + { Name = "Product A"; Tags = [|"Tag1"; "Tag2"|] } + { Name = "Product B"; Tags = [|"Tag2"; "Tag3"|] } + { Name = "Product C"; Tags = [|"Tag3"; "Tag4"|] } + { Name = "Product D"; Tags = [|"Tag4"; "Tag5"|] } + ] + let queryable = productArray.AsQueryable() + let filter = Contains { FieldName = "Tags"; Value = "Tag3" } + let filteredData = queryable.Apply(filter) |> Seq.toList + List.length filteredData |> equals 2 + let result1 = List.head filteredData + result1.Name |> equals "Product B" + let result2 = List.last filteredData + result2.Name |> equals "Product C" + +type SetTagsProduct = { + Name : string + Tags : string Set +} [] -let ``ObjectListFilter works with Contains operator collection type properties with array``() = +let ``ObjectListFilter works with Contains operator on set collection properties``() = + + let productArray = + [ + { Name = "Product A"; Tags = [|"Tag1"; "Tag2"|] |> Set.ofArray} + { Name = "Product B"; Tags = [|"Tag2"; "Tag3"|] |> Set.ofArray} + { Name = "Product C"; Tags = [|"Tag3"; "Tag4"|] |> Set.ofArray} + { Name = "Product D"; Tags = [|"Tag4"; "Tag5"|] |> Set.ofArray} + ] let queryable = productArray.AsQueryable() let filter = Contains { FieldName = "Tags"; Value = "Tag3" } let filteredData = queryable.Apply(filter) |> Seq.toList From fd37e4a004b188a5bd16c471f9f5f6208670c5c2 Mon Sep 17 00:00:00 2001 From: Viktor Tochonov Date: Mon, 17 Mar 2025 14:36:32 +0200 Subject: [PATCH 12/14] PR fixes --- src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs | 2 -- tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs index 96a6a256d..4ea0f8c45 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs @@ -75,7 +75,6 @@ type ObjectListFilterLinqOptions<'T, 'D> ( new (getDiscriminator : Expression>) = ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, null) new (compareDiscriminator : Expression>) = ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator, null) new (getDiscriminatorValue : Type -> 'D) = ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator = null , getDiscriminatorValue = getDiscriminatorValue) - new (getDiscriminator : Expression>, getDiscriminatorValue : Type -> 'D) = ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, getDiscriminatorValue) /// Contains tooling for working with ObjectListFilter. @@ -138,7 +137,6 @@ module ObjectListFilter = let ienumerable = memberType.GetInterfaces().First(fun i -> i.FullName.StartsWith "System.Collections.Generic.IEnumerable`1") containsGenericStaticMethod.MakeGenericMethod([| ienumerable.GenericTypeArguments[0] |]) - let getField (param : ParameterExpression) fieldName = Expression.PropertyOrField (param, fieldName) [] diff --git a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs index 86091b3e1..8613dcb52 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs @@ -372,8 +372,8 @@ let ``ObjectListFilter works with Contains operator on array collection properti result2.Name |> equals "Product C" type SetTagsProduct = { - Name : string - Tags : string Set + Name : string + Tags : string Set } [] From a4b7bfc1e5a63391cf2444fee2d593a8dc3d503c Mon Sep 17 00:00:00 2001 From: Viktor Tochonov Date: Mon, 17 Mar 2025 16:25:58 +0200 Subject: [PATCH 13/14] Changed comments in buildTypeDiscriminatorCheck --- .../ObjectListFilter.fs | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs index 4ea0f8c45..fd85f863c 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs @@ -193,22 +193,37 @@ module ObjectListFilter = let buildTypeDiscriminatorCheck (param : SourceExpression) (t : Type) = match options.CompareDiscriminator, options.GetDiscriminatorValue with | ValueNone, ValueNone -> - // use __typename from filter and do type.ToSting() for values - let typename = t.FullName - Expression.Equal (Expression.PropertyOrField (param, "__typename"), Expression.Constant (typename)) :> Expression + Expression.Equal ( + // Default discriminator property + Expression.PropertyOrField (param, "__typename"), + // Default discriminator value + Expression.Constant (t.FullName) + ) :> Expression | ValueSome discExpr, ValueNone -> - // use discriminator and do type.ToSting() for values - let typename = t.FullName - Expression.Invoke (discExpr, param, Expression.Constant (typename)) :> Expression + Expression.Invoke ( + // Provided discriminator comparison + discExpr, + param, + // Default discriminator value gathered from type + Expression.Constant(t.FullName) + ) :> Expression | ValueNone, ValueSome discValueFn -> - // use __typename from filter and execute discValueFn for values let discriminatorValue = discValueFn t - Expression.Equal (Expression.PropertyOrField (param, "__typename"), Expression.Constant (discriminatorValue)) :> Expression + Expression.Equal ( + // Default discriminator property + Expression.PropertyOrField (param, "__typename"), + // Provided discriminator value gathered from type + Expression.Constant (discriminatorValue) + ) :> Expression | ValueSome discExpr, ValueSome discValueFn -> - // use discriminator and execute discValueFn for values let discriminatorValue = discValueFn t - Expression.Invoke (discExpr, param, Expression.Constant (discriminatorValue)) - + Expression.Invoke ( + // Provided discriminator comparison + discExpr, + param, + // Provided discriminator value gathered from type + Expression.Constant (discriminatorValue) + ) let queryExpr = let param = Expression.Parameter (typeof<'T>, "x") let body = buildFilterExpr (SourceExpression param) buildTypeDiscriminatorCheck filter From 024ca3c416cb4f6679bedd9bc7e6c39e824227e5 Mon Sep 17 00:00:00 2001 From: Viktor Tochonov Date: Mon, 17 Mar 2025 16:28:21 +0200 Subject: [PATCH 14/14] Format file ObjectListFilter.fs --- .../ObjectListFilter.fs | 96 ++++++++++++------- 1 file changed, 61 insertions(+), 35 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs index fd85f863c..5be35c26c 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs @@ -56,10 +56,8 @@ open System.Reflection /// ) /// [] -type ObjectListFilterLinqOptions<'T, 'D> ( - [] compareDiscriminator : Expression> | null, - [] getDiscriminatorValue : (Type -> 'D) | null -) = +type ObjectListFilterLinqOptions<'T, 'D> + ([] compareDiscriminator : Expression> | null, [] getDiscriminatorValue : (Type -> 'D) | null) = member _.CompareDiscriminator = compareDiscriminator |> ValueOption.ofObj member _.GetDiscriminatorValue = getDiscriminatorValue |> ValueOption.ofObj @@ -69,13 +67,16 @@ type ObjectListFilterLinqOptions<'T, 'D> ( static member GetCompareDiscriminator (getDiscriminatorValue : Expression>) = let tParam = Expression.Parameter (typeof<'T>, "x") let dParam = Expression.Parameter (typeof<'D>, "d") - let body = Expression.Equal(Expression.Invoke(getDiscriminatorValue, tParam), dParam) + let body = Expression.Equal (Expression.Invoke (getDiscriminatorValue, tParam), dParam) Expression.Lambda> (body, tParam, dParam) - new (getDiscriminator : Expression>) = ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, null) + new (getDiscriminator : Expression>) = + ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, null) new (compareDiscriminator : Expression>) = ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator, null) - new (getDiscriminatorValue : Type -> 'D) = ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator = null , getDiscriminatorValue = getDiscriminatorValue) - new (getDiscriminator : Expression>, getDiscriminatorValue : Type -> 'D) = ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, getDiscriminatorValue) + new (getDiscriminatorValue : Type -> 'D) = + ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator = null, getDiscriminatorValue = getDiscriminatorValue) + new (getDiscriminator : Expression>, getDiscriminatorValue : Type -> 'D) = + ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, getDiscriminatorValue) /// Contains tooling for working with ObjectListFilter. module ObjectListFilter = @@ -128,15 +129,25 @@ module ObjectListFilter = let private StringEndsWithMethod = typeof.GetMethod ("EndsWith", [| typeof |]) let private StringContainsMethod = typeof.GetMethod ("Contains", [| typeof |]) let private getEnumerableContainsMethod (memberType : Type) = - match typeof.GetMethods(BindingFlags.Static ||| BindingFlags.Public).FirstOrDefault(fun m -> m.Name = "Contains" && m.GetParameters().Length = 2) with + match + typeof + .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) + 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] |]) - + let ienumerable = + memberType + .GetInterfaces() + .First (fun i -> i.FullName.StartsWith "System.Collections.Generic.IEnumerable`1") + containsGenericStaticMethod.MakeGenericMethod ([| ienumerable.GenericTypeArguments[0] |]) + let getField (param : ParameterExpression) fieldName = Expression.PropertyOrField (param, fieldName) [] @@ -158,25 +169,38 @@ module ObjectListFilter = | Equals f -> Expression.Equal (Expression.PropertyOrField (param, f.FieldName), Expression.Constant (f.Value)) | GreaterThan f -> Expression.GreaterThan (Expression.PropertyOrField (param, f.FieldName), Expression.Constant (f.Value)) | LessThan f -> Expression.LessThan (Expression.PropertyOrField (param, f.FieldName), Expression.Constant (f.Value)) - | StartsWith f -> - Expression.Call (Expression.PropertyOrField (param, f.FieldName), StringStartsWithMethod, Expression.Constant (f.Value)) - | EndsWith f -> - Expression.Call (Expression.PropertyOrField (param, f.FieldName), StringEndsWithMethod, Expression.Constant (f.Value)) + | StartsWith f -> Expression.Call (Expression.PropertyOrField (param, f.FieldName), StringStartsWithMethod, Expression.Constant (f.Value)) + | EndsWith f -> Expression.Call (Expression.PropertyOrField (param, f.FieldName), StringEndsWithMethod, Expression.Constant (f.Value)) | Contains f -> let ``member`` = Expression.PropertyOrField (param, f.FieldName) - let isEnumerable (memberType: Type) = - not (Type.(=)(memberType, typeof)) - && typeof.IsAssignableFrom(memberType) - && memberType.GetInterfaces().Any(fun i -> i.FullName.StartsWith "System.Collections.Generic.IEnumerable`1") - match ``member``.Member with + let isEnumerable (memberType : Type) = + not (Type.(=) (memberType, typeof)) + && typeof.IsAssignableFrom (memberType) + && memberType + .GetInterfaces() + .Any (fun i -> i.FullName.StartsWith "System.Collections.Generic.IEnumerable`1") + 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)) + 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)) - | _ -> - Expression.Call (``member``, StringContainsMethod, Expression.Constant (f.Value)) + Expression.Call ( + getEnumerableContainsMethod field.FieldType, + Expression.PropertyOrField (param, f.FieldName), + Expression.Constant (f.Value) + ) + | _ -> Expression.Call (``member``, StringContainsMethod, Expression.Constant (f.Value)) | OfTypes types -> types |> Seq.map (fun t -> buildTypeDiscriminatorCheck param t) @@ -198,15 +222,17 @@ module ObjectListFilter = Expression.PropertyOrField (param, "__typename"), // Default discriminator value Expression.Constant (t.FullName) - ) :> Expression + ) + :> Expression | ValueSome discExpr, ValueNone -> Expression.Invoke ( // Provided discriminator comparison discExpr, param, // Default discriminator value gathered from type - Expression.Constant(t.FullName) - ) :> Expression + Expression.Constant (t.FullName) + ) + :> Expression | ValueNone, ValueSome discValueFn -> let discriminatorValue = discValueFn t Expression.Equal ( @@ -214,7 +240,8 @@ module ObjectListFilter = Expression.PropertyOrField (param, "__typename"), // Provided discriminator value gathered from type Expression.Constant (discriminatorValue) - ) :> Expression + ) + :> Expression | ValueSome discExpr, ValueSome discValueFn -> let discriminatorValue = discValueFn t Expression.Invoke ( @@ -243,5 +270,4 @@ module ObjectListFilterExtensions = type IQueryable<'T> with - member inline query.Apply (filter : ObjectListFilter, [] options : ObjectListFilterLinqOptions<'T, 'D>) = - apply options filter query + member inline query.Apply (filter : ObjectListFilter, [] options : ObjectListFilterLinqOptions<'T, 'D>) = apply options filter query