├── .DS_Store ├── .gitignore ├── .travis.yml ├── .vscode ├── launch.json └── tasks.json ├── DataModel └── StarWarsContext.cs ├── NodeCollection.cs ├── Program.cs ├── README.md ├── ResolveGraphQL.csproj └── Schema ├── .DS_Store ├── StarWarsQuery.cs ├── StarWarsSchema.cs └── Types ├── CharacterInterface.cs ├── DroidType.cs └── HumanType.cs /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanli83/ResolveGraphQL/3d4b200d96e3ecb1276fbc69aae0900533a3ab62/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/* 2 | obj/* 3 | Migrations/* 4 | project.lock.json 5 | starwars.db 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | language: csharp 4 | mono: none 5 | before_install: 6 | - curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg 7 | - sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-trusty-prod trusty main" > /etc/apt/sources.list.d/dotnetdev.list' 8 | - sudo apt-get update 9 | - sudo apt-get install dotnet-sdk-2.1.3 -y --allow-unauthenticated 10 | script: 11 | - dotnet restore 12 | - dotnet build 13 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | "program": "${workspaceFolder}/bin/Debug/netcoreapp2.0/ResolveGraphQL.dll", 13 | "args": [], 14 | "cwd": "${workspaceFolder}", 15 | "stopAtEntry": false, 16 | "internalConsoleOptions": "openOnSessionStart", 17 | "launchBrowser": { 18 | "enabled": true, 19 | "args": "${auto-detect-url}", 20 | "windows": { 21 | "command": "cmd.exe", 22 | "args": "/C start ${auto-detect-url}" 23 | }, 24 | "osx": { 25 | "command": "open" 26 | }, 27 | "linux": { 28 | "command": "xdg-open" 29 | } 30 | }, 31 | "env": { 32 | "ASPNETCORE_ENVIRONMENT": "Development" 33 | }, 34 | "sourceFileMap": { 35 | "/Views": "${workspaceFolder}/Views" 36 | } 37 | }, 38 | { 39 | "name": ".NET Core Attach", 40 | "type": "coreclr", 41 | "request": "attach", 42 | "processId": "${command:pickProcess}" 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "shell", 8 | "group": { 9 | "isDefault": true, 10 | "kind": "build" 11 | }, 12 | "args": [ 13 | "build", 14 | "ResolveGraphQL.csproj" 15 | ], 16 | "presentation": { 17 | "reveal": "always" 18 | }, 19 | "problemMatcher": [] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /DataModel/StarWarsContext.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace ResolveGraphQL.DataModel 5 | { 6 | public class StarWarsContext : DbContext 7 | { 8 | public DbSet Humans { get; set; } 9 | public DbSet Droids { get; set; } 10 | public DbSet HumanFriends { get; set; } 11 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 12 | { 13 | optionsBuilder.UseSqlite("Filename=./starwars.db"); 14 | } 15 | } 16 | 17 | public class Human : ICharacter 18 | { 19 | public int HumanId { get; set; } 20 | public string Name { get; set; } 21 | public string HomePlanet { get; set; } 22 | public List Friends { get; set; } 23 | } 24 | 25 | public class HumanFreind 26 | { 27 | public int HumanFreindId { get; set; } 28 | public int HumanId { get; set; } 29 | public int DroidId { get; set; } 30 | public Human Human { get; set; } 31 | public Droid Droid { get; set; } 32 | } 33 | 34 | public class Droid : ICharacter 35 | { 36 | public int DroidId { get; set; } 37 | public string Name { get; set; } 38 | public string PrimaryFunction { get; set; } 39 | public List Friends { get; set; } 40 | } 41 | 42 | public interface ICharacter 43 | { 44 | string Name { get; set; } 45 | 46 | List Friends { get; set; } 47 | } 48 | } -------------------------------------------------------------------------------- /NodeCollection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Concurrent; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using GraphQL.Types; 7 | 8 | namespace ResolveGraphQL 9 | { 10 | public class NodeCollection : IEnumerable> 11 | { 12 | private readonly ConcurrentDictionary _relations = new ConcurrentDictionary(); 13 | 14 | private readonly GraphNode[] _nodes; 15 | 16 | public NodeCollection(IEnumerable nodes) 17 | { 18 | _nodes = nodes.Select(n => new GraphNode(n, this)).ToArray(); 19 | } 20 | 21 | internal IndexedNodeCollection GetOrAddRelation( 22 | NodeCollectionIndexer indexer, 23 | Func> childCollectionLoader) 24 | { 25 | return (IndexedNodeCollection)_relations.GetOrAdd( 26 | indexer, 27 | rn => 28 | { 29 | var collection = childCollectionLoader(); 30 | return indexer.Apply(collection); 31 | }); 32 | } 33 | 34 | public IEnumerator> GetEnumerator() 35 | { 36 | return ((IEnumerable>)_nodes).GetEnumerator(); 37 | } 38 | 39 | IEnumerator IEnumerable.GetEnumerator() 40 | { 41 | return _nodes.GetEnumerator(); 42 | } 43 | } 44 | public class IndexedNodeCollection 45 | { 46 | private Dictionary[]> _index; 47 | 48 | public IndexedNodeCollection(Dictionary[]> index) 49 | { 50 | _index = index; 51 | } 52 | 53 | public GraphNode GetSingleByKey(TIndex key) 54 | { 55 | return _index != null && _index.ContainsKey(key) 56 | ? _index[key].SingleOrDefault() 57 | : default(GraphNode); 58 | } 59 | 60 | public GraphNode[] GetManyByKey(TIndex key) 61 | { 62 | return _index != null && _index.ContainsKey(key) 63 | ? _index[key].ToArray() 64 | : null; 65 | } 66 | 67 | } 68 | 69 | public class NodeCollectionIndexer 70 | { 71 | private readonly Func, Dictionary[]>> _indexFunc; 72 | public NodeCollectionIndexer(Func, Dictionary[]>> indexFunc) 73 | { 74 | _indexFunc = indexFunc; 75 | } 76 | 77 | public IndexedNodeCollection Apply(NodeCollection collection) 78 | { 79 | return new IndexedNodeCollection(_indexFunc(collection)); 80 | } 81 | } 82 | 83 | public class GraphNode 84 | { 85 | public GraphNode(T node, NodeCollection collection) 86 | { 87 | Node = node; 88 | Collection = collection; 89 | } 90 | 91 | public T Node { get; protected set; } 92 | 93 | public NodeCollection Collection { get; protected set; } 94 | } 95 | 96 | public static class GraphNodeExtensions 97 | { 98 | public static T GetGraphNode(this ResolveFieldContext context) 99 | { 100 | return ((GraphNode)context.Source).Node; 101 | } 102 | 103 | public static T GetGraphNode(this ResolveFieldContext> context) 104 | { 105 | return context.Source.Node; 106 | } 107 | 108 | public static NodeCollection GetNodeCollection(this ResolveFieldContext context) 109 | { 110 | return ((GraphNode)context.Source).Collection; 111 | } 112 | 113 | public static NodeCollection GetNodeCollection(this ResolveFieldContext> context) 114 | { 115 | return context.Source.Collection; 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using GraphQL; 5 | using GraphQL.Http; 6 | using GraphQL.Types; 7 | using ResolveGraphQL.DataModel; 8 | using ResolveGraphQL.Schema; 9 | using Unity; 10 | 11 | namespace ResolveDataModel 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | using (var db = new StarWarsContext()) 18 | { 19 | // insert some testing data into database 20 | if (db.Humans.Count() == 0) 21 | InsertData(db); 22 | 23 | var container = new UnityContainer(); 24 | 25 | container.RegisterInstance(db); 26 | 27 | container.RegisterType(); 28 | container.RegisterType(); 29 | container.RegisterType(); 30 | 31 | var schema = new StarWarsSchema((t) => container.Resolve(t) as GraphType); 32 | 33 | var query = @" 34 | query AllHumansQuery { 35 | humans { 36 | name 37 | friends { 38 | id 39 | name 40 | ...on Droid { 41 | primaryFunction 42 | } 43 | } 44 | } 45 | }"; 46 | 47 | Console.WriteLine("Run AllHumansQuery"); 48 | 49 | var result = Execute(schema, null, query); 50 | 51 | Console.WriteLine(result.Result); 52 | Console.WriteLine(); 53 | 54 | query = @" 55 | query AllCharactersQuery { 56 | characters { 57 | name 58 | friends { 59 | name 60 | } 61 | } 62 | }"; 63 | 64 | Console.WriteLine("Run AllCharactersQuery"); 65 | 66 | result = Execute(schema, null, query); 67 | 68 | Console.WriteLine(result.Result); 69 | } 70 | } 71 | 72 | public static async Task Execute( 73 | Schema schema, 74 | object rootObject, 75 | string query, 76 | string operationName = null, 77 | Inputs inputs = null) 78 | { 79 | var executer = new DocumentExecuter(); 80 | var writer = new DocumentWriter(); 81 | 82 | var result = await executer.ExecuteAsync(schema, rootObject, query, operationName, inputs); 83 | return writer.Write(result); 84 | } 85 | 86 | private static void InsertData(StarWarsContext db) 87 | { 88 | db.Humans.Add(new Human 89 | { 90 | HumanId = 1, 91 | Name = "Luke", 92 | HomePlanet = "Tatooine" 93 | }); 94 | 95 | db.Humans.Add(new Human 96 | { 97 | HumanId = 2, 98 | Name = "Vader", 99 | HomePlanet = "Tatooine" 100 | }); 101 | 102 | db.Droids.Add(new Droid 103 | { 104 | DroidId = 1, 105 | Name = "R2-D2", 106 | PrimaryFunction = "Astromech" 107 | }); 108 | 109 | db.Droids.Add(new Droid 110 | { 111 | DroidId = 2, 112 | Name = "C-3PO", 113 | PrimaryFunction = "Protocol" 114 | }); 115 | 116 | db.HumanFriends.Add(new HumanFreind 117 | { 118 | HumanId = 1, 119 | DroidId = 1 120 | }); 121 | 122 | db.HumanFriends.Add(new HumanFreind 123 | { 124 | HumanId = 1, 125 | DroidId = 2 126 | }); 127 | 128 | db.HumanFriends.Add(new HumanFreind 129 | { 130 | HumanId = 2, 131 | DroidId = 1 132 | }); 133 | 134 | var count = db.SaveChanges(); 135 | Console.WriteLine("{0} records saved to database", count); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ResolveGraphQL [![Build Status](https://travis-ci.org/ethanli83/ResolveGraphQL.svg?branch=master)](https://travis-ci.org/ethanli83/ResolveGraphQL) 2 | An example of how to solve the N+1 problem for GraphQL using graphql-dotnet 3 | 4 | # Setup .Net core 5 | This exmaple uses .net core. You can get it and learn more about it from 6 | https://www.microsoft.com/net/core 7 | 8 | # Setup Entity Framework 9 | I use entity framework + sqlite to create a simple database to run queries. 10 | 11 | You will need to run the commands below if you are running the code for the 12 | first time. 13 | 14 | ### 0. Restore packages 15 | 16 | dotnet restore 17 | 18 | ### 1. Create migration file 19 | 20 | dotnet ef migrations add InitialSetup 21 | 22 | ### 2. Update database with generated schema 23 | 24 | dotnet ef database update 25 | 26 | If you want to recreate the database, run the following commands to drop the db 27 | first, then you can run the previous commands to create it again. 28 | 29 | ### 1. Drop the database first 30 | 31 | dotnet ef database drop 32 | 33 | ### 2. Remove the migration file 34 | 35 | dotnet ef migrations remove 36 | 37 | # The concept of the solution. 38 | In the original resolving process, siblings are resolved individually. 39 | Which means that even though all of the children of some given siblings 40 | are stored in the same table, we will call the database once for each sibling, 41 | hitting the same table over and over. This is inefficient for most databases 42 | such as mssql, or sqlite. 43 | 44 | This sample code tries to improve the performance of resolving graphql by 45 | only calling the database once per child property. For example, assuming, 46 | in the StarWars database, we have 3 humans and each of them has 2 friends. 47 | 48 | When we resolve this graphql 49 | 50 | ```graphql 51 | query HumansQuery { 52 | humans { 53 | name 54 | friends { 55 | name 56 | } 57 | } 58 | } 59 | ``` 60 | 61 | We only want to call the database once for resolving `friends`, rather than 62 | calling the database 3 times. 63 | 64 | To achieve this goal, we need two pieces of information when we resolve a 65 | child property of a sibling. Firstly, we need to have access to all of the 66 | siblings so that we can compose a query to the database containing all the 67 | parent ids (that is, the ids of the siblings). Secondly, we need a way to 68 | tell if the query for the child property has already been called to ensure 69 | we only call the database once. 70 | 71 | In this code, we have two core classes, namely: 72 | `GraphNode` and `NodeCollection`. 73 | 74 | `GraphNode` has a property called `Collection` which contains all of the 75 | siblings of the Node. 76 | 77 | `NodeCollection` has a loader function, and a private field `_nodes`. 78 | This class will only call the loader function once to fill the `_nodes` field 79 | and will not call the loader again if `_nodes` has already been initialized. 80 | 81 | `NodeCollection` also has a private dictionary called `_relations`. 82 | This dictionary stores all of the resolved child properties. When resolving a 83 | child, we first check the dictionary to see if the child has already been resolved. 84 | If it has, we grab the stored result rather than calling the database again. 85 | 86 | With help from these two classes, we achieve the goal of only calling the 87 | database once per child property. 88 | 89 | Now, let's have a look at a real example. 90 | 91 | Here's how the resolve function of the `friends` field is implemented for `HumanType`: 92 | 93 | ```csharp 94 | public class HumanType : ObjectGraphType> 95 | { 96 | // This indexer will generate a map from each human to their friends. 97 | // It will perform better than search for droids in for loop. 98 | private static NodeCollectionIndexer _friendsIndexer = 99 | new NodeCollectionIndexer( 100 | nc => nc. 101 | Select(n => n.Node). 102 | SelectMany(h => h.Friends). 103 | GroupBy(h => h.HumanId). 104 | ToDictionary(g => g.Key, g => g.Select(f => new GraphNode(f.Droid, nc)).ToArray())); 105 | 106 | public HumanType(StarWarsContext db) 107 | { 108 | Name = "Human"; 109 | 110 | Field(x => x.Node.HumanId). 111 | Name("id"). 112 | Description("The id of the human."); 113 | 114 | Field(x => x.Node.Name). 115 | Name("name"). 116 | Description("The name of the human."); 117 | 118 | Field(x => x.Node.HomePlanet). 119 | Name("homePlanet"). 120 | Description("The home planet of the human."); 121 | 122 | Field>( 123 | "friends", 124 | resolve: context => 125 | { 126 | // All of the humans we are resolving in the query. 127 | var collection = context.GetNodeCollection(); 128 | 129 | // Fetch all of the friends for every human we are resolving (only once). 130 | // GetOrAddRelation will first check if there is a stored result for the indexer 131 | // if the result exists, it will immediately return the stored result 132 | // otherwise, it will create a new NodeCollection using the given loader function 133 | var indexedCollection = collection.GetOrAddRelation( 134 | _friendsIndexer, 135 | () => 136 | { 137 | var humanIds = collection.Select(n => n.Node.HumanId).ToArray(); 138 | 139 | Console.WriteLine("Loading all friends for humans"); 140 | var droids = db.Droids. 141 | Where(d => d.Friends.Any(f => humanIds.Contains(f.HumanId))). 142 | Include(d => d.Friends). 143 | ToList(); 144 | 145 | return new NodeCollection(droids); 146 | }); 147 | 148 | // Return the friends of the human currently being resolved. 149 | var human = context.GetGraphNode(); 150 | return indexedCollection.GetManyByKey(human.HumanId); 151 | } 152 | ); 153 | 154 | Interface(); 155 | 156 | IsTypeOf = value => value is GraphNode; 157 | } 158 | } 159 | ``` 160 | -------------------------------------------------------------------------------- /ResolveGraphQL.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp2.0 4 | aspnet-webapi-DB5C0519-EE6F-4073-9532-CEC7A3FD4963 5 | 6 | 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Schema/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanli83/ResolveGraphQL/3d4b200d96e3ecb1276fbc69aae0900533a3ab62/Schema/.DS_Store -------------------------------------------------------------------------------- /Schema/StarWarsQuery.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using GraphQL.Types; 4 | using ResolveGraphQL.DataModel; 5 | 6 | namespace ResolveGraphQL.Schema 7 | { 8 | public class StarWarsQuery : ObjectGraphType 9 | { 10 | public StarWarsQuery(StarWarsContext db) 11 | { 12 | Name = "Query"; 13 | 14 | Field("hero", resolve: context => null ); 15 | 16 | Field( 17 | "human", 18 | arguments: new QueryArguments( 19 | new QueryArgument> { Name = "id", Description = "id of the human" } 20 | ), 21 | resolve: context => 22 | { 23 | var id = context.GetArgument("id"); 24 | Console.WriteLine("Loading human: " + id); 25 | var humans = db.Humans.Where(h => h.HumanId == id).ToList(); 26 | var collection = new NodeCollection(humans); 27 | 28 | return collection.Single(); 29 | } 30 | ); 31 | 32 | Field>( 33 | "humans", 34 | resolve: context => { 35 | Console.WriteLine("Loading all humans"); 36 | var humans = db.Humans.ToList(); 37 | var collection = new NodeCollection(humans); 38 | 39 | return collection; 40 | } 41 | ); 42 | 43 | Field( 44 | "droid", 45 | arguments: new QueryArguments( 46 | new QueryArgument> { Name = "id", Description = "id of the droid" } 47 | ), 48 | resolve: context => 49 | { 50 | var id = context.GetArgument("id"); 51 | Console.WriteLine("Loading droid: " + id); 52 | var driods = db.Droids.Where(d => d.DroidId == id).ToList(); 53 | var collection = new NodeCollection(driods); 54 | 55 | return collection.Single(); 56 | } 57 | ); 58 | 59 | Field>( 60 | "characters", 61 | resolve: context => { 62 | Console.WriteLine("Loading all humans for characters"); 63 | var humans = new NodeCollection(db.Humans.ToList()); 64 | 65 | Console.WriteLine("Loading all droids for characters"); 66 | var droids = new NodeCollection(db.Droids.ToList()); 67 | 68 | return humans.Cast().Union(droids); 69 | } 70 | ); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Schema/StarWarsSchema.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using GraphQL.Types; 3 | 4 | namespace ResolveGraphQL.Schema 5 | { 6 | public class StarWarsSchema : GraphQL.Types.Schema 7 | { 8 | public StarWarsSchema(Func resolveType) 9 | : base(resolveType) 10 | { 11 | Query = (ObjectGraphType)resolveType(typeof (StarWarsQuery)); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Schema/Types/CharacterInterface.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | 3 | namespace ResolveGraphQL.Schema 4 | { 5 | public class CharacterInterface : InterfaceGraphType 6 | { 7 | public CharacterInterface() 8 | { 9 | Name = "Character"; 10 | 11 | Field>("id", "The id of the character."); 12 | 13 | Field("name", "The name of the character."); 14 | 15 | Field>("friends"); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Schema/Types/DroidType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using GraphQL.Types; 4 | using Microsoft.EntityFrameworkCore; 5 | using ResolveGraphQL.DataModel; 6 | 7 | namespace ResolveGraphQL.Schema 8 | { 9 | public class DroidType : ObjectGraphType> 10 | { 11 | private static NodeCollectionIndexer _friendsIndexer = 12 | new NodeCollectionIndexer( 13 | nc => nc. 14 | Select(n => n.Node). 15 | SelectMany(h => h.Friends). 16 | GroupBy(h => h.DroidId). 17 | ToDictionary(g => g.Key, g => g.Select(f => new GraphNode(f.Human, nc)).ToArray())); 18 | 19 | public DroidType(StarWarsContext db) 20 | { 21 | Name = "Droid"; 22 | Description = "A mechanical creature in the Star Wars universe."; 23 | 24 | Field(x => x.Node.DroidId). 25 | Name("id"). 26 | Description("The id of the droid."); 27 | 28 | Field(x => x.Node.Name). 29 | Name("name"). 30 | Description("The name of the droid."); 31 | 32 | Field(x => x.Node.PrimaryFunction). 33 | Name("primaryFunction"). 34 | Description("The primary function of the droid."); 35 | 36 | Field>( 37 | "friends", 38 | resolve: context => 39 | { 40 | var collection = context.GetNodeCollection(); 41 | var indexedCollection = collection.GetOrAddRelation( 42 | _friendsIndexer, 43 | () => 44 | { 45 | var droidIds = collection.Select(n => n.Node.DroidId).ToArray(); 46 | 47 | Console.WriteLine("Loading all friends for droids"); 48 | var humans = db.Humans. 49 | Where(d => d.Friends.Any(f => droidIds.Contains(f.DroidId))). 50 | Include(d => d.Friends). 51 | ToList(); 52 | 53 | return new NodeCollection(humans); 54 | }); 55 | 56 | var droid = context.GetGraphNode(); 57 | return indexedCollection.GetManyByKey(droid.DroidId); 58 | } 59 | ); 60 | 61 | Interface(); 62 | 63 | IsTypeOf = value => value is GraphNode; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Schema/Types/HumanType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using GraphQL.Types; 4 | using Microsoft.EntityFrameworkCore; 5 | using ResolveGraphQL.DataModel; 6 | 7 | namespace ResolveGraphQL.Schema 8 | { 9 | public class HumanType : ObjectGraphType> 10 | { 11 | // This indexer will generate a map from each human to their friends. 12 | // It will perform better than search for droids in for loop. 13 | private static NodeCollectionIndexer _friendsIndexer = 14 | new NodeCollectionIndexer( 15 | nc => nc. 16 | Select(n => n.Node). 17 | SelectMany(h => h.Friends). 18 | GroupBy(h => h.HumanId). 19 | ToDictionary(g => g.Key, g => g.Select(f => new GraphNode(f.Droid, nc)).ToArray())); 20 | 21 | public HumanType(StarWarsContext db) 22 | { 23 | Name = "Human"; 24 | 25 | Field(x => x.Node.HumanId). 26 | Name("id"). 27 | Description("The id of the human."); 28 | 29 | Field(x => x.Node.Name). 30 | Name("name"). 31 | Description("The name of the human."); 32 | 33 | Field(x => x.Node.HomePlanet). 34 | Name("homePlanet"). 35 | Description("The home planet of the human."); 36 | 37 | Field>( 38 | "friends", 39 | resolve: context => 40 | { 41 | // All of the humans we are resolving in the query. 42 | var collection = context.GetNodeCollection(); 43 | 44 | // Fetch all of the friends for every human we are resolving (only once). 45 | // GetOrAddRelation will first check if there is a stored result for the indexer 46 | // if the result exists, it will immediately return the stored result 47 | // otherwise, it will create a new NodeCollection using the given loader function 48 | var indexedCollection = collection.GetOrAddRelation( 49 | _friendsIndexer, 50 | () => 51 | { 52 | var humanIds = collection.Select(n => n.Node.HumanId).ToArray(); 53 | 54 | Console.WriteLine("Loading all friends for humans"); 55 | var droids = db.Droids. 56 | Where(d => d.Friends.Any(f => humanIds.Contains(f.HumanId))). 57 | Include(d => d.Friends). 58 | ToList(); 59 | 60 | return new NodeCollection(droids); 61 | }); 62 | 63 | // Return the friends of the human currently being resolved. 64 | var human = context.GetGraphNode(); 65 | return indexedCollection.GetManyByKey(human.HumanId); 66 | } 67 | ); 68 | 69 | Interface(); 70 | 71 | IsTypeOf = value => value is GraphNode; 72 | } 73 | } 74 | } 75 | --------------------------------------------------------------------------------