Skip to content

Implemented support for optional properties usage in non-Enumerable LINQ queries #529

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 8, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 89 additions & 19 deletions src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs
Original file line number Diff line number Diff line change
@@ -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 }
Expand All @@ -26,6 +27,8 @@ open System.Linq
open System.Linq.Expressions
open System.Runtime.InteropServices
open System.Reflection
open System.Collections
open System.Collections.Generic

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

Expand Down Expand Up @@ -139,6 +142,7 @@ module ObjectListFilter =
Expression.Call (whereMethod, [| query.Expression; Expression.Lambda<Func<'T, bool>> (predicate, param) |])

let private stringType = typeof<string>
let private genericIEnumerableType = typedefof<IEnumerable<_>>
let private StringStartsWithMethod = stringType.GetMethod ("StartsWith", [| stringType |])
let private StringEndsWithMethod = stringType.GetMethod ("EndsWith", [| stringType |])
let private StringContainsMethod = stringType.GetMethod ("Contains", [| stringType |])
Expand Down Expand Up @@ -175,29 +179,89 @@ 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 iequtableType = typedefof<IEquatable<_>>
let equalsMethod =
typeof<obj>
|> _.GetMethods(BindingFlags.Instance ||| 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``, typeof<obj>), ``type``)

match filter with
| Not (Equals f) ->
let ``member`` = Expression.PropertyOrField (param, f.FieldName)
let ``const`` = Expression.Constant (Values.normalizeOptional ``member``.Type f.Value)
if isEnumerableQuery && f.Value <> null then
Expression.Not (Expression.Call (``const``, equalsMethod, ``member``))
else
Expression.NotEqual (``member``, ``const``)
| 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)
match f.Value with
| NoCast ->
Expression.Equal (``member``, Expression.Constant f.Value)
| Enumerable ->
Expression.Equal (``member``, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value))
| NonEnumerableCast ``type`` ->
Expression.Equal ((unsafeConvertTo ``type`` ``member``), Expression.Constant f.Value)
| GreaterThan f ->
let ``member`` = Expression.PropertyOrField (param, f.FieldName)
let ``const`` = Expression.Constant (Values.normalizeOptional ``member``.Type f.Value)
if isEnumerableQuery then
Expression.GreaterThan (``member``, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value))
else
Expression.GreaterThan (Expression.Convert (Expression.Convert(``member``, typeof<obj>), f.Value.GetType ()), 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))
Expression.Call (``member``, StringStartsWithMethod, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value))
else
Expression.Call (Expression.Convert (``member``, stringType), StringStartsWithMethod, Expression.Constant (f.Value))
Expression.Call (Expression.Convert (``member``, stringType), StringStartsWithMethod, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value))
| EndsWith f ->
let ``member`` = Expression.PropertyOrField (param, f.FieldName)
if ``member``.Type = stringType then
Expression.Call (``member``, StringEndsWithMethod, Expression.Constant (f.Value))
Expression.Call (``member``, StringEndsWithMethod, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value))
else
Expression.Call (Expression.Convert (``member``, stringType), StringEndsWithMethod, Expression.Constant (f.Value))
Expression.Call (Expression.Convert (``member``, stringType), StringEndsWithMethod, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value))
| Contains f ->
let ``member`` = Expression.PropertyOrField (param, f.FieldName)
let isEnumerable (memberType : Type) =
Expand All @@ -216,35 +280,38 @@ module ObjectListFilter =
| value -> value.GetType()
let castedMember =
if itemType = valueType then ``member`` :> Expression
else
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))
Expression.Call (enumerableContains, castedMember, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value))
| ValueSome instanceContainsMethod ->
Expression.Call (castedMember, instanceContainsMethod, Expression.Constant (f.Value))
Expression.Call (castedMember, instanceContainsMethod, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value))
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))
Expression.Call (``member``, StringContainsMethod, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value))
else
Expression.Call (Expression.Convert (``member``, stringType), StringContainsMethod, Expression.Constant (f.Value))
Expression.Call (Expression.Convert (``member``, stringType), StringContainsMethod, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value))
| In f when not (f.Value.IsEmpty) ->
let ``member`` = Expression.PropertyOrField (param, f.FieldName)
let enumerableContains = getEnumerableContainsMethod typeof<IComparable>
Expression.Call (enumerableContains, Expression.Constant (f.Value), Expression.Convert (``member``, typeof<IComparable>))
Expression.Call (enumerableContains, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value), Expression.Convert (``member``, typeof<IComparable>))
| In f -> Expression.Constant (true)
| 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>,
Expand All @@ -260,7 +327,10 @@ module ObjectListFilter =
else
node :> Expression

let enumerableQueryType = typedefof<EnumerableQuery<_>>

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
Expand Down Expand Up @@ -290,7 +360,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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>FSharp.Data.GraphQL.Server.AspNetCore</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>FSharp.Data.GraphQL.Server.Middleware</_Parameter1>
</AssemblyAttribute>
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/FSharp.Data.GraphQL.Server/Values.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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<obj>
| value ->
Expand Down
21 changes: 21 additions & 0 deletions src/FSharp.Data.GraphQL.Shared/Helpers/ObjAndStructConversions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading