LINQ support
Tracking properties
One of the unique features, ExecutionInfo
gives to FSharp.Data.GraphQL, is an ability to make more advanced analysis of the requested query ahead of its execution. One of them is an ability to create so called property trees, also known as Trackers.
Tracker is a tree of all dependent members (properties or fields) accessed in given execution info resolver expression, as well as its continuations in all nested execution infos, that will be called further. ExecutionInfo resolve object parameter is considered as a tree root in this case.
Keep in mind that tracker is only able to resolve dependent properties within range of the given resolver expression - it won't be able to determine which members were accessed in subsequent method calls.
WARNING: At the moment of current version (v0.2-beta) this feature is not working with abstract types either.
Example: using property trees with Entity Framework
With this knowledge about how to construct property trees set in place, we can use them for example for things like optimization of SQL queries.
Think about using GraphQL for performing a database query using Entity Framework as part of the field resolution:
1: 2: 3: 4: 5: 6: 7: |
|
While this is very simplistic query, it could expose potential problem - as we don't know what other entities beside People
table will be queried as part of GraphQL query at this point, as subsequent objects may define their own fields with other relationships not visible from this function. This may potentially lead to problem of N+1 SELECTs, where related tables - which have not been mentioned in the original database query - could be a subjects of many subsequent queries.
However, given information lying in ExecutionInfo
- and property Tracker
tree as its extension - we are able to determine which entity relations will be called before actual execution of the query. Given that knowledge, we can construct a simple logic which will apply Include("<Relationship>")
method to warn Entity Framework, which tables to prefetch when an actual LINQ query will be called.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: |
|
With that, we can simply modify the code from the beginning, to include all referenced tables - even thou the GraphQL query itself may still be constructed dynamically:
1: 2: 3: 4: 5: 6: 7: |
|
LINQ generation
Beside being able to use trackers for custom use, FSharp.Data.GraphQL server offers an ability to apply that information in form of LINQ expression on top of an existing IQueryable<>
collection. Such application is able to create subsequent selects if form of A
⇒ A'{ list of field acessed from A }
, where it takes only those fields, which will be used during query execution.
What it also does is free interpretation of field parameters into their LINQ "equivalent". This includes:
skip
,take
,orderBy
andorderByDesc
will be translated into corresponding LINQ equivalent methods.id
will be translated into.Where(x => x.Id == <id>)
. This requires returned type to provide an Id member used as entity identifier.first
/after
andlast
/before
combinations known to the RelayJS users will be translated into.OrderBy(x => x.Id).Where(x => x.Id > <after>).Take(<first>)
and.OrderByDescending(x => x.Id).Wherex(x => x.Id < <before>).Take(<last>)
equivalents. This also requires from queried elements to provide Id member used as entity identifier.
This list can be extended and overriden by custom user implementation.
To better illustrate lets use the example. Consider having following type definition:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: |
|
And query such as:
1: 2: 3: 4: 5: 6: 7: 8: |
|
Once executed, a LINQ interpreter will produce equivalent similar in form to:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: |
|
Full name: linq.schema
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.query
from Microsoft.FSharp.Collections
Full name: Microsoft.FSharp.Collections.Seq.toList
module Set
from Microsoft.FSharp.Collections
--------------------
type Set<'T (requires comparison)> =
interface IComparable
interface IEnumerable
interface IEnumerable<'T>
interface ICollection<'T>
new : elements:seq<'T> -> Set<'T>
member Add : value:'T -> Set<'T>
member Contains : value:'T -> bool
override Equals : obj -> bool
member IsProperSubsetOf : otherSet:Set<'T> -> bool
member IsProperSupersetOf : otherSet:Set<'T> -> bool
...
Full name: Microsoft.FSharp.Collections.Set<_>
--------------------
new : elements:seq<'T> -> Set<'T>
Full name: Microsoft.FSharp.Collections.Set.fold
Full name: Microsoft.FSharp.Core.Operators.defaultArg
module Map
from Microsoft.FSharp.Collections
--------------------
type Map<'Key,'Value (requires comparison)> =
interface IEnumerable
interface IComparable
interface IEnumerable<KeyValuePair<'Key,'Value>>
interface ICollection<KeyValuePair<'Key,'Value>>
interface IDictionary<'Key,'Value>
new : elements:seq<'Key * 'Value> -> Map<'Key,'Value>
member Add : key:'Key * value:'Value -> Map<'Key,'Value>
member ContainsKey : key:'Key -> bool
override Equals : obj -> bool
member Remove : key:'Key -> Map<'Key,'Value>
...
Full name: Microsoft.FSharp.Collections.Map<_,_>
--------------------
new : elements:seq<'Key * 'Value> -> Map<'Key,'Value>
Full name: Microsoft.FSharp.Collections.Map.empty
{Email: string;
PhoneNumber: string;}
Full name: linq.Contact
val string : value:'T -> string
Full name: Microsoft.FSharp.Core.Operators.string
--------------------
type string = System.String
Full name: Microsoft.FSharp.Core.string
{FirstName: string;
LastName: string;
Contacts: Contact list;}
Full name: linq.Person
Full name: Microsoft.FSharp.Collections.list<_>
val Contact : obj
Full name: linq.Contact
--------------------
type Contact =
{Email: string;
PhoneNumber: string;}
Full name: linq.Contact
from Microsoft.FSharp.Core
val Person : obj
Full name: linq.Person
--------------------
type Person =
{FirstName: string;
LastName: string;
Contacts: Contact list;}
Full name: linq.Person