diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/Helpers.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/Helpers.fs index bfe415ae..d281b3f2 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/Helpers.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/Helpers.fs @@ -43,7 +43,9 @@ module ReflectionHelpers = open Microsoft.FSharp.Quotations.Patterns - let getModuleType = function + let getModuleType quotation = + match quotation with | PropertyGet (_, propertyInfo, _) -> propertyInfo.DeclaringType + | FieldGet (_, fieldInfo) -> fieldInfo.DeclaringType | _ -> failwith "Expression is no property." diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs index 221fd80a..12f54d71 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs @@ -1,6 +1,7 @@ namespace FSharp.Data.GraphQL.Server.Middleware open System +open FSharp.Data.GraphQL /// A filter definition for a field value. type FieldFilter<'Val> = { FieldName : string; Value : 'Val } @@ -15,7 +16,7 @@ type ObjectListFilter = | GreaterThanOrEqual of FieldFilter | LessThan of FieldFilter | LessThanOrEqual of FieldFilter - | In of FieldFilter + | In of FieldFilter | StartsWith of FieldFilter | EndsWith of FieldFilter | Contains of FieldFilter @@ -26,6 +27,7 @@ open System.Linq open System.Linq.Expressions open System.Runtime.InteropServices open System.Reflection +open System.Collections.Generic type private CompareDiscriminatorExpression<'T, 'D> = Expression> @@ -138,15 +140,22 @@ module ObjectListFilter = let whereMethod = genericWhereMethod.MakeGenericMethod ([| typeof<'T> |]) Expression.Call (whereMethod, [| query.Expression; Expression.Lambda> (predicate, param) |]) + let private objectType = typeof let private stringType = typeof + let private genericIEnumerableType = typedefof> + let private StringStartsWithMethod = stringType.GetMethod ("StartsWith", [| stringType |]) let private StringEndsWithMethod = stringType.GetMethod ("EndsWith", [| stringType |]) let private StringContainsMethod = stringType.GetMethod ("Contains", [| stringType |]) - let private getCollectionInstanceContainsMethod (memberType : Type) = + let private unwrapOptionMethod = + FSharp.Data.GraphQL.Helpers.moduleType.GetMethod (nameof Helpers.unwrap) + + let private getCollectionInstanceContainsMethod (memberType : Type) = memberType .GetMethods(BindingFlags.Instance ||| BindingFlags.Public) .FirstOrDefault (fun m -> m.Name = "Contains" && m.GetParameters().Length = 1) - |> ValueOption.ofObj + |> ValueOption.ofObj + let private getEnumerableContainsMethod (itemType : Type) = match typeof @@ -155,6 +164,7 @@ module ObjectListFilter = with | null -> raise (MissingMemberException "Static 'Contains' method with 2 parameters not found on 'Enumerable' class") | containsGenericStaticMethod -> containsGenericStaticMethod.MakeGenericMethod ([| itemType |]) + let private getEnumerableCastMethod (itemType : Type) = match typeof @@ -166,6 +176,14 @@ module ObjectListFilter = let getField (param : ParameterExpression) fieldName = Expression.PropertyOrField (param, fieldName) + let hasEqualityOperator (``type`` : Type) = + ``type``.GetMethods (BindingFlags.Public ||| BindingFlags.Static) + |> Seq.exists (fun m -> m.Name = " op_Equality") + + let hasInequalityOperator (``type`` : Type) = + ``type``.GetMethods (BindingFlags.Public ||| BindingFlags.Static) + |> Seq.exists (fun m -> m.Name = "op_Inequality") + [] type SourceExpression private (expression : Expression) = new (parameter : ParameterExpression) = SourceExpression (parameter :> Expression) @@ -175,92 +193,158 @@ module ObjectListFilter = 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 + let equalsMethod = + objectType + |> _.GetMethods(BindingFlags.Instance ||| BindingFlags.Public) + |> Seq.where (fun m -> m.Name = "Equals") + |> Seq.head + + let staticEqualsMethod = + objectType + |> _.GetMethods(BindingFlags.Static ||| BindingFlags.Public) + |> Seq.where (fun m -> m.Name = "Equals") + |> Seq.head + + let rec buildFilterExpr isEnumerableQuery (param : SourceExpression) buildTypeDiscriminatorCheck filter : Expression = + + let build = buildFilterExpr isEnumerableQuery param buildTypeDiscriminatorCheck + + let (|NoCast|Enumerable|NonEnumerableCast|) value = + if obj.ReferenceEquals (value, null) then NoCast + else if isEnumerableQuery then Enumerable + else NonEnumerableCast (value.GetType ()) + + let unsafeConvertTo ``type`` ``member`` = Expression.Convert (Expression.Convert (``member``, objectType), ``type``) + + let normalizeStringMemberExpr (``member`` : MemberExpression) : Expression = + match ``member``.Type with + | t when t = stringType -> ``member`` + | _ when not isEnumerableQuery -> unsafeConvertTo stringType ``member`` + | _ when isEnumerableQuery -> Expression.Convert (Expression.Call (unwrapOptionMethod, ``member``), stringType) + | _ -> Expression.Convert (``member``, stringType) + match filter with + | Not (Equals f) -> + let ``member`` = Expression.PropertyOrField (param, f.FieldName) + let hasEqualityOperator = hasEqualityOperator ``member``.Type + match f.Value with + | NoCast when hasEqualityOperator -> Expression.NotEqual (``member``, Expression.Constant f.Value) + | NoCast + | NonEnumerableCast _ -> + Expression.NotEqual (Expression.Convert (``member``, objectType), Expression.Convert ((Expression.Constant f.Value), objectType)) + | Enumerable -> + let ``const`` = Expression.Constant (Values.normalizeOptional ``member``.Type f.Value) + Expression.Not (Expression.Call (``const``, equalsMethod, ``member``)) | 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)) - | GreaterThanOrEqual f -> Expression.GreaterThanOrEqual (Expression.PropertyOrField (param, f.FieldName), Expression.Constant (f.Value)) - | LessThanOrEqual f -> Expression.LessThanOrEqual (Expression.PropertyOrField (param, f.FieldName), Expression.Constant (f.Value)) + | Equals f -> + let ``member`` = Expression.PropertyOrField (param, f.FieldName) + let hasEqualityOperator = hasEqualityOperator ``member``.Type + match f.Value with + | NoCast when hasEqualityOperator -> Expression.Equal (``member``, Expression.Constant f.Value) + | NoCast + | NonEnumerableCast _ -> + Expression.Equal (Expression.Convert (``member``, objectType), Expression.Convert ((Expression.Constant f.Value), objectType)) + | Enumerable -> + let ``const`` = Expression.Constant (Values.normalizeOptional ``member``.Type f.Value) + Expression.Call (``const``, equalsMethod, ``member``) + | GreaterThan f -> + let ``member`` = Expression.PropertyOrField (param, f.FieldName) + match f.Value with + | NoCast -> Expression.GreaterThan (``member``, Expression.Constant f.Value) + | Enumerable -> Expression.GreaterThan (``member``, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value)) + | NonEnumerableCast ``type`` -> Expression.GreaterThan ((unsafeConvertTo ``type`` ``member``), Expression.Constant f.Value) + | LessThan f -> + let ``member`` = Expression.PropertyOrField (param, f.FieldName) + match f.Value with + | NoCast -> Expression.LessThan (``member``, Expression.Constant f.Value) + | Enumerable -> Expression.LessThan (``member``, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value)) + | NonEnumerableCast ``type`` -> Expression.LessThan ((unsafeConvertTo ``type`` ``member``), Expression.Constant f.Value) + | GreaterThanOrEqual f -> + let ``member`` = Expression.PropertyOrField (param, f.FieldName) + match f.Value with + | NoCast -> Expression.GreaterThanOrEqual (``member``, Expression.Constant f.Value) + | Enumerable -> Expression.GreaterThanOrEqual (``member``, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value)) + | NonEnumerableCast ``type`` -> Expression.GreaterThanOrEqual ((unsafeConvertTo ``type`` ``member``), Expression.Constant f.Value) + | LessThanOrEqual f -> + let ``member`` = Expression.PropertyOrField (param, f.FieldName) + match f.Value with + | NoCast -> Expression.LessThanOrEqual (``member``, Expression.Constant f.Value) + | Enumerable -> Expression.LessThanOrEqual (``member``, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value)) + | NonEnumerableCast ``type`` -> Expression.LessThanOrEqual ((unsafeConvertTo ``type`` ``member``), Expression.Constant f.Value) | StartsWith f -> let ``member`` = Expression.PropertyOrField (param, f.FieldName) - if ``member``.Type = stringType then - Expression.Call (``member``, StringStartsWithMethod, Expression.Constant (f.Value)) - else - Expression.Call (Expression.Convert (``member``, stringType), StringStartsWithMethod, Expression.Constant (f.Value)) + Expression.Call (normalizeStringMemberExpr ``member``, StringStartsWithMethod, Expression.Constant f.Value) | EndsWith f -> let ``member`` = Expression.PropertyOrField (param, f.FieldName) - if ``member``.Type = stringType then - Expression.Call (``member``, StringEndsWithMethod, Expression.Constant (f.Value)) - else - Expression.Call (Expression.Convert (``member``, stringType), StringEndsWithMethod, Expression.Constant (f.Value)) + Expression.Call (normalizeStringMemberExpr ``member``, StringEndsWithMethod, Expression.Constant f.Value) + | Contains f -> let ``member`` = Expression.PropertyOrField (param, f.FieldName) let isEnumerable (memberType : Type) = not (Type.(=) (memberType, stringType)) && typeof.IsAssignableFrom (memberType) - && memberType - .GetInterfaces() - .Any (fun i -> i.FullName.StartsWith "System.Collections.Generic.IEnumerable`1") + && memberType.GetInterfaces().Any (fun i -> i.FullName.StartsWith "System.Collections.Generic.IEnumerable`1") + let normalizedValue = Values.normalizeOptional ``member``.Type f.Value let callContains memberType = let itemType = - if ``member``.Type.IsArray then ``member``.Type.GetElementType() - else ``member``.Type.GetGenericArguments()[0] + if ``member``.Type.IsArray then + ``member``.Type.GetElementType () + else + ``member``.Type.GetGenericArguments()[0] let valueType = - match f.Value with + match normalizedValue with | null -> itemType - | value -> value.GetType() + | value -> value.GetType () let castedMember = - if itemType = valueType then ``member`` :> Expression - else + if itemType = valueType then + ``member`` :> Expression + elif isEnumerableQuery then let castMethod = getEnumerableCastMethod valueType Expression.Call (castMethod, ``member``) + else + let castedEnumerableType = genericIEnumerableType.MakeGenericType ([| valueType |]) + unsafeConvertTo castedEnumerableType ``member`` match getCollectionInstanceContainsMethod memberType with | ValueNone -> let enumerableContains = getEnumerableContainsMethod valueType - Expression.Call (enumerableContains, castedMember, Expression.Constant (f.Value)) - | ValueSome instanceContainsMethod -> - Expression.Call (castedMember, instanceContainsMethod, Expression.Constant (f.Value)) + Expression.Call (enumerableContains, castedMember, Expression.Constant (normalizedValue)) + | ValueSome instanceContainsMethod -> Expression.Call (castedMember, instanceContainsMethod, Expression.Constant (normalizedValue)) match ``member``.Member with | :? PropertyInfo as prop when prop.PropertyType |> isEnumerable -> callContains prop.PropertyType | :? FieldInfo as field when field.FieldType |> isEnumerable -> callContains field.FieldType | _ -> - if ``member``.Type = stringType then - Expression.Call (``member``, StringContainsMethod, Expression.Constant (f.Value)) - else - Expression.Call (Expression.Convert (``member``, stringType), StringContainsMethod, Expression.Constant (f.Value)) + let unwrappedValue = Helpers.unwrap f.Value + Expression.Call (normalizeStringMemberExpr ``member``, StringContainsMethod, Expression.Constant unwrappedValue) | In f when not (f.Value.IsEmpty) -> let ``member`` = Expression.PropertyOrField (param, f.FieldName) - let enumerableContains = getEnumerableContainsMethod typeof - Expression.Call (enumerableContains, Expression.Constant (f.Value), Expression.Convert (``member``, typeof)) - | In f -> Expression.Constant (true) + let enumerableContains = getEnumerableContainsMethod objectType + Expression.Call (enumerableContains, (Expression.Constant f.Value), Expression.Convert (``member``, objectType)) + | In f -> Expression.Constant (false) | OfTypes types -> types |> Seq.map (fun t -> buildTypeDiscriminatorCheck param t) |> Seq.reduce (fun acc expr -> Expression.OrElse (acc, expr)) | FilterField f -> let paramExpr = Expression.PropertyOrField (param, f.FieldName) - buildFilterExpr (SourceExpression paramExpr) buildTypeDiscriminatorCheck f.Value + buildFilterExpr isEnumerableQuery (SourceExpression paramExpr) buildTypeDiscriminatorCheck f.Value - type private CompareDiscriminatorExpressionVisitor<'T, 'D> ( - compareDiscriminator : CompareDiscriminatorExpression<'T, 'D>, - param : SourceExpression, - value : obj - ) = + type private CompareDiscriminatorExpressionVisitor<'T, 'D> + (compareDiscriminator : CompareDiscriminatorExpression<'T, 'D>, param : SourceExpression, value : obj) = inherit ExpressionVisitor () - override _.VisitParameter(node) = + override _.VisitParameter (node) = if node = compareDiscriminator.Parameters.[0] then param.Value elif node = compareDiscriminator.Parameters.[1] then - Expression.Constant(value) :> Expression + Expression.Constant (value) :> Expression else node :> Expression + let enumerableQueryType = typedefof> + let apply (options : ObjectListFilterLinqOptions<'T, 'D>) (filter : ObjectListFilter) (query : IQueryable<'T>) = + let isEnumerableQuery = query.GetType().GetGenericTypeDefinition () = enumerableQueryType // Helper for discriminator comparison let buildTypeDiscriminatorCheck (param : SourceExpression) (t : Type) = match options.CompareDiscriminator, options.GetDiscriminatorValue with @@ -270,7 +354,8 @@ module ObjectListFilter = Expression.PropertyOrField (param, "__typename"), // Default discriminator value Expression.Constant (t.FullName) - ) :> Expression + ) + :> Expression | ValueSome discExpr, ValueNone -> // Replace parameters from the original expression with our new ones let replacer = CompareDiscriminatorExpressionVisitor (discExpr, param, t.FullName) @@ -282,7 +367,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 // Replace parameters from the original expression with our new ones @@ -290,7 +376,7 @@ module ObjectListFilter = replacer.Visit discExpr.Body let queryExpr = let param = Expression.Parameter (typeof<'T>, "x") - let body = buildFilterExpr (SourceExpression param) buildTypeDiscriminatorCheck filter + let body = buildFilterExpr isEnumerableQuery (SourceExpression param) buildTypeDiscriminatorCheck filter whereExpr<'T> query param body // Create and execute the final expression query.Provider.CreateQuery<'T> (queryExpr) diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs b/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs index a668cd60..ad79c67a 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs @@ -116,7 +116,7 @@ let rec private coerceObjectListFilterInput x : Result Seq.map (function - | EquatableValue v -> Ok v + | EquatableValue v -> Ok (box v) | NonEquatableValue v -> Error { new IGQLError with diff --git a/src/FSharp.Data.GraphQL.Server/FSharp.Data.GraphQL.Server.fsproj b/src/FSharp.Data.GraphQL.Server/FSharp.Data.GraphQL.Server.fsproj index e2576040..6da1f3c2 100644 --- a/src/FSharp.Data.GraphQL.Server/FSharp.Data.GraphQL.Server.fsproj +++ b/src/FSharp.Data.GraphQL.Server/FSharp.Data.GraphQL.Server.fsproj @@ -26,6 +26,9 @@ <_Parameter1>FSharp.Data.GraphQL.Server.AspNetCore + + <_Parameter1>FSharp.Data.GraphQL.Server.Middleware + diff --git a/src/FSharp.Data.GraphQL.Server/Values.fs b/src/FSharp.Data.GraphQL.Server/Values.fs index 6eda2fa5..dabc5839 100644 --- a/src/FSharp.Data.GraphQL.Server/Values.fs +++ b/src/FSharp.Data.GraphQL.Server/Values.fs @@ -30,7 +30,7 @@ let private wrapOptionalNone (outputType : Type) (inputType : Type) = else null -let private normalizeOptional (outputType : Type) value = +let normalizeOptional (outputType : Type) value = match value with | null -> wrapOptionalNone outputType typeof | value -> diff --git a/src/FSharp.Data.GraphQL.Shared/Helpers/ObjAndStructConversions.fs b/src/FSharp.Data.GraphQL.Shared/Helpers/ObjAndStructConversions.fs index 948daf71..863c359b 100644 --- a/src/FSharp.Data.GraphQL.Shared/Helpers/ObjAndStructConversions.fs +++ b/src/FSharp.Data.GraphQL.Shared/Helpers/ObjAndStructConversions.fs @@ -38,6 +38,27 @@ module internal Seq = |> Seq.map ValueSome |> _.FirstOrDefault() + let vtryHead (source : 'T seq) = + use enumerator = source.GetEnumerator () + if not (enumerator.MoveNext ()) then + ValueNone + else + match enumerator.Current with + | null -> ValueNone + | head -> ValueSome head + + let vtryLast (source : 'T seq) = + use enumerator = source.GetEnumerator () + if not (enumerator.MoveNext ()) then + ValueNone + else + let mutable last = enumerator.Current + while enumerator.MoveNext () do + last <- enumerator.Current + match last with + | null -> ValueNone + | last -> ValueSome last + module internal List = let vchoose mapping list = list |> Seq.ofList |> Seq.vchoose mapping |> List.ofSeq diff --git a/src/FSharp.Data.GraphQL.Shared/Helpers/Reflection.fs b/src/FSharp.Data.GraphQL.Shared/Helpers/Reflection.fs index f8e8b0c1..d160e1ef 100644 --- a/src/FSharp.Data.GraphQL.Shared/Helpers/Reflection.fs +++ b/src/FSharp.Data.GraphQL.Shared/Helpers/Reflection.fs @@ -10,43 +10,28 @@ open System.Reflection open System.Text.Json.Serialization /// General helper functions and types. -module Helpers = +module internal ReflectionHelper = - /// Casts a System.Object to a System.Object option. - let optionCast (value: obj) = - if isNull value then None - else - let t = value.GetType() - if t.FullName.StartsWith "Microsoft.FSharp.Core.FSharpOption`1" then - let p = t.GetProperty("Value") - Some (p.GetValue(value, [||])) - elif t.FullName.StartsWith "Microsoft.FSharp.Core.FSharpValueOption`1" then - if value = Activator.CreateInstance t then None - else - let p = t.GetProperty("Value") - Some (p.GetValue(value, [||])) - else None + open Microsoft.FSharp.Quotations.Patterns - /// Matches a System.Object with an option. - /// If the object is an Option, returns it as Some, otherwise, return None. - let (|ObjectOption|_|) = optionCast + let getModuleType quotation = + match quotation with + | PropertyGet (_, propertyInfo, _) -> propertyInfo.DeclaringType + | FieldGet (_, fieldInfo) -> fieldInfo.DeclaringType + | _ -> failwith "Expression is no property." - /// Lifts a System.Object to an option, unless it is already an option. - let toOption x = - match x with - | null -> None - | ObjectOption v - | v -> Some v - -module internal ReflectionHelper = + let [] OptionTypeName = "Microsoft.FSharp.Core.FSharpOption`1" + let [] ValueOptionTypeName = "Microsoft.FSharp.Core.FSharpValueOption`1" + let [] SkippableTypeName = "System.Text.Json.Serialization.Skippable`1" + let private listGenericTypeInfo = typedefof<_ list>.GetTypeInfo() /// /// Returns pair of function constructors for `cons(head,tail)` and `nil` /// used to create list of type given at runtime. /// /// Type used for result list constructors as type param let listOfType t = - let listType = typedefof<_ list>.GetTypeInfo().MakeGenericType([|t|]).GetTypeInfo() + let listType = listGenericTypeInfo.MakeGenericType([|t|]).GetTypeInfo() let nil = let empty = listType.GetDeclaredProperty "Empty" empty.GetValue (null) @@ -67,13 +52,14 @@ module internal ReflectionHelper = ) array :> obj + let private optionGenericTypeInfo = typedefof<_ option>.GetTypeInfo() /// /// Returns pair of function constructors for `some(value)` and `none` /// used to create option of type given at runtime. /// /// Type used for result option constructors as type param let optionOfType t = - let optionType = typedefof<_ option>.GetTypeInfo().MakeGenericType([|t|]).GetTypeInfo() + let optionType = optionGenericTypeInfo.MakeGenericType([|t|]).GetTypeInfo() let none = let x = optionType.GetDeclaredProperty "None" x.GetValue(null) @@ -101,13 +87,14 @@ module internal ReflectionHelper = else input (some, none, value) + let private valueOptionGenericTypeInfo = typedefof<_ voption>.GetTypeInfo() /// /// Returns pair of function constructors for `some(value)` and `none` /// used to create option of type given at runtime. /// /// Type used for result option constructors as type param let vOptionOfType t = - let optionType = typedefof<_ voption>.GetTypeInfo().MakeGenericType([|t|]).GetTypeInfo() + let optionType = valueOptionGenericTypeInfo.MakeGenericType([|t|]).GetTypeInfo() let none = let x = optionType.GetDeclaredProperty "None" x.GetValue(null) @@ -157,13 +144,14 @@ module internal ReflectionHelper = else createInclude.Invoke(null, [| value |]) (``include``, skip) + let skippableGenericTypeInfo = typedefof<_ Skippable>.GetTypeInfo() /// /// Returns pair of function constructors for `include(value)` and `skip` /// used to create option of type given at runtime. /// /// Type used for result option constructors as type param let skippableOfType t = - let skippableType = typedefof<_ Skippable>.GetTypeInfo().MakeGenericType([|t|]).GetTypeInfo() + let skippableType = skippableGenericTypeInfo.MakeGenericType([|t|]).GetTypeInfo() let skip = let x = skippableType.GetDeclaredProperty "Skip" x.GetValue(null) @@ -178,3 +166,56 @@ module internal ReflectionHelper = then value else createInclude.Invoke(null, [| value |]) (``include``, skip) + +module Helpers = + + let rec internal moduleType = ReflectionHelper.getModuleType <@ moduleType @> + + /// + /// Casts a to a . + /// + let optionCast (value: obj) = + if isNull value then None + else + let t = value.GetType() + if t.FullName.StartsWith ReflectionHelper.OptionTypeName then + let p = t.GetProperty("Value") + Some (p.GetValue(value, [||])) + elif t.FullName.StartsWith ReflectionHelper.ValueOptionTypeName then + if value = Activator.CreateInstance t then None + else + let p = t.GetProperty("Value") + Some (p.GetValue(value, [||])) + else None + + /// + /// Matches a System.Object with an option. + /// If the object is an . + /// + let (|ObjectOption|_|) = optionCast + + /// + /// Lifts a to an , unless it is already an . + /// + let toOption x = + match x with + | null -> None + | ObjectOption v + | v -> Some v + + /// + /// Unwraps a from an or , + /// unless it is not wrapped. + /// + let unwrap (value : objnull) = + match value with + | null -> null + | value -> + let t = value.GetType() + if t.FullName.StartsWith ReflectionHelper.OptionTypeName then + t.GetProperty("Value").GetValue (value, [||]) + elif t.FullName.StartsWith ReflectionHelper.ValueOptionTypeName then + if value = Activator.CreateInstance t then null + else + t.GetProperty("Value").GetValue (value, [||]) + else value diff --git a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqGenerateTests.fs b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqGenerateTests.fs index 7dc5a1e5..d249e788 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqGenerateTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqGenerateTests.fs @@ -23,9 +23,9 @@ type ValidStringStruct = static member internal op_GreaterThanOrEqual (ValidStringStruct left, right) = left >= right static member internal op_LessThan (ValidStringStruct left, right) = left < right static member internal op_LessThanOrEqual (ValidStringStruct left, right) = left <= right - member str.StartsWith (value : string) = let (ValidStringStruct str) = str in str.StartsWith(value) - member str.EndsWith (value : string) = let (ValidStringStruct str) = str in str.EndsWith(value) - member str.Contains (value : string) = let (ValidStringStruct str) = str in str.Contains(value) + member str.StartsWith (value : string) = let (ValidStringStruct str) = str in str.StartsWith (value) + member str.EndsWith (value : string) = let (ValidStringStruct str) = str in str.EndsWith (value) + member str.Contains (value : string) = let (ValidStringStruct str) = str in str.Contains (value) // Just for demo purposes interface IEqualityOperators with static member op_Equality (ValidStringStruct left, ValidStringStruct right) = left = right @@ -65,6 +65,7 @@ type ValidStringObject = type ValidIntStruct = internal | ValidIntStruct of Int64 + static member internal op_Equality (ValidIntStruct left, ValidIntStruct right) = left = right static member internal op_Inequality (ValidIntStruct left, ValidIntStruct right) = left <> right static member internal op_GreaterThan (ValidIntStruct left, right : Int64) = left > right @@ -72,11 +73,16 @@ type ValidIntStruct = type ValidIntObject = internal | ValidIntObject of Int64 + static member internal op_Equality (ValidIntObject left, ValidIntObject right) = left = right static member internal op_Inequality (ValidIntObject left, ValidIntObject right) = left <> right static member internal op_GreaterThan (ValidIntObject left, right : Int64) = left > right type FakeEntity = { + ValueOptionString : string voption + OptionString : string option + ValueOptionInt : int voption + OptionInt : int option ValidStringStruct : ValidStringStruct ValidStringObject : ValidStringObject ValidStringStructList : ValidStringStruct list @@ -89,19 +95,98 @@ type FakeEntity = { let jsonOptions = Json.getSerializerOptions Seq.empty let cosmosClient = - let options = CosmosClientOptions(UseSystemTextJsonSerializerWithOptions = jsonOptions) + let options = CosmosClientOptions (UseSystemTextJsonSerializerWithOptions = jsonOptions) new CosmosClient ("https://localhost:8081/", "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==", options) -let container = cosmosClient.GetContainer("database", "container") -let filterOptions = - ObjectListFilterLinqOptions.None +let container = cosmosClient.GetContainer ("database", "container") +let filterOptions = ObjectListFilterLinqOptions.None [] let ``ObjectListFilter works with Equals operator for ValidStringStruct`` () = let queryable = container.GetItemLinqQueryable () - let filter = Equals { FieldName = "validStringStruct"; Value = "Jonathan"} + let filter = Equals { FieldName = "validStringStruct"; Value = "Jonathan" } + let filterQuery = queryable.Apply (filter, filterOptions) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText """SELECT VALUE root FROM root WHERE (root["validStringStruct"] = "Jonathan")""" + +[] +let ``ObjectListFilter works with not Equals operator for ValidStringStruct`` () = + let queryable = container.GetItemLinqQueryable () + let filter = Not (Equals { FieldName = "validStringStruct"; Value = "Jonathan" }) + let filterQuery = queryable.Apply (filter, filterOptions) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText """SELECT VALUE root FROM root WHERE (root["validStringStruct"] != "Jonathan")""" + +[] +let ``ObjectListFilter works with Equals operator for ValueOptionString`` () = + let queryable = container.GetItemLinqQueryable () + let filter = Equals { FieldName = "valueOptionString"; Value = "Jonathan" } + let filterQuery = queryable.Apply (filter, filterOptions) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText """SELECT VALUE root FROM root WHERE (root["valueOptionString"] = "Jonathan")""" + +[] +let ``ObjectListFilter works with not Equals operator for ValueOptionString`` () = + let queryable = container.GetItemLinqQueryable () + let filter = Not (Equals { FieldName = "valueOptionString"; Value = "Jonathan" }) + let filterQuery = queryable.Apply (filter, filterOptions) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText """SELECT VALUE root FROM root WHERE (root["valueOptionString"] != "Jonathan")""" + +[] +let ``ObjectListFilter works with Equals operator for null ValueOptionString`` () = + let queryable = container.GetItemLinqQueryable () + let filter = Equals { FieldName = "valueOptionString"; Value = null } + let filterQuery = queryable.Apply (filter, filterOptions) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["valueOptionString"] = null)""" + +[] +let ``ObjectListFilter works with Equals operator for ValueNone ValueOptionString`` () = + let queryable = container.GetItemLinqQueryable () + let filter = Equals { FieldName = "valueOptionString"; Value = (ValueNone : voption) } + let filterQuery = queryable.Apply (filter, filterOptions) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["valueOptionString"] = null)""" + +[] +let ``ObjectListFilter works with not Equals operator for ValueNone ValueOptionString`` () = + let queryable = container.GetItemLinqQueryable () + let filter = Not (Equals { FieldName = "valueOptionString"; Value = (ValueNone : voption) }) + let filterQuery = queryable.Apply (filter, filterOptions) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["valueOptionString"] != null)""" + +[] +let ``ObjectListFilter works with Equals operator for OptionString`` () = + let queryable = container.GetItemLinqQueryable () + let filter = Equals { FieldName = "optionString"; Value = "Jonathan" } + let filterQuery = queryable.Apply (filter, filterOptions) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText """SELECT VALUE root FROM root WHERE (root["optionString"] = "Jonathan")""" + +[] +let ``ObjectListFilter works with not Equals operator for OptionString`` () = + let queryable = container.GetItemLinqQueryable () + let filter = Not (Equals { FieldName = "optionString"; Value = "Jonathan" }) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery - equals queryDefinition.QueryText """SELECT VALUE root FROM root WHERE (root["validStringStruct"] = "Jonathan")""" + equals queryDefinition.QueryText """SELECT VALUE root FROM root WHERE (root["optionString"] != "Jonathan")""" + +[] +let ``ObjectListFilter works with Equals operator for null OptionString`` () = + let queryable = container.GetItemLinqQueryable () + let filter = Equals { FieldName = "optionString"; Value = null } + let filterQuery = queryable.Apply (filter, filterOptions) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["optionString"] = null)""" + +[] +let ``ObjectListFilter works with not Equals operator for null OptionString`` () = + let queryable = container.GetItemLinqQueryable () + let filter = Not (Equals { FieldName = "optionString"; Value = null }) + let filterQuery = queryable.Apply (filter, filterOptions) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["optionString"] = null)""" [] let ``ObjectListFilter works with StartsWith operator for ValidStringStruct`` () = @@ -127,7 +212,7 @@ let ``ObjectListFilter works with Contains operator for ValidStringStruct`` () = let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE CONTAINS(root["validStringStruct"], "athan")""" -[] +[] let ``ObjectListFilter works with Contains operator for ValidStringStruct list`` () = let queryable = container.GetItemLinqQueryable () let filter = Contains { FieldName = "validStringStructList"; Value = "athan" } @@ -143,6 +228,13 @@ let ``ObjectListFilter works with In operator for ValidStringStruct list`` () = let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE ARRAY_CONTAINS([ "athan", "gaja" ], root["validStringStruct"])""" +[] +let ``ObjectListFilter works with In operator for empty ValidStringStruct list`` () = + let queryable = container.GetItemLinqQueryable () + let filter = In { FieldName = "validStringStruct"; Value = [ ] } + let filterQuery = queryable.Apply (filter, filterOptions) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE false""" [] let ``ObjectListFilter works with Equals operator for ValidStringObject`` () = @@ -152,6 +244,13 @@ let ``ObjectListFilter works with Equals operator for ValidStringObject`` () = let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["validStringObject"] = "Jonathan")""" +[] +let ``ObjectListFilter works with not Equals operator for ValidStringObject`` () = + let filter = Not (Equals { FieldName = "validStringObject"; Value = ValidStringObject "Jonathan" }) + let queryable = container.GetItemLinqQueryable () + let filterQuery = queryable.Apply (filter) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["validStringObject"] = "Jonathan")""" [] let ``ObjectListFilter works with GreaterThan operator for ValidIntStruct`` () = @@ -169,3 +268,114 @@ let ``ObjectListFilter works with GreaterThan operator for ValidIntObject`` () = let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["validIntObject"] > 6)""" +[] +let ``ObjectListFilter works with GreaterThan operator for ValueOptionInt`` () = + let filter = GreaterThan { FieldName = "valueOptionInt"; Value = 1 } + let queryable = container.GetItemLinqQueryable () + let filterQuery = queryable.Apply (filter) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["valueOptionInt"] > 1)""" + +[] +let ``ObjectListFilter works with GreaterThan operator for OptionInt`` () = + let filter = GreaterThan { FieldName = "optionInt"; Value = 1 } + let queryable = container.GetItemLinqQueryable () + let filterQuery = queryable.Apply (filter) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["optionInt"] > 1)""" + +[] +let ``ObjectListFilter works with LessThan operator for ValidIntStruct`` () = + let queryable = container.GetItemLinqQueryable () + let filter = LessThan { FieldName = "validIntStruct"; Value = 6L } + let filterQuery = queryable.Apply (filter, filterOptions) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["validIntStruct"] < 6)""" + +[] +let ``ObjectListFilter works with LessThan operator for ValidIntObject`` () = + let filter = LessThan { FieldName = "validIntObject"; Value = 6L } + let queryable = container.GetItemLinqQueryable () + let filterQuery = queryable.Apply (filter) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["validIntObject"] < 6)""" + +[] +let ``ObjectListFilter works with LessThan operator for ValueOptionInt`` () = + let filter = LessThan { FieldName = "valueOptionInt"; Value = 1 } + let queryable = container.GetItemLinqQueryable () + let filterQuery = queryable.Apply (filter) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["valueOptionInt"] < 1)""" + +[] +let ``ObjectListFilter works with LessThan operator for OptionInt`` () = + let filter = LessThan { FieldName = "optionInt"; Value = 1 } + let queryable = container.GetItemLinqQueryable () + let filterQuery = queryable.Apply (filter) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["optionInt"] < 1)""" + +[] +let ``ObjectListFilter works with GreaterThanOrEqual operator for ValidIntStruct`` () = + let queryable = container.GetItemLinqQueryable () + let filter = GreaterThanOrEqual { FieldName = "validIntStruct"; Value = 6L } + let filterQuery = queryable.Apply (filter, filterOptions) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["validIntStruct"] >= 6)""" + +[] +let ``ObjectListFilter works with GreaterThanOrEqual operator for ValidIntObject`` () = + let filter = GreaterThanOrEqual { FieldName = "validIntObject"; Value = 6L } + let queryable = container.GetItemLinqQueryable () + let filterQuery = queryable.Apply (filter) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["validIntObject"] >= 6)""" + +[] +let ``ObjectListFilter works with GreaterThanOrEqual operator for ValueOptionInt`` () = + let filter = GreaterThanOrEqual { FieldName = "valueOptionInt"; Value = 1 } + let queryable = container.GetItemLinqQueryable () + let filterQuery = queryable.Apply (filter) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["valueOptionInt"] >= 1)""" + +[] +let ``ObjectListFilter works with GreaterThanOrEqual operator for OptionInt`` () = + let filter = GreaterThanOrEqual { FieldName = "optionInt"; Value = 1 } + let queryable = container.GetItemLinqQueryable () + let filterQuery = queryable.Apply (filter) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["optionInt"] >= 1)""" + +[] +let ``ObjectListFilter works with LessThanOrEqual operator for ValidIntStruct`` () = + let queryable = container.GetItemLinqQueryable () + let filter = LessThanOrEqual { FieldName = "validIntStruct"; Value = 6L } + let filterQuery = queryable.Apply (filter, filterOptions) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["validIntStruct"] <= 6)""" + +[] +let ``ObjectListFilter works with LessThanOrEqual operator for ValidIntObject`` () = + let filter = LessThanOrEqual { FieldName = "validIntObject"; Value = 6L } + let queryable = container.GetItemLinqQueryable () + let filterQuery = queryable.Apply (filter) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["validIntObject"] <= 6)""" + +[] +let ``ObjectListFilter works with LessThanOrEqual operator for ValueOptionInt`` () = + let filter = LessThanOrEqual { FieldName = "valueOptionInt"; Value = 1 } + let queryable = container.GetItemLinqQueryable () + let filterQuery = queryable.Apply (filter) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["valueOptionInt"] <= 1)""" + +[] +let ``ObjectListFilter works with LessThanOrEqual operator for OptionInt`` () = + let filter = LessThanOrEqual { FieldName = "optionInt"; Value = 1 } + let queryable = container.GetItemLinqQueryable () + let filterQuery = queryable.Apply (filter) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["optionInt"] <= 1)"""