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 all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/FSharp.Data.GraphQL.Server.AspNetCore/Helpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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."

182 changes: 134 additions & 48 deletions src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ let rec private coerceObjectListFilterInput x : Result<ObjectListFilter voption,
let! parsedValues =
values
|> Seq.map (function
| EquatableValue v -> Ok v
| EquatableValue v -> Ok (box v)
| NonEquatableValue v ->
Error
{ new IGQLError with
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
101 changes: 71 additions & 30 deletions src/FSharp.Data.GraphQL.Shared/Helpers/Reflection.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 [<Literal>] OptionTypeName = "Microsoft.FSharp.Core.FSharpOption`1"
let [<Literal>] ValueOptionTypeName = "Microsoft.FSharp.Core.FSharpValueOption`1"
let [<Literal>] SkippableTypeName = "System.Text.Json.Serialization.Skippable`1"

let private listGenericTypeInfo = typedefof<_ list>.GetTypeInfo()
/// <summary>
/// Returns pair of function constructors for `cons(head,tail)` and `nil`
/// used to create list of type <paramref name="t"/> given at runtime.
/// </summary>
/// <param name="t">Type used for result list constructors as type param</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)
Expand All @@ -67,13 +52,14 @@ module internal ReflectionHelper =
)
array :> obj

let private optionGenericTypeInfo = typedefof<_ option>.GetTypeInfo()
/// <summary>
/// Returns pair of function constructors for `some(value)` and `none`
/// used to create option of type <paramref name="t"/> given at runtime.
/// </summary>
/// <param name="t">Type used for result option constructors as type param</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)
Expand Down Expand Up @@ -101,13 +87,14 @@ module internal ReflectionHelper =
else input
(some, none, value)

let private valueOptionGenericTypeInfo = typedefof<_ voption>.GetTypeInfo()
/// <summary>
/// Returns pair of function constructors for `some(value)` and `none`
/// used to create option of type <paramref name="t"/> given at runtime.
/// </summary>
/// <param name="t">Type used for result option constructors as type param</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)
Expand Down Expand Up @@ -157,13 +144,14 @@ module internal ReflectionHelper =
else createInclude.Invoke(null, [| value |])
(``include``, skip)

let skippableGenericTypeInfo = typedefof<_ Skippable>.GetTypeInfo()
/// <summary>
/// Returns pair of function constructors for `include(value)` and `skip`
/// used to create option of type <paramref name="t"/> given at runtime.
/// </summary>
/// <param name="t">Type used for result option constructors as type param</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)
Expand All @@ -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 @>

/// <summary>
/// Casts a <see cref="System.Object"/> to a <see cref="option{System.Object}"/>.
/// </summary>
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")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why isn't there a test for if the value is a None like in the ValueOption case (ValueNone)? I mean:

if value = Activator.CreateInstance t then None

?
Is it intentional?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a check for ValueOption.None. As it is a value type it's instance is empty struct

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

/// <summary>
/// Matches a System.Object with an option.
/// If the object is an <see cref="Option{T}", returns it as Some, otherwise, return <see cref="None"/>.
/// </summary>
let (|ObjectOption|_|) = optionCast

/// <summary>
/// Lifts a <see cref="System.Object"/> to an <see cref="option{System.Object}"/>, unless it is already an <see cref="option{System.Object}"/>.
/// </summary>
let toOption x =
match x with
| null -> None
| ObjectOption v
| v -> Some v

/// <summary>
/// Unwraps a <see cref="System.Object"/> from an <see cref="option{System.Object}"/> or <see cref="voption{System.Object}"/>,
/// unless it is not wrapped.
/// </summary>
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
Loading