-
Notifications
You must be signed in to change notification settings - Fork 76
Expand file tree
/
Copy pathLinq.fs
More file actions
656 lines (604 loc) · 31.6 KB
/
Linq.fs
File metadata and controls
656 lines (604 loc) · 31.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
// The MIT License (MIT)
// Copyright (c) 2016 Bazinga Technologies Inc
module FSharp.Data.GraphQL.Linq
open System
open System.Collections
open System.Collections.Generic
open System.Linq
open System.Linq.Expressions
open FSharp.Reflection
open FSharp.Data.GraphQL.Extensions
open FSharp.Data.GraphQL.Types
open FSharp.Data.GraphQL.Types.Patterns
open FSharp.Quotations
/// Record defining an argument resolved as part of the property tracker.
[<CustomComparison;CustomEquality>]
type Arg =
{ /// Name of the argument. Matches field arguments from GraphQL type definitions.
Name: string;
/// Value of the argument, provided with user request, query string or as default argument value.
Value: obj }
override x.Equals(y) =
match y with
| :? Arg as a -> x.Name = a.Name
| _ -> false
override x.GetHashCode() = x.Name.GetHashCode()
interface IEquatable<Arg> with
member x.Equals y = x.Name = y.Name
interface IComparable with
member x.CompareTo y =
match y with
| :? Arg as a -> x.Name.CompareTo a.Name
| _ -> failwithf "Cannot compare Arg to %O" (y.GetType())
/// A track is used to represend a single member access statement in F# quotation expressions.
/// It can be represented as (member: from -> to), where `member` in name of field/property getter,
/// `from` determines a type, which member is accessed and `to` a returned type of a member.
[<CustomComparison; CustomEquality>]
type Track =
{ /// Name of the field or property. None is used only by root of the property tracker tree.
Name: string option
/// Type declaring field or property.
ParentType: Type
/// Type of value returned or stored by field or property.
ReturnType: Type }
override x.Equals(y) =
match y with
| :? Track as t -> x.Name = t.Name
| _ -> false
override x.GetHashCode() = x.Name.GetHashCode()
override x.ToString() = sprintf "(%s: %s -> %s)" (defaultArg x.Name "") x.ParentType.Name x.ReturnType.Name
interface IEquatable<Track> with
member x.Equals y = (x.Name = y.Name && x.ParentType.FullName = y.ParentType.FullName && x.ReturnType.FullName = y.ReturnType.FullName)
interface IComparable with
member x.CompareTo y =
match y with
| :? Track as t ->
let c = compare x.Name t.Name
if c = 0
then
let c = compare x.ParentType.FullName t.ParentType.FullName
if c = 0 then compare x.ReturnType.FullName t.ReturnType.FullName else c
else c
| _ -> invalidArg "y" "Track cannot be compared to other type"
/// A representation of the property tree - describes a tree
/// of all properties and subproperties accessed in provided
/// ExecutionInfo with top level argument given as a root.
type Tracker =
/// Leaf of the tree. Marks a direct field/property access with no sub-trees.
/// Consists of <see cref="Track"/> record and (neglible in this case) list of arguments.
| Direct of Track * Arg Set
/// Marks branched field/property access - property value withh possible sub-trees.
/// Consists of <see cref="Track"/> record list of arguments used to parametrize GraphQL
/// field definition and set of subtrees.
| Compose of Track * Arg Set * Tracker Set
member x.Track =
match x with
| Direct(track, _) -> track
| Compose(track, _, _) -> track
/// Adds a child Tracker to it's new parent, returning updated parent as the result.
let rec private addChild child =
function
| Direct(track, args) -> Compose(track, args, Set.singleton child)
| Compose(track, args, children) -> Compose(track, args, Set.add child children)
/// Helper function for creating Track structures.
let private mkTrack name src dst = { Name = Some name; ParentType = src; ReturnType = dst }
/// Takes function with 2 parameters and applies them in reversed order
let inline private flip fn a b = fn b a
let inline private argVal vars argDef argOpt =
match argOpt with
| Some arg ->
Execution.argumentValue vars argDef arg
// TODO: Improve error propagation
|> Result.defaultWith (failwithf "%A")
|> Some
| None -> argDef.DefaultValue
/// Resolves an object representing one of the supported arguments
/// given a variables set and GraphQL input data.
let private resolveLinqArg vars (name, argdef, arg) =
argVal vars argdef arg |> Option.map (fun v -> { Arg.Name = name; Value = v })
let rec private unwrapType =
function
| List inner -> unwrapType inner
| Nullable inner -> unwrapType inner
| other -> other.Type
open System.Reflection
open System.Collections.Immutable
let private bindingFlags = BindingFlags.Instance|||BindingFlags.IgnoreCase|||BindingFlags.Public
let private memberExpr (t: Type) name parameter =
let prop =
t.GetProperty(name, bindingFlags)
match prop with
| null ->
match t.GetField(name, bindingFlags) with
| null -> failwithf "Couldn't find property or field '%s' inside type '%O'" name t
| field -> Expression.Field(parameter, field)
| property -> Expression.Property(parameter, property)
/// Input for custom argument applicators. Contains metadata associated with
/// current property call.
type CallableArg =
{ /// Resolved argument value related to current argument application.
Argument: Arg
/// List of all other arguments resolved as part of other possible
/// applications on the current property track.
AllArguments: Arg Set
/// Track describing field or property access.
Track: Track
/// Source type of the property - in case when track returns a collection or option,
/// this is the type of the nested element.
Type: Type
/// Set of trackers defining nested property subtrees.
Fields: Set<Tracker> }
/// Delegate describing application of the callable argument on the provided LINQ expression.
type ArgApplication = Expression -> CallableArg -> Expression
let private extractStringValueIfOption =
let _, _, (getValue : obj -> obj) = ReflectionHelper.optionOfType typeof<string>
getValue >> string
let private sourceAndMethods track =
match track.ReturnType with
| Gen.Queryable t -> t, Gen.queryableMethods
| Gen.Enumerable t -> t, Gen.enumerableMethods
| other -> failwithf "Type '%O' is neither queryable nor enumerable" other
let private extractValueIfOption callable =
let optionType = typeof<option<_>>.GetGenericTypeDefinition()
let argType = callable.Argument.Value.GetType()
if argType.IsGenericType && argType.GetGenericTypeDefinition() = optionType then
let _, _, (getValue : obj -> obj) = ReflectionHelper.optionOfType argType.GenericTypeArguments[0]
callable.Argument.Value |> getValue
else
callable.Argument.Value
/// Id(value) -> expression.Where(p0 => p0.(idField) == value)
let private applyId: ArgApplication = fun expression callable ->
let tSource, methods = sourceAndMethods callable.Track
let p0 = Expression.Parameter tSource
let idProperty = memberExpr callable.Type "id" p0
// Func<tSource, bool> predicate = p0 => p0 == value
let toStringMethodInfo = idProperty.Type.GetMethod("ToString", Array.empty)
let predicate = Expression.Lambda(Expression.Equal(Expression.Call(idProperty, toStringMethodInfo), Expression.Constant (extractValueIfOption callable)), p0)
let where = methods.Where.MakeGenericMethod [| tSource |]
upcast Expression.Call(null, where, expression, predicate)
/// OrderBy(fieldName) -> expression.OrderBy(p0 => p0.(fieldName))
let private applyOrderBy: ArgApplication = fun expression callable ->
let tSource, methods = sourceAndMethods callable.Track
let p0 = Expression.Parameter tSource
// Func<tSource, tResult> memberAccess = p0 => p0.<value>
let property = memberExpr callable.Type (callable.Argument.Value |> extractStringValueIfOption) p0
let memberAccess = Expression.Lambda(property, [| p0 |])
let orderBy = methods.OrderBy.MakeGenericMethod [| tSource; memberAccess.ReturnType |]
upcast Expression.Call(null, orderBy, expression, memberAccess)
/// OrderByDesc(fieldName) -> expression.OrderByDescending(p0 => p0.(fieldName))
let private applyOrderByDesc: ArgApplication = fun expression callable ->
let tSource, methods = sourceAndMethods callable.Track
let p0 = Expression.Parameter tSource
// Func<tSource, tResult> memberAccess = p0 => p0.<value>
let property = memberExpr callable.Type (callable.Argument.Value |> extractStringValueIfOption) p0
let memberAccess = Expression.Lambda(property, [| p0 |])
let orderBy = methods.OrderByDesc.MakeGenericMethod [| tSource; memberAccess.ReturnType |]
upcast Expression.Call(null, orderBy, expression, memberAccess)
/// Skip(num) -> expression.Skip(num)
let private applySkip: ArgApplication = fun expression callable ->
let tSource, methods = sourceAndMethods callable.Track
let skip = methods.Skip.MakeGenericMethod [| tSource |]
upcast Expression.Call(null, skip, expression, Expression.Constant (extractValueIfOption callable))
/// Take(num) -> expression.Take(num)
let private applyTake: ArgApplication = fun expression callable ->
let tSource, methods = sourceAndMethods callable.Track
let skip = methods.Take.MakeGenericMethod [| tSource |]
upcast Expression.Call(null, skip, expression, Expression.Constant (extractValueIfOption callable))
/// First(num)/After(id) -> expression.Where(p0 => p0.(idField) > id).OrderBy(p0 => p0.(idField)).Take(num)
let private applyFirst: ArgApplication = fun expression callable ->
let tSource, methods = sourceAndMethods callable.Track
let p0 = Expression.Parameter tSource
// 1. Find ID field of the structure (info object is needed)
let idProperty = memberExpr callable.Type "id" p0
// 2. apply q.OrderBy(p0 => p0.<ID_field>) on the expression
let idAccess = Expression.Lambda(idProperty, [| p0 |])
let orderBy = methods.OrderBy.MakeGenericMethod [| tSource; idAccess.ReturnType |]
let ordered = Expression.Call(null, orderBy, expression, idAccess)
let afterOption = callable.AllArguments |> Seq.tryFind (fun a -> a.Name = "after")
let result =
match afterOption with
| Some(after) ->
//TODO: 3a. parse id value using Relay GlobalId and retrieve "actual" id value
// 4a. apply q.Where(p0 => p0.<ID_field> > id) on the ordered expression
let predicate = Expression.Lambda(Expression.GreaterThan(p0, Expression.Constant (after.Value |> extractStringValueIfOption)), p0)
let where = methods.Where.MakeGenericMethod [| tSource |]
Expression.Call(null, where, ordered, predicate)
| None -> ordered
// 5. apply result.Take(value)
let take = methods.Take.MakeGenericMethod [| tSource |]
upcast Expression.Call(null, take, result, Expression.Constant (callable.Argument.Value |> extractStringValueIfOption))
/// Last(num)/Before(id) -> expression.Where(p0 => p0.(idField) < id).OrderByDescending(p0 => p0.(idField)).Take(num)
let private applyLast: ArgApplication = fun expression callable ->
let tSource, methods = sourceAndMethods callable.Track
let p0 = Expression.Parameter tSource
// 1. Find ID field of the structure (info object is needed)
let idProperty = memberExpr callable.Type "id" p0
// 2. apply q.OrderBy(p0 => p0.<ID_field>) on the expression
let idAccess = Expression.Lambda(idProperty, [| p0 |])
let orderByDesc = methods.OrderByDesc.MakeGenericMethod [| tSource; idAccess.ReturnType |]
let ordered = Expression.Call(null, orderByDesc, expression, idAccess)
let beforeOption = callable.AllArguments |> Seq.tryFind (fun a -> a.Name = "after")
let result =
match beforeOption with
| Some(before) ->
//TODO: 3a. parse id value using Relay GlobalId and retrieve "actual" id value
// 4a. apply q.Where(p0 => p0.<ID_field> > id) on the ordered expression
let predicate = Expression.Lambda(Expression.LessThan(p0, Expression.Constant (before.Value |> extractStringValueIfOption)), p0)
let where = methods.Where.MakeGenericMethod [| tSource |]
Expression.Call(null, where, ordered, predicate)
| None -> ordered
// 5. apply result.Take(value)
let take = methods.Take.MakeGenericMethod [| tSource |]
upcast Expression.Call(null, take, result, Expression.Constant (callable.Argument.Value |> extractStringValueIfOption))
/// Tries to resolve all supported LINQ args with values
/// from a given ExecutionInfo and variables collection
let private linqArgs vars info =
let argDefs = info.Definition.Args
if Array.isEmpty argDefs then Set.empty
else
let args = info.Ast.Arguments
argDefs
|> Seq.map (fun a -> (a.Name, a, args |> List.tryFind (fun x -> x.Name = a.Name)))
|> Seq.choose (resolveLinqArg vars)
|> Set.ofSeq
let rec private track set e =
match e with
| Patterns.PropertyGet(Some subject, propertyInfo, _) -> Set.add (mkTrack propertyInfo.Name propertyInfo.DeclaringType propertyInfo.PropertyType) (track set subject)
| Patterns.Lambda(_, body) -> track set body
| Patterns.FieldGet(Some subject, fieldInfo) -> Set.add (mkTrack fieldInfo.Name fieldInfo.DeclaringType fieldInfo.FieldType) (track set subject)
| Patterns.Var(_) -> set
| Patterns.Application(var, body) -> (flip track var >> flip track body) set
| Patterns.Call(subject, _, args) -> args |> List.fold track (match subject with Some x -> track set x | None -> set)
| Patterns.Coerce(expr, _) -> track set expr
| Patterns.ForIntegerRangeLoop(_, lower, upper, iter) -> (flip track lower >> flip track upper >> flip track iter) set
| Patterns.IfThenElse(condition, ifTrue, ifFalse) -> (flip track condition >> flip track ifTrue >> flip track ifFalse) set
| Patterns.Let(_, expr, body) -> (flip track expr >> flip track body) set
| Patterns.LetRecursive(bindings, body) -> bindings |> List.fold (fun acc (_, e) -> track acc e) (track set body)
| Patterns.NewArray(_, exprs) -> exprs |> List.fold track set
| Patterns.NewDelegate(_, _, body) -> track set body
| Patterns.NewObject(_, args) -> args |> List.fold track set
| Patterns.NewRecord(_, args) -> args |> List.fold track set
| Patterns.NewTuple(args) -> args |> List.fold track set
| Patterns.NewUnionCase(_, args) -> args |> List.fold track set
| Patterns.QuoteRaw(expr) -> track set expr
| Patterns.QuoteTyped(expr) -> track set expr
| Patterns.Sequential(prev, next) -> (flip track prev >> flip track next) set
| Patterns.TryFinally(tryBlock, finalBlock) -> (flip track tryBlock >> flip track finalBlock) set
| Patterns.TryWith(tryBlock, _, filter, _, handler) -> (flip track tryBlock >> flip track filter >> flip track handler) set
| Patterns.TupleGet(expr, _) -> track set expr
| Patterns.TypeTest(expr, _) -> track set expr
| Patterns.UnionCaseTest(expr, _) -> track set expr
| Patterns.VarSet(_, expr) -> track set expr
| Patterns.WhileLoop(condition, body) -> (flip track condition >> flip track body) set
| Patterns.WithValue(_, _, expr) -> track set expr
//TODO: move all unnecessary calls into else `_` case
| Patterns.Value _ -> set
| Patterns.ValueWithName _ -> set
| Patterns.AddressOf _ -> set
| Patterns.AddressSet _ -> set
| Patterns.DefaultValue _ -> set
| Patterns.FieldSet _ -> set
| Patterns.PropertySet _ -> set
| _ -> set
/// Intermediate representation containing info about all resolved
/// member acesses. Contains execution info, which resolver function
/// was used, set of tracks found inside resolver function
/// and list of all children (empty for ResolveValue).
type private IR = IR of ExecutionInfo * Set<Tracker> * IR list
/// Checks if `tFrom` somewhat matches `tRoot`, either by direct type comparison
/// or as a type argument in enumerable or option of root.
let private canJoin (tRoot: Type) (tFrom: Type) =
if tFrom = tRoot then true
elif tRoot.IsGenericType && (typeof<IEnumerable>.IsAssignableFrom tRoot || typedefof<Option<_>>.IsAssignableFrom tRoot)
then tFrom = (tRoot.GetGenericArguments().[0])
else false
let private fieldOrProperty = MemberTypes.Field ||| MemberTypes.Property
/// Check if there exists a field or property described by the current track.
let private isOwn (track: Track) =
match track.Name with
| None -> false
| Some name ->
match track.ParentType.GetMember(name, fieldOrProperty, bindingFlags) with
| [||] -> false
| array ->
array |> Array.exists(fun m ->
match m with
| :? PropertyInfo as p -> p.PropertyType = track.ReturnType
| :? FieldInfo as f -> f.FieldType = track.ReturnType
| _ -> false)
let private coreType (t: Type): Type =
match t with
| Gen.Enumerable tCore -> tCore
| Gen.Option tCore -> tCore
| _ -> t
let rec private merge (parent: Tracker) (members: Set<Tracker>) =
if Set.isEmpty members
then Set.singleton parent
else match parent with
| Direct(track, args) ->
if Option.isSome track.Name
then Compose(track, args, members) |> Set.singleton
else match track.ReturnType with
| Gen.Enumerable _ -> Compose(track, args, members) |> Set.singleton
| _ -> members
| Compose(track, args, children) ->
if Option.isSome track.Name
then Compose(track, args, children + members) |> Set.singleton
else match track.ReturnType with
| Gen.Enumerable _ -> Compose(track, args, children + members) |> Set.singleton
| _ -> members
/// Composes trackers into tree within range of a single ExecutionInfo
/// `allTracks` is expected to be the list of Direct's (unrelated tracks)
/// which are going to be composed.
let rec private infoComposer (root: Tracker) (allTracks: Set<Tracker>) : Set<Tracker> =
let rootTrack = root.Track
let parentType = rootTrack.ReturnType
let members =
allTracks
|> Set.filter (fun track ->
match track with
| Direct (track, _) -> canJoin parentType track.ParentType && isOwn track
| x -> failwith <| sprintf "Expected Direct Track, but got %A" x)
if Set.isEmpty members
then
// check for artificial property
// eg. given type Parent = { fname: string; lname: string }
// and field definition "fullName": p -> p.fname + " " + p.lname
// we don't want to return fullName Tracker (as such field doesn't exists)
// but fname and lname instead
let grandpaType = rootTrack.ParentType
let members =
allTracks
|> Set.filter (fun track ->
match track with
| Direct (track, _) -> canJoin grandpaType track.ParentType && isOwn track
| x -> failwithf "Expected Direct Track, but got %A" x)
if Set.isEmpty members
then root |> Set.singleton
else
let remaining = Set.difference allTracks members
members
|> Set.map(fun tracker -> infoComposer tracker remaining)
|> Seq.collect id
|> Set.ofSeq
else
// compose remaining elements recursivelly under members
let remaining = Set.difference allTracks members
let results =
members
|> Set.map(fun tracker -> infoComposer tracker remaining)
|> Seq.collect id
|> Set.ofSeq
let newRoot = merge root results
newRoot
/// Composes tracks collected within a single ExecutionInfo
/// (but not across the ExecutionInfo boundaries)
let rec private compose vars ir =
let (IR(info, directs, children)) = ir
let rootTrack = { Name = None; ParentType = info.ParentDef.Type; ReturnType = info.ReturnDef.Type }
let root = Direct(rootTrack, linqArgs vars info)
let composed = infoComposer root directs
IR(info, composed, children |> List.map (compose vars))
/// Get unrelated tracks from current info and its children (if any)
/// Returned set of trackers ALWAYS consists of Direct trackers only
let rec private getTracks alreadyFound info =
let expr =
match info.Definition.Resolve.Expr with
| (Patterns.WithValue(_,_, (Patterns.Lambda(_, Patterns.Lambda(_, expr))))) -> expr
| _ -> failwith <| sprintf "Unexpected Resolve Definition Expression!"
let tracks =
track Set.empty expr
|> Set.map(fun track -> Direct(track, Set.empty))
|> flip Set.difference alreadyFound
match info.Kind with
| ResolveDeferred inner -> getTracks alreadyFound inner
| ResolveStreamed (inner,_) -> getTracks alreadyFound inner
| ResolveLive inner -> getTracks alreadyFound inner
| ResolveValue -> IR(info, tracks, [])
| SelectFields fieldInfos -> IR(info, tracks, fieldInfos |> List.map (getTracks (alreadyFound + tracks)))
| ResolveCollection inner -> IR(info, tracks, [ getTracks (alreadyFound + tracks) inner ])
| ResolveAbstraction typeMap ->
let found = alreadyFound + tracks
let children =
typeMap
|> Map.toSeq
|> Seq.map (snd >> (List.map (getTracks found)))
|> Seq.collect id
|> Seq.toList
IR(info, found, children)
let rec private assignableChildren (parentType: Type) (childTracks: Set<Tracker>) =
let assignable, unassignable =
childTracks
|> Set.partition (fun child -> parentType.IsAssignableFrom (coreType child.Track.ParentType))
unassignable
|> Set.collect (function Direct _ -> Set.empty | Compose(_, _, grandChildren) -> assignableChildren parentType grandChildren)
|> ((+) assignable)
let rec private join (parentTracks: Set<Tracker>) childIRs =
match childIRs with
| [] -> parentTracks
| _ ->
let childrenCombined =
childIRs
|> List.fold (fun acc childIR ->
let (IR(_, childTracks, grandchildIRs)) = childIR
let joined = join childTracks grandchildIRs
acc + joined ) Set.empty
parentTracks
|> Set.collect (fun parentTrack ->
let parentType = coreType parentTrack.Track.ReturnType
let assignable = assignableChildren parentType childrenCombined
merge parentTrack assignable)
/// Adds `childTrack` as a node to current `parentTrack`, using `parentInfo`
/// and `childInfo` to determine to point of connection.
and private branch parentTrack (parentInfo: ExecutionInfo) childTrack (childInfo: ExecutionInfo) =
let parentType = unwrapType parentInfo.ReturnDef
if parentType = childInfo.ParentDef.Type
then addChild childTrack parentTrack
else
match parentTrack with
| Compose(track, args, fields) -> Compose(track, args, fields |> Set.map (fun child -> branch child parentInfo childTrack childInfo))
| Direct(_) -> parentTrack
let private (|Object|Record|NotSupported|) (t: Type) =
if FSharpType.IsRecord t then Record
elif FSharpType.IsTuple t then NotSupported
elif FSharpType.IsUnion t then NotSupported
else Object
/// Checks which collection type needs to be represented
/// and perfroms a necessary cast.
let private castTo tCollection callExpr : Expression =
match tCollection with
| Gen.List tRes ->
let cast = Gen.listOfSeq.MakeGenericMethod [| tRes |]
upcast Expression.Call(null, cast, [ callExpr ])
| Gen.Array tRes ->
let cast = Gen.arrayOfSeq.MakeGenericMethod [| tRes |]
upcast Expression.Call(null, cast, [ callExpr ])
| Gen.Set tRes ->
let cast = Gen.setOfSeq.MakeGenericMethod [| tRes |]
upcast Expression.Call(null, cast, [ callExpr ])
| _ -> callExpr
let rec private construct (argApplicators: Map<string, ArgApplication>) tracker (inParam: Expression) : Expression =
match tracker with
| Direct _ -> inParam// upcast Expression.PropertyOrField(inParam, track.Name)
| Compose(track, _, fields) ->
match track.ReturnType with
| Gen.Enumerable _ -> constructCollection argApplicators tracker inParam |> castTo track.ReturnType
| _ -> constructObject argApplicators track.ReturnType fields inParam
and private constructObject argApplicators tObj fields (inParam: Expression) : Expression =
let trackerMap = Dictionary<_,_>()
fields
|> Set.filter (fun t -> t.Track.Name |> Option.isSome)
|> Set.iter (fun t -> trackerMap.Add(t.Track.Name.Value.ToLowerInvariant(), t))
let fieldNames = trackerMap.Keys.ToArray()
let ctor =
match tObj with
| Record -> FSharpValue.PreComputeRecordConstructorInfo tObj
| Object ->
ReflectionHelper.matchConstructor tObj fieldNames
| NotSupported ->
raise <| NotSupportedException (sprintf "LINQ conversion for type %O is not supported. Only POCOs and records are allowed." tObj)
let ctorArgs =
ctor.GetParameters()
|> Array.map (fun parameter ->
let paramName = parameter.Name.ToLower ()
match trackerMap.TryGetValue paramName with
| true, childTracker ->
trackerMap.Remove paramName |> ignore
let fieldOrProperty = memberExpr tObj paramName inParam
construct argApplicators childTracker fieldOrProperty
| false, _ -> upcast Expression.Default parameter.ParameterType)
if trackerMap.Count = 0
then upcast Expression.New(ctor, ctorArgs)
else
let members =
tObj.GetProperties(BindingFlags.SetProperty|||BindingFlags.Instance|||BindingFlags.IgnoreCase|||BindingFlags.Public)
|> Array.map (fun p -> p:> MemberInfo)
|> Array.append (tObj.GetFields(BindingFlags.Instance|||BindingFlags.IgnoreCase|||BindingFlags.Public) |> Array.map (fun p -> p:> MemberInfo))
|> Array.map (fun m -> (m.Name.ToLower(), m))
|> Map.ofArray
let memberBindings =
trackerMap
|> Seq.map (fun kv ->
let m = Map.find kv.Key members
let fieldOrProperty = memberExpr tObj kv.Key inParam
Expression.Bind(m, construct argApplicators kv.Value fieldOrProperty) :> MemberBinding)
|> Seq.toArray
upcast Expression.MemberInit(Expression.New(ctor, ctorArgs), memberBindings)
and private constructCollection argApplicators tracker (inParam: Expression) : Expression =
let track, args, fields =
match tracker with
| Compose (track, args, fields) -> track, args, fields
| x -> failwith <| sprintf "Expected Compose Track, but got %A" x
let tSource, methods = sourceAndMethods track
let p0 = Expression.Parameter(tSource)
let body = constructObject argApplicators tSource fields p0
// call method - ((IQueryable<tSource>)inputExpr).Select(p0 => body)
let call: Expression =
upcast Expression.Call(
// Select<tSource, tResult> - method to invoke
methods.Select.MakeGenericMethod [| tSource; tSource |],
// `this` param - Convert(inputValue, IQueryable<tSource>)
Expression.Convert(inParam, methods.Type.MakeGenericType [| tSource |]),
// `mapFunc` param - (p0 => body )
Expression.Lambda(body, p0))
let final = args |> Set.fold (fun acc (arg: Arg) ->
match Map.tryFind (arg.Name.ToLowerInvariant()) argApplicators with
| Some apply -> apply acc { AllArguments = args; Argument = arg; Track = track; Fields = fields; Type = tSource }
| None -> acc) call
final
let private defaultArgApplicators: Map<string, ArgApplication> =
Map.ofList [
"id", applyId
"orderby", applyOrderBy
"orderbydesc", applyOrderByDesc
"skip", applySkip
"take", applyTake
"first", applyFirst
"last", applyLast ]
/// <summary>
/// <para>
/// Creates an intermediate representation of ExecutionInfo by
/// traversing all F# quotations provided in field definition resolvers
/// in order to catch all member accesses (fields / property getters) and
/// tries to construct a dependency tree from them with root starting from
/// query `root` object parameter.
/// </para><para>
/// This is a fast and naive way to resolve all properties that are possibily
/// accessed in resolver functions to include them inside LINQ query
/// constructed later - this way we can potentially omit multiple database
/// calls as a result of underfetched data.
/// </para><para>
/// NOTE: This technique doesn't track exact properties accessed from the `root`
/// and can can cause eager overfetching.
/// </para>
/// </summary>
let rec tracker (vars: ImmutableDictionary<string, obj>) (info: ExecutionInfo) : Tracker =
let ir = getTracks Set.empty info
let composed = compose vars ir
let (IR(_, trackers, children)) = composed
join trackers children |> Seq.head
let private toLinq info (query: IQueryable<'Source>) variables (argApplicators: Map<string, ArgApplication>) : IQueryable<'Source> =
let parameter = Expression.Parameter (query.GetType())
let ir = tracker variables info
let expr = construct argApplicators ir parameter
let compiled =
match expr with
| :? MethodCallExpression as call ->
let lambda = Expression.Lambda(call, [| parameter |])
let compiled = lambda.Compile()
compiled
| selector ->
let tSource = typeof<'Source>
let mSelect = Gen.queryableMethods.Select.MakeGenericMethod [| tSource; tSource |]
let destinationType = Gen.queryableMethods.Type.MakeGenericType [| tSource |]
let call = Expression.Call(mSelect, Expression.Convert(parameter, destinationType), selector)
Expression.Lambda(call, [| parameter |]).Compile()
let result = compiled.DynamicInvoke [| box query |]
match result with
| :? IQueryable<'Source> as q -> q
| :? IEnumerable<'Source> as e -> e.AsQueryable()
| _ -> failwithf "Unrecognized type '%O' is neither IEnumerable<%O> nor IQueryable<%O>" (result.GetType()) typeof<'Source> typeof<'Source>
type System.Linq.IQueryable<'Source> with
/// <summary>
/// <para>
/// Replaces top level type of the execution info with provided queryable source
/// and constructs a LINQ expression from it, returing queryable with all applied
/// operations as the result.
/// </para>
/// <para>
/// By default, GraphQL may define queries that will be interpreted in terms of LINQ operations,
/// such as `orderBy`, `orderByDesc`, `skip`, `take, but also more advanced like:
/// <para>- `id` returning where comparison with object's id field.</para>
/// <para>- `first`/`after` returning slice of the collection, ordered by id with id greater than provided in after param.</para>
/// <para>- `last`/`before` returning slice of the collection, ordered by id descending with id less than provided in after param.</para>
/// </para>
/// </summary>
/// <param name="info">Execution info data to be applied on the queryable.</param>
/// <param name="variables">Optional map with client-provided arguments used to resolve argument values.</param>
/// <param name="applicators">Map of applicators used to define LINQ expression mutations based on GraphQL arguments.</param>
member this.Apply(info: ExecutionInfo, ?variables: ImmutableDictionary<string, obj>, ?applicators: Map<string, ArgApplication>) : IQueryable<'Source> =
let appl =
match applicators with
| None -> defaultArgApplicators
| Some a -> a |> Map.fold (fun acc key value -> Map.add (key.ToLowerInvariant()) value acc) defaultArgApplicators
toLinq info this (defaultArg variables ImmutableDictionary.Empty) appl