├── src ├── dotnet-gqlgen │ ├── ITypeInfo.cs │ ├── ISchemaNode.cs │ ├── GqlFieldNameAttribute.cs │ ├── SchemaException.cs │ ├── FieldConsumer.cs │ ├── GraphQLSchema.g4 │ ├── dotnet-gqlgen.csproj │ ├── types.cshtml │ ├── SchemaCompiler.cs │ ├── client.cshtml │ ├── Program.cs │ ├── SchemaVisitor.cs │ ├── SchemaInfo.cs │ └── BaseGraphQLClient.cs └── tests │ └── DotNetGraphQLQueryGen.Tests │ ├── DotNetGraphQLQueryGen.Tests.csproj │ ├── schema.graphql │ ├── VisitorTests.cs │ ├── TestMakeQuery.cs │ └── GeneratedTypes.cs ├── .gitignore ├── .circleci └── config.yml ├── CHANGELOG.md ├── LICENSE ├── DotNetGraphQLQueryGen.sln └── README.md /src/dotnet-gqlgen/ITypeInfo.cs: -------------------------------------------------------------------------------- 1 | namespace dotnet_gqlgen 2 | { 3 | internal interface ITypeInfo 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /src/dotnet-gqlgen/ISchemaNode.cs: -------------------------------------------------------------------------------- 1 | namespace dotnet_gqlgen 2 | { 3 | internal interface ISchemaNode 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | package-lock.json 3 | .vscode/ 4 | .idea/ 5 | obj/ 6 | .DS_Store 7 | .antlr/ 8 | 9 | # User-specific files 10 | *.suo 11 | *.user 12 | *.userosscache 13 | *.sln.docstates 14 | 15 | # Visual Studio 2015 cache/options directory 16 | .vs/ -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: microsoft/dotnet:2.2-sdk 6 | steps: 7 | - checkout 8 | - run: 9 | name: Run unit tests 10 | command: dotnet test src/tests/DotNetGraphQLQueryGen.Tests/ -------------------------------------------------------------------------------- /src/dotnet-gqlgen/GqlFieldNameAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DotNetGqlClient 4 | { 5 | public class GqlFieldNameAttribute : Attribute 6 | { 7 | public string Name { get; } 8 | 9 | public GqlFieldNameAttribute(string name) 10 | { 11 | this.Name = name; 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.0 2 | - Inital version, given a GraphQL schema file we generate 3 | - C# interfaces to write strongly typed queries against 4 | - C# classes for any Input types so we can actually use the classes and initialise with values 5 | - Generate a sample client for users to start implemented their own auth (if required) 6 | - Use scalars from schema to make better choices on generated interfaces 7 | - Support using typed objects instead of anonymous only -------------------------------------------------------------------------------- /src/dotnet-gqlgen/SchemaException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace dotnet_gqlgen 5 | { 6 | public class SchemaException : Exception 7 | { 8 | public SchemaException(string message) : base(message) 9 | { 10 | } 11 | 12 | public SchemaException(string message, Exception innerException) : base(message, innerException) 13 | { 14 | } 15 | 16 | protected SchemaException(SerializationInfo info, StreamingContext context) : base(info, context) 17 | { 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/dotnet-gqlgen/FieldConsumer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace dotnet_gqlgen 5 | { 6 | internal class FieldConsumer : IDisposable 7 | { 8 | private readonly SchemaVisitor schemaVisitor; 9 | private List schema; 10 | 11 | public FieldConsumer(SchemaVisitor schemaVisitor, List schema) 12 | { 13 | this.schemaVisitor = schemaVisitor; 14 | this.schema = schema; 15 | schemaVisitor.SetFieldConsumer(schema); 16 | } 17 | 18 | public void Dispose() 19 | { 20 | schemaVisitor.SetFieldConsumer(null); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/tests/DotNetGraphQLQueryGen.Tests/DotNetGraphQLQueryGen.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tests for dotnet-gqlgen 5 | netcoreapp2.2 6 | true 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Luke Murray. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/dotnet-gqlgen/GraphQLSchema.g4: -------------------------------------------------------------------------------- 1 | grammar GraphQLSchema; 2 | 3 | // This is our expression language 4 | schema : (schemaDef | typeDef | scalarDef | inputDef | enumDef)+; 5 | 6 | schemaDef : comment* 'schema' ws* objectDef; 7 | typeDef : comment* 'type ' ws* typeName=NAME ws* objectDef; 8 | scalarDef : comment* 'scalar' ws* typeName=NAME ws+; 9 | inputDef : comment* 'input' ws* typeName=NAME ws* objectDef; 10 | enumDef : comment* 'enum' ws* typeName=NAME ws* '{' (ws* enumItem ws* comment* ws*)+ '}' ws*; 11 | 12 | objectDef : '{' ws* fieldDef (ws* fieldDef)* ws* '}' ws*; 13 | 14 | fieldDef : comment* name=NAME ('(' args=arguments ')')? ws* ':' ws* type=dataType; 15 | enumItem : comment* name=NAME; 16 | arguments : ws* argument (ws* ','* ws* argument)*; 17 | argument : NAME ws* ':' ws* dataType; 18 | 19 | dataType : (type=NAME required='!'? | '[' arrayType=NAME elementTypeRequired='!'? ']' arrayRequired='!'?); 20 | NAME : [a-z_A-Z] [a-z_A-Z0-9-]*; 21 | 22 | comment : ws* (('"' ~('\n'|'\r')* '"') | ('"""' ~'"""'* '"""') | ('#' ~('\n'|'\r')*)) ws*; 23 | 24 | ws : ' ' | '\t' | '\n' | '\r'; 25 | -------------------------------------------------------------------------------- /src/dotnet-gqlgen/dotnet-gqlgen.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.2 6 | dotnet_gqlgen 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | MSBuild:Compile 26 | GraphQLSchema.Grammer 27 | False 28 | True 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/tests/DotNetGraphQLQueryGen.Tests/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: RootQuery 3 | mutation: Mutation 4 | } 5 | 6 | scalar Date 7 | 8 | type RootQuery { 9 | "Pagination. [defaults: page = 1, pagesize = 10]" 10 | actorPager(page: Int, pagesize: Int, search: String): PersonPagination 11 | "List of actors" 12 | actors: [Person] 13 | "List of directors" 14 | directors: [Person] 15 | "Return a Movie by its Id" 16 | movie(id: Int!): Movie 17 | "Collection of Movies" 18 | movies: [Movie] 19 | "Collection of Peoples" 20 | people: [Person] 21 | "Return a Person by its Id" 22 | person(id: Int!): Person 23 | "List of writers" 24 | writers: [Person] 25 | 26 | } 27 | 28 | type SubscriptionType { 29 | name: String 30 | } 31 | 32 | """ 33 | This is a movie entity 34 | """ 35 | type Movie { 36 | id: Int! 37 | name: String! 38 | "Enum of Genre" 39 | genre: Int! 40 | released: Date! 41 | "Actors in the movie" 42 | actors: [Person!]! 43 | "Writers in the movie" 44 | writers: [Person!]! 45 | director: Person! 46 | directorId: Int! 47 | rating: Float 48 | } 49 | 50 | type Actor { 51 | personId: Int! 52 | person: Person! 53 | movieId: Int! 54 | movie: Movie! 55 | } 56 | 57 | type Person { 58 | id: Int! 59 | firstName: String! 60 | lastName: String! 61 | dob: Date! 62 | "Movies they acted in" 63 | actorIn: [Movie] 64 | "Movies they wrote" 65 | writerOf: [Movie] 66 | directorOf: [Movie] 67 | died: Date 68 | isDeleted: Boolean! 69 | "Show the person's age" 70 | age: Int! 71 | "Person's name" 72 | name: String! 73 | } 74 | 75 | type Writer { 76 | personId: Int! 77 | person: Person! 78 | movieId: Int! 79 | movie: Movie! 80 | } 81 | 82 | type PersonPagination { 83 | "total records to match search" 84 | total: Int! 85 | "total pages based on page size" 86 | pageCount: Int! 87 | "collection of people" 88 | people: [Person!] 89 | } 90 | 91 | input Detail { 92 | description: String! 93 | } 94 | 95 | 96 | type Mutation { 97 | "Add a new Movie object" 98 | addMovie(name: String!, rating: Float, details: [Detail], genre: Int, released: Date): Movie 99 | addActor(firstName: String, lastName: String, movieId: Int): Person 100 | addActor2(firstName: String, lastName: String, movieId: Int): Person 101 | 102 | } -------------------------------------------------------------------------------- /src/dotnet-gqlgen/types.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | DisableEncoding = true; 3 | } 4 | @using dotnet_gqlgen; 5 | 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq.Expressions; 9 | using DotNetGqlClient; 10 | 11 | /// 12 | /// Generated interfaces for making GraphQL API calls with a typed interface. 13 | /// 14 | /// Generated on @DateTime.Now from @Model.SchemaFile with arguments @Model.CmdArgs 15 | /// 16 | 17 | namespace @Model.Namespace 18 | { 19 | 20 | @foreach(var kvp in Model.Enums) 21 | { 22 | @:public enum @kvp.Key { 23 | @foreach(var field in kvp.Value) 24 | { 25 | @:@field, 26 | } 27 | @:} 28 | } 29 | 30 | @foreach(var gqlType in Model.Types.Values) 31 | { 32 | if (!string.IsNullOrEmpty(gqlType.Description)) 33 | { 34 | @:/// 35 | @:/// @gqlType.Description 36 | @:/// 37 | } 38 | if (gqlType.IsInput) 39 | { 40 | @:public class @gqlType.Name 41 | } 42 | else 43 | { 44 | @:public interface @gqlType.Name 45 | } 46 | @:{ 47 | @foreach(var field in gqlType.Fields) 48 | { 49 | @if (field.ShouldBeProperty || gqlType.IsInput) 50 | { 51 | @if (!string.IsNullOrEmpty(field.Description)) 52 | { 53 | @:/// 54 | @:/// @field.Description 55 | @:/// 56 | } 57 | @:[GqlFieldName("@field.Name")] 58 | if (gqlType.IsInput) 59 | { 60 | @:public @field.DotNetType @field.DotNetName { get; set; } 61 | } 62 | else 63 | { 64 | @:@field.DotNetType @field.DotNetName { get; } 65 | } 66 | } 67 | else 68 | { 69 | if (gqlType != Model.Mutation) 70 | { 71 | @:/// 72 | @if (!string.IsNullOrEmpty(field.Description)) 73 | { 74 | @:/// @field.Description 75 | @:/// 76 | } 77 | @:/// This shortcut will return a selection of all fields 78 | @:/// 79 | @:[GqlFieldName("@field.Name")] 80 | @:@field.DotNetType @(field.DotNetName)(); 81 | } 82 | 83 | @:/// 84 | @if (!string.IsNullOrEmpty(field.Description)) 85 | { 86 | @:/// @field.Description 87 | @:/// 88 | } 89 | @:/// 90 | @:/// Projection of fields to select from the object 91 | @:[GqlFieldName("@field.Name")] 92 | @field.OutputMethodSig() 93 | } 94 | } 95 | @:} 96 | } 97 | 98 | } -------------------------------------------------------------------------------- /src/dotnet-gqlgen/SchemaCompiler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Antlr4.Runtime; 5 | using Antlr4.Runtime.Misc; 6 | using GraphQLSchema.Grammer; 7 | 8 | namespace dotnet_gqlgen 9 | { 10 | public static class SchemaCompiler 11 | { 12 | public static SchemaInfo Compile(string schemaText, Dictionary typeMappings = null) 13 | { 14 | try 15 | { 16 | var stream = new AntlrInputStream(schemaText); 17 | var lexer = new GraphQLSchemaLexer(stream); 18 | var tokens = new CommonTokenStream(lexer); 19 | var parser = new GraphQLSchemaParser(tokens); 20 | parser.BuildParseTree = true; 21 | parser.ErrorHandler = new BailErrorStrategy(); 22 | var tree = parser.schema(); 23 | var visitor = new SchemaVisitor(typeMappings); 24 | // visit each node. it will return a linq expression for each entity requested 25 | visitor.Visit(tree); 26 | 27 | if (visitor.SchemaInfo.Schema == null || !visitor.SchemaInfo.Schema.Any(f => f.Name == "query")) 28 | { 29 | throw new SchemaException("A schema definition is required and must define the query type e.g. \"schema { query: MyQueryType }\""); 30 | } 31 | 32 | return visitor.SchemaInfo; 33 | } 34 | catch (ParseCanceledException pce) 35 | { 36 | if (pce.InnerException != null) 37 | { 38 | if (pce.InnerException is NoViableAltException) 39 | { 40 | var nve = (NoViableAltException)pce.InnerException; 41 | throw new SchemaException($"Error: line {nve.OffendingToken.Line}:{nve.OffendingToken.Column} no viable alternative at input '{nve.OffendingToken.Text}'"); 42 | } 43 | else if (pce.InnerException is InputMismatchException) 44 | { 45 | var ime = (InputMismatchException)pce.InnerException; 46 | var expecting = string.Join(", ", ime.GetExpectedTokens()); 47 | throw new SchemaException($"Error: line {ime.OffendingToken.Line}:{ime.OffendingToken.Column} extraneous input '{ime.OffendingToken.Text}' expecting {expecting}"); 48 | } 49 | throw new SchemaException(pce.InnerException.Message); 50 | } 51 | throw new SchemaException(pce.Message); 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/dotnet-gqlgen/client.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | DisableEncoding = true; 3 | } 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq.Expressions; 7 | using System.Net.Http; 8 | using System.Net.Http.Headers; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using DotNetGqlClient; 12 | using Newtonsoft.Json; 13 | 14 | /// 15 | /// Generated interfaces for making GraphQL API calls with a typed interface. 16 | /// 17 | /// Generated on @DateTime.Now from @Model.SchemaFile 18 | /// 19 | 20 | namespace @Model.Namespace 21 | { 22 | public class GqlError 23 | { 24 | public string Message { get; set; } 25 | } 26 | public class GqlResult 27 | { 28 | public List Errors { get; set; } 29 | public TQuery Data { get; set; } 30 | } 31 | 32 | public class @Model.ClientClassName : BaseGraphQLClient 33 | { 34 | private Uri apiUrl; 35 | private readonly HttpClient client; 36 | 37 | public @(Model.ClientClassName)() 38 | { 39 | this.apiUrl = new Uri(""); 40 | this.client = new HttpClient(); 41 | this.client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 42 | } 43 | 44 | public @(Model.ClientClassName)(HttpClient client) 45 | : this(client.BaseAddress, client) 46 | { 47 | } 48 | 49 | public @(Model.ClientClassName)(Uri apiUrl, HttpClient client) 50 | { 51 | this.apiUrl = apiUrl; 52 | this.client = client; 53 | this.client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 54 | } 55 | 56 | private async Task> ProcessResult(string gql) 57 | { 58 | // gql is a GraphQL doc e.g. { query MyQuery { stuff { id name } } } 59 | // you can replace the following with what ever HTTP library you use 60 | // don't forget to implement your authentication if required 61 | var req = new HttpRequestMessage { 62 | RequestUri = this.apiUrl, 63 | Method = HttpMethod.Post, 64 | }; 65 | var queryReq = new 66 | { 67 | Query = gql.ToString() 68 | }; 69 | req.Content = new StringContent(JsonConvert.SerializeObject(queryReq), Encoding.UTF8, "application/json"); 70 | // you will need to implement any auth 71 | // req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); 72 | var res = await client.SendAsync(req); 73 | var strResult = await res.Content.ReadAsStringAsync(); 74 | var data = JsonConvert.DeserializeObject>(strResult); 75 | return data; 76 | } 77 | 78 | public async Task> QueryAsync(Expression> query) 79 | { 80 | var gql = base.MakeQuery<@Model.Query.Name, TQuery>(query); 81 | return await ProcessResult(gql); 82 | } 83 | 84 | public async Task> MutateAsync(Expression> query) 85 | { 86 | var gql = base.MakeQuery<@Model.Mutation.Name, TQuery>(query, true); 87 | return await ProcessResult(gql); 88 | } 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /DotNetGraphQLQueryGen.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E65B3177-BF31-4B95-A594-06CF381474C7}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0112D762-344C-4545-9377-86D255E5AEAB}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetGraphQLQueryGen.Tests", "src\tests\DotNetGraphQLQueryGen.Tests\DotNetGraphQLQueryGen.Tests.csproj", "{FFEBA61F-81B5-44C3-8178-1723A7BDB8AC}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet-gqlgen", "src\dotnet-gqlgen\dotnet-gqlgen.csproj", "{8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Debug|x64 = Debug|x64 18 | Debug|x86 = Debug|x86 19 | Release|Any CPU = Release|Any CPU 20 | Release|x64 = Release|x64 21 | Release|x86 = Release|x86 22 | EndGlobalSection 23 | GlobalSection(SolutionProperties) = preSolution 24 | HideSolutionNode = FALSE 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC}.Debug|x64.ActiveCfg = Debug|Any CPU 30 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC}.Debug|x64.Build.0 = Debug|Any CPU 31 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC}.Debug|x86.ActiveCfg = Debug|Any CPU 32 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC}.Debug|x86.Build.0 = Debug|Any CPU 33 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC}.Release|x64.ActiveCfg = Release|Any CPU 36 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC}.Release|x64.Build.0 = Release|Any CPU 37 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC}.Release|x86.ActiveCfg = Release|Any CPU 38 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC}.Release|x86.Build.0 = Release|Any CPU 39 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Debug|x64.ActiveCfg = Debug|Any CPU 42 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Debug|x64.Build.0 = Debug|Any CPU 43 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Debug|x86.ActiveCfg = Debug|Any CPU 44 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Debug|x86.Build.0 = Debug|Any CPU 45 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Release|Any CPU.Build.0 = Release|Any CPU 47 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Release|x64.ActiveCfg = Release|Any CPU 48 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Release|x64.Build.0 = Release|Any CPU 49 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Release|x86.ActiveCfg = Release|Any CPU 50 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Release|x86.Build.0 = Release|Any CPU 51 | EndGlobalSection 52 | GlobalSection(NestedProjects) = preSolution 53 | {0112D762-344C-4545-9377-86D255E5AEAB} = {E65B3177-BF31-4B95-A594-06CF381474C7} 54 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC} = {0112D762-344C-4545-9377-86D255E5AEAB} 55 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C} = {E65B3177-BF31-4B95-A594-06CF381474C7} 56 | EndGlobalSection 57 | EndGlobal 58 | -------------------------------------------------------------------------------- /src/dotnet-gqlgen/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.IO; 5 | using System.Linq; 6 | using McMaster.Extensions.CommandLineUtils; 7 | using RazorLight; 8 | 9 | namespace dotnet_gqlgen 10 | { 11 | public class Program 12 | { 13 | [Argument(0, Description = "Path the the GraphQL schema file")] 14 | [Required] 15 | public string SchemaFile { get; } 16 | 17 | [Option(LongName = "namespace", ShortName = "n", Description = "Namespace to generate code under")] 18 | public string Namespace { get; } = "Generated"; 19 | 20 | [Option(LongName = "client_class_name", ShortName = "c", Description = "Name for the client class")] 21 | public string ClientClassName { get; } = "GraphQLClient"; 22 | [Option(LongName = "scalar_mapping", ShortName = "m", Description = "Map of custom schema scalar types to dotnet types. Use \"GqlType=DotNetClassName,ID=Guid,...\"")] 23 | public string ScalarMapping { get; } 24 | [Option(LongName = "output", ShortName = "o", Description = "Output directory")] 25 | public string OutputDir { get; } = "output"; 26 | 27 | public static int Main(string[] args) => CommandLineApplication.Execute(args); 28 | 29 | private async void OnExecute() 30 | { 31 | try 32 | { 33 | Console.WriteLine($"Loading {SchemaFile}..."); 34 | var schemaText = File.ReadAllText(SchemaFile); 35 | 36 | var mappings = new Dictionary(); 37 | if (!string.IsNullOrEmpty(ScalarMapping)) 38 | { 39 | mappings = ScalarMapping.Split(',').Select(s => s.Split('=')).ToDictionary(k => k[0], v => v[1]); 40 | } 41 | 42 | // parse into AST 43 | var typeInfo = SchemaCompiler.Compile(schemaText, mappings); 44 | 45 | Console.WriteLine($"Generating types in namespace {Namespace}, outputting to {ClientClassName}.cs"); 46 | 47 | // pass the schema to the template 48 | var engine = new RazorLightEngineBuilder() 49 | .UseEmbeddedResourcesProject(typeof(Program)) 50 | .UseMemoryCachingProvider() 51 | .Build(); 52 | 53 | var allTypes = typeInfo.Types.Concat(typeInfo.Inputs).ToDictionary(k => k.Key, v => v.Value); 54 | 55 | string result = await engine.CompileRenderAsync("types.cshtml", new 56 | { 57 | Namespace = Namespace, 58 | SchemaFile = SchemaFile, 59 | Types = allTypes, 60 | Enums = typeInfo.Enums, 61 | Mutation = typeInfo.Mutation, 62 | CmdArgs = $"-n {Namespace} -c {ClientClassName} -m {ScalarMapping}" 63 | }); 64 | Directory.CreateDirectory(OutputDir); 65 | File.WriteAllText($"{OutputDir}/GeneratedTypes.cs", result); 66 | 67 | result = await engine.CompileRenderAsync("client.cshtml", new 68 | { 69 | Namespace = Namespace, 70 | SchemaFile = SchemaFile, 71 | Query = typeInfo.Query, 72 | Mutation = typeInfo.Mutation, 73 | ClientClassName = ClientClassName, 74 | }); 75 | File.WriteAllText($"{OutputDir}/{ClientClassName}.cs", result); 76 | 77 | Console.WriteLine($"Done."); 78 | } 79 | catch (Exception e) 80 | { 81 | Console.WriteLine("Error: " + e.ToString()); 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/tests/DotNetGraphQLQueryGen.Tests/VisitorTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | using dotnet_gqlgen; 4 | using Xunit; 5 | 6 | namespace DotNetGraphQLQueryGen.Tests 7 | { 8 | public class VisitorTests 9 | { 10 | [Fact] 11 | public void TestSchemaQueryRequired() 12 | { 13 | Assert.Throws(() => SchemaCompiler.Compile("schema { mutation: Mutation }")); 14 | } 15 | 16 | [Fact] 17 | public void TestSchemaQueryType() 18 | { 19 | var results = SchemaCompiler.Compile(File.ReadAllText("../../../schema.graphql")); 20 | Assert.Equal(2, results.Schema.Count); 21 | Assert.Equal(8, results.Types.Count); 22 | Assert.Single(results.Inputs); 23 | var queryTypeName = results.Schema.First(s => s.Name == "query").TypeName; 24 | 25 | var queryType = results.Types[queryTypeName]; 26 | Assert.Equal(8, queryType.Fields.Count); 27 | Assert.Equal("actors", queryType.Fields.ElementAt(1).Name); 28 | Assert.Equal("Person", queryType.Fields.ElementAt(1).TypeName); 29 | Assert.True(queryType.Fields.ElementAt(1).IsArray); 30 | Assert.Equal("person", queryType.Fields.ElementAt(6).Name); 31 | Assert.Equal("Person", queryType.Fields.ElementAt(6).TypeName); 32 | Assert.False(queryType.Fields.ElementAt(6).IsArray); 33 | Assert.Single(queryType.Fields.ElementAt(6).Args); 34 | Assert.Equal("id", queryType.Fields.ElementAt(6).Args.First().Name); 35 | Assert.Equal("Int", queryType.Fields.ElementAt(6).Args.First().TypeName); 36 | Assert.True(queryType.Fields.ElementAt(6).Args.First().Required); 37 | } 38 | 39 | [Fact] 40 | public void TestSchemaMutationType() 41 | { 42 | var results = SchemaCompiler.Compile(File.ReadAllText("../../../schema.graphql")); 43 | var mutationTypeName = results.Schema.First(s => s.Name == "mutation").TypeName; 44 | 45 | var mutType = results.Types[mutationTypeName]; 46 | Assert.Equal(3, mutType.Fields.Count); 47 | Assert.Equal("addMovie", mutType.Fields.ElementAt(0).Name); 48 | Assert.Equal("Add a new Movie object", mutType.Fields.ElementAt(0).Description); 49 | Assert.Equal("Movie", mutType.Fields.ElementAt(0).TypeName); 50 | Assert.False(mutType.Fields.ElementAt(0).IsArray); 51 | 52 | Assert.Equal(5, mutType.Fields.ElementAt(0).Args.Count); 53 | Assert.Equal("name", mutType.Fields.ElementAt(0).Args.ElementAt(0).Name); 54 | Assert.True(mutType.Fields.ElementAt(0).Args.ElementAt(0).Required); 55 | Assert.Equal("String", mutType.Fields.ElementAt(0).Args.ElementAt(0).TypeName); 56 | Assert.Equal("rating", mutType.Fields.ElementAt(0).Args.ElementAt(1).Name); 57 | Assert.Equal("Float", mutType.Fields.ElementAt(0).Args.ElementAt(1).TypeName); 58 | Assert.Equal("details", mutType.Fields.ElementAt(0).Args.ElementAt(2).Name); 59 | Assert.Equal("Detail", mutType.Fields.ElementAt(0).Args.ElementAt(2).TypeName); 60 | Assert.True(mutType.Fields.ElementAt(0).Args.ElementAt(2).IsArray); 61 | Assert.Equal("released", mutType.Fields.ElementAt(0).Args.ElementAt(4).Name); 62 | Assert.Equal("Date", mutType.Fields.ElementAt(0).Args.ElementAt(4).TypeName); 63 | Assert.False(mutType.Fields.ElementAt(0).Args.ElementAt(4).IsArray); 64 | } 65 | 66 | [Fact] 67 | public void TestSchemaTypeDef() 68 | { 69 | var results = SchemaCompiler.Compile(File.ReadAllText("../../../schema.graphql")); 70 | Assert.Equal(2, results.Schema.Count); 71 | Assert.Equal(8, results.Types.Count); 72 | Assert.Single(results.Inputs); 73 | var queryTypeName = results.Schema.First(s => s.Name == "query").TypeName; 74 | var mutationTypeName = results.Schema.First(s => s.Name == "mutation").TypeName; 75 | 76 | var typeDef = results.Types["Movie"]; 77 | Assert.Equal("This is a movie entity", typeDef.Description); 78 | Assert.Equal(9, typeDef.Fields.Count); 79 | Assert.Equal("id", typeDef.Fields.ElementAt(0).Name); 80 | Assert.Equal("String", typeDef.Fields.ElementAt(1).TypeName); 81 | Assert.False(typeDef.Fields.ElementAt(1).IsArray); 82 | Assert.Equal("actors", typeDef.Fields.ElementAt(4).Name); 83 | Assert.Equal("Person", typeDef.Fields.ElementAt(4).TypeName); 84 | Assert.True(typeDef.Fields.ElementAt(4).IsArray); 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /src/tests/DotNetGraphQLQueryGen.Tests/TestMakeQuery.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using DotNetGqlClient; 5 | using Generated; 6 | using Xunit; 7 | using Xunit.Abstractions; 8 | 9 | namespace CoreData.Model.Tests 10 | { 11 | public class TestClient : BaseGraphQLClient 12 | { 13 | internal string MakeQuery(Expression> p, bool mutation = false) 14 | { 15 | return base.MakeQuery(p, mutation); 16 | } 17 | internal string MakeMutation(Expression> p, bool mutation = false) 18 | { 19 | return base.MakeQuery(p, mutation); 20 | } 21 | } 22 | 23 | public class TestTypedQuery 24 | { 25 | public TestTypedQuery(ITestOutputHelper testOutputHelper) 26 | { 27 | } 28 | 29 | [Fact] 30 | public void SimpleArgs() 31 | { 32 | var client = new TestClient(); 33 | var query = client.MakeQuery(q => new { 34 | Movies = q.Movies(s => new { 35 | s.Id, 36 | }), 37 | }); 38 | Assert.Equal($@"query BaseGraphQLClient {{ 39 | Movies: movies {{ 40 | Id: id 41 | }} 42 | }}".Replace("\r\n", "\n"), query); 43 | } 44 | 45 | [Fact] 46 | public void SimpleQuery() 47 | { 48 | var client = new TestClient(); 49 | var query = client.MakeQuery(q => new { 50 | Actors = q.Actors(s => new { 51 | s.Id, 52 | DirectorOf = s.DirectorOf(), 53 | }), 54 | }); 55 | Assert.Equal($@"query BaseGraphQLClient {{ 56 | Actors: actors {{ 57 | Id: id 58 | DirectorOf: directorOf {{ 59 | Id: id 60 | Name: name 61 | Genre: genre 62 | Released: released 63 | DirectorId: directorId 64 | Rating: rating 65 | }} 66 | }} 67 | }}".Replace("\r\n", "\n"), query); 68 | } 69 | 70 | [Fact] 71 | public void TypedClass() 72 | { 73 | var client = new TestClient(); 74 | var query = client.MakeQuery(q => new MyResult 75 | { 76 | Movies = q.Movies(s => new MovieResult 77 | { 78 | Id = s.Id, 79 | }), 80 | }); 81 | Assert.Equal($@"query BaseGraphQLClient {{ 82 | Movies: movies {{ 83 | Id: id 84 | }} 85 | }}".Replace("\r\n", "\n"), query); 86 | } 87 | 88 | [Fact] 89 | public void TestMutationWithDate() 90 | { 91 | var client = new TestClient(); 92 | var query = client.MakeMutation(q => new 93 | { 94 | Movie = q.AddMovie("movie", 5.5, null, null, new DateTime(2019, 10, 30, 17, 55, 23), s => new 95 | { 96 | s.Id, 97 | }), 98 | }, true); 99 | Assert.Equal($@"mutation BaseGraphQLClient {{ 100 | Movie: addMovie(name: ""movie"", rating: 5.5, released: ""2019-10-30T17:55:23.0000000"") {{ 101 | Id: id 102 | }} 103 | }}".Replace("\r\n", "\n"), query); 104 | } 105 | 106 | [Fact] 107 | public void TestErrorOnInvalidPropertySelection() 108 | { 109 | Assert.Throws(() => { 110 | var client = new TestClient(); 111 | var query = client.MakeQuery(q => new 112 | { 113 | Movie = q.Movies(s => new 114 | { 115 | // we generate gql we don't select the .Value the value will be serialised in the object we're creating 116 | s.Rating.Value, 117 | }), 118 | }); 119 | }); 120 | } 121 | [Fact] 122 | public void TestErrorOnInvalidPropertySelection2() 123 | { 124 | Assert.Throws(() => { 125 | var client = new TestClient(); 126 | var query = client.MakeQuery(q => new 127 | { 128 | Movie = q.Movies(s => new 129 | { 130 | // we generate gql we don't select the .Value the value will be serialised in the object we're creating 131 | s.Director().Died 132 | }), 133 | }); 134 | }); 135 | } 136 | } 137 | 138 | public class MovieResult 139 | { 140 | public int Id { get; set; } 141 | } 142 | 143 | public class MyResult 144 | { 145 | public List Movies { get; set; } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/dotnet-gqlgen/SchemaVisitor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using GraphQLSchema.Grammer; 4 | 5 | namespace dotnet_gqlgen 6 | { 7 | internal class SchemaVisitor : GraphQLSchemaBaseVisitor 8 | { 9 | private readonly SchemaInfo schemaInfo; 10 | private List addFieldsTo; 11 | 12 | public SchemaInfo SchemaInfo => schemaInfo; 13 | 14 | public SchemaVisitor(Dictionary typeMappings) 15 | { 16 | this.schemaInfo = new SchemaInfo(typeMappings); 17 | } 18 | 19 | public override object VisitFieldDef(GraphQLSchemaParser.FieldDefContext context) 20 | { 21 | var result = base.VisitFieldDef(context); 22 | var docComment = context.comment().LastOrDefault(); 23 | var desc = docComment != null ? (string)VisitComment(docComment) : null; 24 | var name = context.name.Text; 25 | var args = (List)VisitArguments(context.args); 26 | var type = context.type.type?.Text; 27 | var arrayType = context.type.arrayType?.Text; 28 | addFieldsTo.Add(new Field(this.schemaInfo) 29 | { 30 | Name = name, 31 | TypeName = arrayType ?? type, 32 | IsArray = context.type.arrayType != null, 33 | Args = args, 34 | Description = desc, 35 | IsNonNullable = (context.type.elementTypeRequired ?? context.type.required) != null 36 | }); 37 | return result; 38 | } 39 | 40 | public override object VisitArguments(GraphQLSchemaParser.ArgumentsContext context) 41 | { 42 | var args = new List(); 43 | if (context != null) 44 | { 45 | foreach (var arg in context.argument()) 46 | { 47 | var type = arg.dataType().type?.Text; 48 | var arrayType = arg.dataType().arrayType?.Text; 49 | args.Add(new Arg(this.schemaInfo) 50 | { 51 | Name = arg.NAME().GetText(), 52 | TypeName = arrayType ?? type, 53 | Required = (arg.dataType().arrayRequired ?? arg.dataType().required) != null, 54 | IsArray = arrayType != null 55 | }); 56 | } 57 | } 58 | return args; 59 | } 60 | 61 | internal void SetFieldConsumer(List item) 62 | { 63 | this.addFieldsTo = item; 64 | } 65 | 66 | public override object VisitComment(GraphQLSchemaParser.CommentContext context) 67 | { 68 | return context.GetText().Trim('"', ' ', '\t', '\n', '\r'); 69 | } 70 | 71 | public override object VisitSchemaDef(GraphQLSchemaParser.SchemaDefContext context) 72 | { 73 | using (new FieldConsumer(this, schemaInfo.Schema)) 74 | { 75 | return base.VisitSchemaDef(context); 76 | } 77 | } 78 | public override object VisitEnumDef(GraphQLSchemaParser.EnumDefContext context) 79 | { 80 | var docComment = context.comment().LastOrDefault(); 81 | var desc = docComment != null ? (string)VisitComment(docComment) : null; 82 | 83 | var fields = new List(); 84 | using (new FieldConsumer(this, fields)) 85 | { 86 | var result = base.VisitEnumDef(context); 87 | schemaInfo.Enums.Add(context.typeName.Text, fields.Select(f => f.Name).ToList()); 88 | return result; 89 | } 90 | } 91 | public override object VisitEnumItem(GraphQLSchemaParser.EnumItemContext context) 92 | { 93 | this.addFieldsTo.Add(new Field(this.schemaInfo) { 94 | Name = context.name.Text, 95 | Args = new List(), 96 | }); 97 | return base.VisitEnumItem(context); 98 | } 99 | public override object VisitInputDef(GraphQLSchemaParser.InputDefContext context) 100 | { 101 | var docComment = context.comment().LastOrDefault(); 102 | var desc = docComment != null ? (string)VisitComment(docComment) : null; 103 | 104 | var fields = new List(); 105 | using (new FieldConsumer(this, fields)) 106 | { 107 | var result = base.Visit(context.objectDef()); 108 | schemaInfo.Inputs.Add(context.typeName.Text, new TypeInfo(fields, context.typeName.Text, desc, isInput:true)); 109 | return result; 110 | } 111 | } 112 | public override object VisitTypeDef(GraphQLSchemaParser.TypeDefContext context) 113 | { 114 | var docComment = context.comment().LastOrDefault(); 115 | var desc = docComment != null ? (string)VisitComment(docComment) : null; 116 | 117 | var fields = new List(); 118 | using (new FieldConsumer(this, fields)) 119 | { 120 | var result = base.Visit(context.objectDef()); 121 | schemaInfo.Types.Add(context.typeName.Text, new TypeInfo(fields, context.typeName.Text, desc)); 122 | return result; 123 | } 124 | } 125 | public override object VisitScalarDef(GraphQLSchemaParser.ScalarDefContext context) 126 | { 127 | var result = base.VisitScalarDef(context); 128 | schemaInfo.Scalars.Add(context.typeName.Text); 129 | return result; 130 | } 131 | } 132 | } -------------------------------------------------------------------------------- /src/dotnet-gqlgen/SchemaInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Text; 4 | 5 | namespace dotnet_gqlgen 6 | { 7 | public class SchemaInfo 8 | { 9 | public Dictionary typeMappings = new Dictionary { 10 | {"String", "string"}, 11 | {"ID", "string"}, 12 | {"Int", "int?"}, 13 | {"Float", "double?"}, 14 | {"Boolean", "bool?"}, 15 | {"String!", "string"}, 16 | {"ID!", "string"}, 17 | {"Int!", "int"}, 18 | {"Float!", "double"}, 19 | {"Boolean!", "bool"}, 20 | }; 21 | 22 | public SchemaInfo(Dictionary typeMappings) 23 | { 24 | if (typeMappings != null) 25 | { 26 | foreach (var item in typeMappings) 27 | { 28 | // overrides 29 | this.typeMappings[item.Key] = item.Value; 30 | } 31 | } 32 | Schema = new List(); 33 | Types = new Dictionary(); 34 | Inputs = new Dictionary(); 35 | Enums = new Dictionary>(); 36 | Scalars = new List(); 37 | } 38 | 39 | public List Schema { get; } 40 | /// 41 | /// Return the query type info. 42 | /// 43 | public TypeInfo Query => Types[Schema.First(f => f.Name == "query").TypeName]; 44 | 45 | /// 46 | /// Return the mutation type info. 47 | /// 48 | public TypeInfo Mutation 49 | { 50 | get 51 | { 52 | var typeName = Schema.First(f => f.Name == "mutation")?.TypeName; 53 | if (typeName != null) 54 | return Types[typeName]; 55 | return null; 56 | } 57 | } 58 | public Dictionary Types { get; } 59 | public Dictionary Inputs { get; } 60 | public Dictionary> Enums { get; set; } 61 | public List Scalars { get; } 62 | 63 | internal bool HasDotNetType(string typeName) 64 | { 65 | return typeMappings.ContainsKey(typeName) || Types.ContainsKey(typeName) || Inputs.ContainsKey(typeName) || Enums.ContainsKey(typeName); 66 | } 67 | 68 | internal string GetDotNetType(string typeName) 69 | { 70 | if (typeMappings.ContainsKey(typeName)) 71 | return typeMappings[typeName]; 72 | if (Types.ContainsKey(typeName)) 73 | return Types[typeName].Name; 74 | if (Enums.ContainsKey(typeName)) 75 | return typeName; 76 | return Inputs[typeName].Name; 77 | } 78 | internal bool IsEnum(string typeName) 79 | { 80 | return Enums.ContainsKey(typeName); 81 | } 82 | } 83 | 84 | public class TypeInfo 85 | { 86 | public TypeInfo(IEnumerable fields, string name, string description, bool isInput = false) 87 | { 88 | Fields = fields.ToList(); 89 | Name = name; 90 | Description = description; 91 | IsInput = isInput; 92 | } 93 | 94 | public List Fields { get; } 95 | public string Name { get; } 96 | public string Description { get; } 97 | public bool IsInput { get; } 98 | } 99 | 100 | public class Field 101 | { 102 | private readonly SchemaInfo schemaInfo; 103 | 104 | public Field(SchemaInfo schemaInfo) 105 | { 106 | Args = new List(); 107 | this.schemaInfo = schemaInfo; 108 | } 109 | public string Name { get; set; } 110 | public string TypeName { get; set; } 111 | public bool IsArray { get; set; } 112 | public bool IsNonNullable { get; set; } 113 | public List Args { get; set; } 114 | public string Description { get; set; } 115 | 116 | public string DotNetName => Name[0].ToString().ToUpper() + string.Join("", Name.Skip(1)); 117 | public string DotNetType 118 | { 119 | get 120 | { 121 | var t = DotNetTypeSingle; 122 | if (IsNonNullable) 123 | t = t.Trim('?'); 124 | else if (!t.EndsWith('?') && schemaInfo.IsEnum(t)) 125 | t = t + "?"; 126 | return IsArray ? $"List<{t}>" : t; 127 | } 128 | } 129 | public string DotNetTypeSingle 130 | { 131 | get 132 | { 133 | if (!schemaInfo.HasDotNetType(TypeName)) 134 | { 135 | throw new SchemaException($"Unknown dotnet type for schema type '{TypeName}'. Please provide a mapping for any custom scalar types defined in the schema"); 136 | } 137 | return schemaInfo.GetDotNetType(TypeName); 138 | } 139 | } 140 | 141 | public bool ShouldBeProperty 142 | { 143 | get 144 | { 145 | return (Args.Count == 0 && !schemaInfo.Types.ContainsKey(TypeName) && !schemaInfo.Inputs.ContainsKey(TypeName)) || schemaInfo.Scalars.Contains(TypeName); 146 | } 147 | } 148 | 149 | public string OutputMethodSig() 150 | { 151 | var sb = new StringBuilder(" "); 152 | sb.Append(IsArray ? "List " : "TReturn "); 153 | sb.Append(DotNetName).Append("("); 154 | sb.Append(ArgsOutput()); 155 | if (Args.Count > 0) 156 | sb.Append(", "); 157 | sb.AppendLine($"Expression> selection);"); 158 | 159 | return sb.ToString(); 160 | } 161 | 162 | public string ArgsOutput() 163 | { 164 | if (!Args.Any()) 165 | return ""; 166 | var result = string.Join(", ", Args.Select(a => $"{(a.Required ? a.DotNetType.Trim('?') : a.DotNetType)} {a.Name}")); 167 | return result; 168 | } 169 | 170 | public override string ToString() 171 | { 172 | return $"{Name}:{(IsArray ? '[' + TypeName + ']': TypeName)}"; 173 | } 174 | } 175 | 176 | public class Arg : Field 177 | { 178 | public Arg(SchemaInfo schemaInfo) : base(schemaInfo) 179 | { 180 | } 181 | 182 | public bool Required { get; set; } 183 | } 184 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dotnet GraphQL Query generator 2 | 3 | Given a GraphQL schema file, this tool will generate interfaces and classes to enable strongly typed querying from C# to a GraphQL API. 4 | 5 | Example, given the following GraphQL schema 6 | ``` 7 | schema { 8 | query: Query 9 | mutation: Mutation 10 | } 11 | 12 | scalar Date 13 | 14 | type Query { 15 | ] actors: [Person] 16 | directors: [Person] 17 | movie(id: Int!): Movie 18 | movies: [Movie] 19 | } 20 | 21 | type Movie { 22 | id: Int 23 | name: String 24 | genre: Int 25 | released: Date 26 | actors: [Person] 27 | writers: [Person] 28 | director: Person 29 | rating: Float 30 | } 31 | 32 | type Person { 33 | id: Int 34 | dob: Date 35 | actorIn: [Movie] 36 | writerOf: [Movie] 37 | directorOf: [Movie] 38 | died: Date 39 | age: Int 40 | name: String 41 | } 42 | 43 | type Mutation { 44 | addPerson(dob: Date!, name: String!): Person 45 | } 46 | ``` 47 | 48 | Running `dotnet run -- schema.graphql -m Date=DateTime` will generate the following 49 | 50 | ```c# 51 | public interface RootQuery 52 | { 53 | [GqlFieldName("actors")] 54 | List Actors(); 55 | 56 | [GqlFieldName("actors")] 57 | List Actors(Expression> selection); 58 | 59 | [GqlFieldName("directors")] 60 | List Directors(); 61 | 62 | [GqlFieldName("directors")] 63 | List Directors(Expression> selection); 64 | 65 | [GqlFieldName("movie")] 66 | Movie Movie(); 67 | 68 | [GqlFieldName("movie")] 69 | TReturn Movie(int id, Expression> selection); 70 | 71 | [GqlFieldName("movies")] 72 | List Movies(); 73 | 74 | [GqlFieldName("movies")] 75 | List Movies(Expression> selection); 76 | } 77 | 78 | public interface Movie 79 | { 80 | [GqlFieldName("id")] 81 | int Id { get; } 82 | [GqlFieldName("name")] 83 | string Name { get; } 84 | [GqlFieldName("genre")] 85 | int Genre { get; } 86 | [GqlFieldName("released")] 87 | DateTime Released { get; } 88 | [GqlFieldName("actors")] 89 | List Actors(); 90 | [GqlFieldName("actors")] 91 | List Actors(Expression> selection); 92 | [GqlFieldName("writers")] 93 | List Writers(); 94 | [GqlFieldName("writers")] 95 | List Writers(Expression> selection); 96 | [GqlFieldName("director")] 97 | Person Director(); 98 | [GqlFieldName("director")] 99 | TReturn Director(Expression> selection); 100 | [GqlFieldName("rating")] 101 | double Rating { get; } 102 | } 103 | 104 | public interface Person 105 | { 106 | [GqlFieldName("id")] 107 | int Id { get; } 108 | [GqlFieldName("dob")] 109 | DateTime Dob { get; } 110 | [GqlFieldName("actorIn")] 111 | List ActorIn(); 112 | [GqlFieldName("actorIn")] 113 | List ActorIn(Expression> selection); 114 | [GqlFieldName("writerOf")] 115 | List WriterOf(); 116 | [GqlFieldName("writerOf")] 117 | List WriterOf(Expression> selection); 118 | [GqlFieldName("directorOf")] 119 | List DirectorOf(); 120 | [GqlFieldName("directorOf")] 121 | List DirectorOf(Expression> selection); 122 | [GqlFieldName("died")] 123 | DateTime Died { get; } 124 | [GqlFieldName("age")] 125 | int Age { get; } 126 | [GqlFieldName("name")] 127 | string Name { get; } 128 | } 129 | 130 | public interface Mutation 131 | { 132 | [GqlFieldName("addPerson")] 133 | TReturn AddPerson(DateTime dob, string name, Expression> selection); 134 | } 135 | ``` 136 | 137 | It also generates a `GraphQLClient` class that will work for an unauthenticated API. You can modify that class to implement the authentication you may need. `GraphQLClient` exposes 2 methods, `async Task> QueryAsync(Expression> query)` for queries and `async Task> MutateAsync(Expression> query)` for mutations. 138 | 139 | Example usage 140 | 141 | ```c# 142 | var client = new GraphQLClient(); 143 | var result = await client.QueryAsync(q => new { 144 | Movie = q.Movie(2, m => new { 145 | m.Id, 146 | m.Name, 147 | ReleaseDate = m.Released, 148 | Director = m.Director(d => new { 149 | d.Name 150 | }) 151 | }), 152 | Actors = q.Actors(a => new { 153 | a.Name, 154 | a.Dob 155 | }) 156 | }); 157 | ``` 158 | 159 | This looks similar to the GraphQL. And now `result.Data` is a strongly type object with the expected types. 160 | 161 | If a field in the schema is an object (can have a selection protected on to it) it will be exposed in the generated code as a method where you pass in any field arguments first and then the selection. 162 | 163 | The GraphQL created by the above will look like this 164 | ``` 165 | { 166 | query { 167 | Movie: movie(id: 2) { 168 | Id: id 169 | Name: name 170 | ReleaseDate: released 171 | Director: director { 172 | Name: name 173 | } 174 | } 175 | Actors: actors { 176 | Name: name 177 | Dob: dob 178 | } 179 | } 180 | } 181 | ``` 182 | 183 | ## Mutations 184 | 185 | Mutations look very similar to the above query, just as they do in GraphQL. The above schema had one mutation that can be called like so 186 | 187 | ```c# 188 | // mutations 189 | var mutationResult = await client.MutateAsync(m => new { 190 | NewPerson = m.AddPerson(new DateTime(1801, 7, 3), "John Johny", p => new { p.Id }) 191 | }); 192 | ``` 193 | 194 | `m` is the `Mutation` type interface and lets you call any of the mutations generated from the schema. Like a query, you create a new anaoymous object and call as many mutations as you wish. Each mutation method has all their arguments and the last one is a selection query on the mutation's return type. 195 | 196 | The above has `mutationResult` strongly typed. E.g. `mutationResult.Data.NewPerson` will only have an `Id` field. 197 | 198 | ## Input types 199 | 200 | Any input types are generated as actual `class`es as the are used as arguments. The `-m Date=DateTime` tells the tool that the `scalar` type `Date` should be the `DateTime` type in C#. You can use this to support any other custom `scalar` types. E.g. `-m Date=DateTime,Point=PointF`, just comma seperate a `GqlScalar=DotnetType` list. 201 | -------------------------------------------------------------------------------- /src/dotnet-gqlgen/BaseGraphQLClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Reflection; 6 | using System.Text; 7 | 8 | namespace DotNetGqlClient 9 | { 10 | public abstract class BaseGraphQLClient 11 | { 12 | protected string MakeQuery(Expression> query, bool mutation = false) 13 | { 14 | var gql = new StringBuilder(); 15 | gql.AppendLine($"{(mutation ? "mutation" : "query")} BaseGraphQLClient {{"); 16 | 17 | if (query.NodeType != ExpressionType.Lambda) 18 | throw new ArgumentException($"Must provide a LambdaExpression", "query"); 19 | var lambda = (LambdaExpression)query; 20 | 21 | if (lambda.Body.NodeType != ExpressionType.New && lambda.Body.NodeType != ExpressionType.MemberInit) 22 | throw new ArgumentException($"LambdaExpression must return a NewExpression or MemberInitExpression"); 23 | 24 | GetObjectSelection(gql, lambda.Body); 25 | 26 | gql.Append(@"}"); 27 | return gql.ToString(); 28 | } 29 | 30 | private static void GetObjectSelection(StringBuilder gql, Expression exp) 31 | { 32 | if (exp.NodeType == ExpressionType.New) 33 | { 34 | var newExp = (NewExpression)exp; 35 | for (int i = 0; i < newExp.Arguments.Count; i++) 36 | { 37 | var fieldVal = newExp.Arguments[i]; 38 | var fieldProp = newExp.Members[i]; 39 | gql.AppendLine($"{fieldProp.Name}: {GetFieldSelection(fieldVal)}"); 40 | } 41 | } 42 | else 43 | { 44 | var mi = (MemberInitExpression)exp; 45 | for (int i = 0; i < mi.Bindings.Count; i++) 46 | { 47 | var valExp = ((MemberAssignment)mi.Bindings[i]).Expression; 48 | var fieldVal = mi.Bindings[i].Member; 49 | gql.AppendLine($"{fieldVal.Name}: {GetFieldSelection(valExp)}"); 50 | } 51 | } 52 | } 53 | 54 | private static string GetFieldSelection(Expression field) 55 | { 56 | if (field.NodeType == ExpressionType.MemberAccess) 57 | { 58 | var memberExp = (MemberExpression)field; 59 | // we only support 1 level field selection as we are just generating gql not doing post processing 60 | // e.g. client.MakeQuery(q => new 61 | // { 62 | // Movie = q.Movies(s => new 63 | // { 64 | // s.Rating.Value, 65 | // s.Director().Died 66 | // }), 67 | // }); 68 | // both of those selections are invalid. You just selection s.Rating and the return value type is float? 69 | // and for the director died date you select it like gql s.Director(d => new { d.Died }) 70 | // TODO we could generate s.Director().Died into the line above 71 | if (memberExp.Expression.NodeType != ExpressionType.Parameter) 72 | throw new ArgumentException("It looks like you are make a deep property call. We only support a single depth to generate GQL. You can use the methods to select nest objects"); 73 | var member = memberExp.Member; 74 | var attribute = member.GetCustomAttributes(typeof(GqlFieldNameAttribute)).Cast().FirstOrDefault(); 75 | if (attribute != null) 76 | return attribute.Name; 77 | return member.Name; 78 | } 79 | else if (field.NodeType == ExpressionType.Call) 80 | { 81 | var call = (MethodCallExpression)field; 82 | return GetSelectionFromMethod(call); 83 | } 84 | else if (field.NodeType == ExpressionType.Quote) 85 | { 86 | return GetFieldSelection(((UnaryExpression)field).Operand); 87 | } 88 | else 89 | { 90 | throw new ArgumentException($"Field expression should be a call or member access expression", "field"); 91 | } 92 | } 93 | 94 | private static string GetSelectionFromMethod(MethodCallExpression call) 95 | { 96 | var select = new StringBuilder(); 97 | 98 | var attribute = call.Method.GetCustomAttributes(typeof(GqlFieldNameAttribute)).Cast().FirstOrDefault(); 99 | if (attribute != null) 100 | select.Append(attribute.Name); 101 | else 102 | select.Append(call.Method.Name); 103 | 104 | if (call.Arguments.Count > 1) 105 | { 106 | var argVals = new List(); 107 | for (int i = 0; i < call.Arguments.Count - 1; i++) 108 | { 109 | var arg = call.Arguments.ElementAt(i); 110 | var param = call.Method.GetParameters().ElementAt(i); 111 | Type argType = null; 112 | object argVal = null; 113 | if (arg.NodeType == ExpressionType.Convert) 114 | { 115 | arg = ((UnaryExpression)arg).Operand; 116 | } 117 | 118 | if (arg.NodeType == ExpressionType.Constant) 119 | { 120 | var constArg = (ConstantExpression)arg; 121 | argType = constArg.Type; 122 | argVal = constArg.Value; 123 | } 124 | else if (arg.NodeType == ExpressionType.MemberAccess) 125 | { 126 | var ma = (MemberExpression)arg; 127 | var ce = (ConstantExpression)ma.Expression; 128 | argType = ma.Type; 129 | if (ma.Member.MemberType == MemberTypes.Field) 130 | argVal = ((FieldInfo)ma.Member).GetValue(ce.Value); 131 | else 132 | argVal = ((PropertyInfo)ma.Member).GetValue(ce.Value); 133 | } 134 | else if (arg.NodeType == ExpressionType.New) 135 | { 136 | argVal = Expression.Lambda(arg).Compile().DynamicInvoke(); 137 | argType = argVal.GetType(); 138 | } 139 | 140 | else 141 | { 142 | throw new Exception($"Unsupported argument type {arg.NodeType}"); 143 | } 144 | if (argVal == null) 145 | continue; 146 | if (argType == typeof(string) || argType == typeof(Guid) || argType == typeof(Guid?)) 147 | { 148 | argVals.Add($"{param.Name}: \"{argVal}\""); 149 | } 150 | else if (argType == typeof(DateTime) || argType == typeof(DateTime?)) 151 | { 152 | argVals.Add($"{param.Name}: \"{((DateTime)argVal).ToString("o")}\""); 153 | } 154 | else 155 | { 156 | argVals.Add($"{param.Name}: {argVal}"); 157 | } 158 | }; 159 | if (argVals.Any()) 160 | select.Append($"({string.Join(", ", argVals)})"); 161 | } 162 | select.Append(" {\n"); 163 | if (call.Arguments.Count == 0) 164 | { 165 | if (call.Method.ReturnType.GetInterfaces().Select(i => i.GetTypeInfo().GetGenericTypeDefinition()).Contains(typeof(IEnumerable<>))) 166 | { 167 | select.Append(GetDefaultSelection(call.Method.ReturnType.GetGenericArguments().First())); 168 | } 169 | else 170 | { 171 | select.Append(GetDefaultSelection(call.Method.ReturnType)); 172 | } 173 | } 174 | else 175 | { 176 | var exp = call.Arguments.Last(); 177 | if (exp.NodeType == ExpressionType.Quote) 178 | exp = ((UnaryExpression)exp).Operand; 179 | if (exp.NodeType == ExpressionType.Lambda) 180 | exp = ((LambdaExpression)exp).Body; 181 | GetObjectSelection(select, exp); 182 | } 183 | select.Append("}"); 184 | return select.ToString(); 185 | } 186 | 187 | private static string GetDefaultSelection(Type returnType) 188 | { 189 | var select = new StringBuilder(); 190 | foreach (var field in returnType.GetProperties()) 191 | { 192 | var name = field.Name; 193 | var attribute = field.GetCustomAttributes(typeof(GqlFieldNameAttribute)).Cast().FirstOrDefault(); 194 | if (attribute != null) 195 | name = attribute.Name; 196 | 197 | select.AppendLine($"{field.Name}: {name}"); 198 | } 199 | return select.ToString(); 200 | } 201 | } 202 | } -------------------------------------------------------------------------------- /src/tests/DotNetGraphQLQueryGen.Tests/GeneratedTypes.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq.Expressions; 5 | using DotNetGqlClient; 6 | 7 | /// 8 | /// Generated interfaces for making GraphQL API calls with a typed interface. 9 | /// 10 | /// Generated on 25/3/20 8:44:43 pm from ../tests/DotNetGraphQLQueryGen.Tests/schema.graphql with arguments -n Generated -c TestClient -m Date=DateTime 11 | /// 12 | 13 | namespace Generated 14 | { 15 | 16 | 17 | public interface RootQuery 18 | { 19 | /// 20 | /// Pagination [defaults: page , pagesize ] 21 | /// 22 | /// This shortcut will return a selection of all fields 23 | /// 24 | [GqlFieldName("actorPager")] 25 | PersonPagination ActorPager(); 26 | /// 27 | /// Pagination [defaults: page , pagesize ] 28 | /// 29 | /// 30 | /// Projection of fields to select from the object 31 | [GqlFieldName("actorPager")] 32 | TReturn ActorPager(int? page, int? pagesize, string search, Expression> selection); 33 | /// 34 | /// List of actors 35 | /// 36 | /// This shortcut will return a selection of all fields 37 | /// 38 | [GqlFieldName("actors")] 39 | List Actors(); 40 | /// 41 | /// List of actors 42 | /// 43 | /// 44 | /// Projection of fields to select from the object 45 | [GqlFieldName("actors")] 46 | List Actors(Expression> selection); 47 | /// 48 | /// List of directors 49 | /// 50 | /// This shortcut will return a selection of all fields 51 | /// 52 | [GqlFieldName("directors")] 53 | List Directors(); 54 | /// 55 | /// List of directors 56 | /// 57 | /// 58 | /// Projection of fields to select from the object 59 | [GqlFieldName("directors")] 60 | List Directors(Expression> selection); 61 | /// 62 | /// Return a Movie by its Id 63 | /// 64 | /// This shortcut will return a selection of all fields 65 | /// 66 | [GqlFieldName("movie")] 67 | Movie Movie(); 68 | /// 69 | /// Return a Movie by its Id 70 | /// 71 | /// 72 | /// Projection of fields to select from the object 73 | [GqlFieldName("movie")] 74 | TReturn Movie(int id, Expression> selection); 75 | /// 76 | /// Collection of Movies 77 | /// 78 | /// This shortcut will return a selection of all fields 79 | /// 80 | [GqlFieldName("movies")] 81 | List Movies(); 82 | /// 83 | /// Collection of Movies 84 | /// 85 | /// 86 | /// Projection of fields to select from the object 87 | [GqlFieldName("movies")] 88 | List Movies(Expression> selection); 89 | /// 90 | /// Collection of Peoples 91 | /// 92 | /// This shortcut will return a selection of all fields 93 | /// 94 | [GqlFieldName("people")] 95 | List People(); 96 | /// 97 | /// Collection of Peoples 98 | /// 99 | /// 100 | /// Projection of fields to select from the object 101 | [GqlFieldName("people")] 102 | List People(Expression> selection); 103 | /// 104 | /// Return a Person by its Id 105 | /// 106 | /// This shortcut will return a selection of all fields 107 | /// 108 | [GqlFieldName("person")] 109 | Person Person(); 110 | /// 111 | /// Return a Person by its Id 112 | /// 113 | /// 114 | /// Projection of fields to select from the object 115 | [GqlFieldName("person")] 116 | TReturn Person(int id, Expression> selection); 117 | /// 118 | /// List of writers 119 | /// 120 | /// This shortcut will return a selection of all fields 121 | /// 122 | [GqlFieldName("writers")] 123 | List Writers(); 124 | /// 125 | /// List of writers 126 | /// 127 | /// 128 | /// Projection of fields to select from the object 129 | [GqlFieldName("writers")] 130 | List Writers(Expression> selection); 131 | } 132 | public interface SubscriptionType 133 | { 134 | [GqlFieldName("name")] 135 | string Name { get; } 136 | } 137 | /// 138 | /// This is a movie entity 139 | /// 140 | public interface Movie 141 | { 142 | [GqlFieldName("id")] 143 | int Id { get; } 144 | [GqlFieldName("name")] 145 | string Name { get; } 146 | /// 147 | /// Enum of Genre 148 | /// 149 | [GqlFieldName("genre")] 150 | int Genre { get; } 151 | [GqlFieldName("released")] 152 | DateTime Released { get; } 153 | /// 154 | /// Actors in the movie 155 | /// 156 | /// This shortcut will return a selection of all fields 157 | /// 158 | [GqlFieldName("actors")] 159 | List Actors(); 160 | /// 161 | /// Actors in the movie 162 | /// 163 | /// 164 | /// Projection of fields to select from the object 165 | [GqlFieldName("actors")] 166 | List Actors(Expression> selection); 167 | /// 168 | /// Writers in the movie 169 | /// 170 | /// This shortcut will return a selection of all fields 171 | /// 172 | [GqlFieldName("writers")] 173 | List Writers(); 174 | /// 175 | /// Writers in the movie 176 | /// 177 | /// 178 | /// Projection of fields to select from the object 179 | [GqlFieldName("writers")] 180 | List Writers(Expression> selection); 181 | /// 182 | /// This shortcut will return a selection of all fields 183 | /// 184 | [GqlFieldName("director")] 185 | Person Director(); 186 | /// 187 | /// 188 | /// Projection of fields to select from the object 189 | [GqlFieldName("director")] 190 | TReturn Director(Expression> selection); 191 | [GqlFieldName("directorId")] 192 | int DirectorId { get; } 193 | [GqlFieldName("rating")] 194 | double? Rating { get; } 195 | } 196 | public interface Actor 197 | { 198 | [GqlFieldName("personId")] 199 | int PersonId { get; } 200 | /// 201 | /// This shortcut will return a selection of all fields 202 | /// 203 | [GqlFieldName("person")] 204 | Person Person(); 205 | /// 206 | /// 207 | /// Projection of fields to select from the object 208 | [GqlFieldName("person")] 209 | TReturn Person(Expression> selection); 210 | [GqlFieldName("movieId")] 211 | int MovieId { get; } 212 | /// 213 | /// This shortcut will return a selection of all fields 214 | /// 215 | [GqlFieldName("movie")] 216 | Movie Movie(); 217 | /// 218 | /// 219 | /// Projection of fields to select from the object 220 | [GqlFieldName("movie")] 221 | TReturn Movie(Expression> selection); 222 | } 223 | public interface Person 224 | { 225 | [GqlFieldName("id")] 226 | int Id { get; } 227 | [GqlFieldName("firstName")] 228 | string FirstName { get; } 229 | [GqlFieldName("lastName")] 230 | string LastName { get; } 231 | [GqlFieldName("dob")] 232 | DateTime Dob { get; } 233 | /// 234 | /// Movies they acted in 235 | /// 236 | /// This shortcut will return a selection of all fields 237 | /// 238 | [GqlFieldName("actorIn")] 239 | List ActorIn(); 240 | /// 241 | /// Movies they acted in 242 | /// 243 | /// 244 | /// Projection of fields to select from the object 245 | [GqlFieldName("actorIn")] 246 | List ActorIn(Expression> selection); 247 | /// 248 | /// Movies they wrote 249 | /// 250 | /// This shortcut will return a selection of all fields 251 | /// 252 | [GqlFieldName("writerOf")] 253 | List WriterOf(); 254 | /// 255 | /// Movies they wrote 256 | /// 257 | /// 258 | /// Projection of fields to select from the object 259 | [GqlFieldName("writerOf")] 260 | List WriterOf(Expression> selection); 261 | /// 262 | /// This shortcut will return a selection of all fields 263 | /// 264 | [GqlFieldName("directorOf")] 265 | List DirectorOf(); 266 | /// 267 | /// 268 | /// Projection of fields to select from the object 269 | [GqlFieldName("directorOf")] 270 | List DirectorOf(Expression> selection); 271 | [GqlFieldName("died")] 272 | DateTime Died { get; } 273 | [GqlFieldName("isDeleted")] 274 | bool IsDeleted { get; } 275 | /// 276 | /// Show the persons age 277 | /// 278 | [GqlFieldName("age")] 279 | int Age { get; } 280 | /// 281 | /// Persons name 282 | /// 283 | [GqlFieldName("name")] 284 | string Name { get; } 285 | } 286 | public interface Writer 287 | { 288 | [GqlFieldName("personId")] 289 | int PersonId { get; } 290 | /// 291 | /// This shortcut will return a selection of all fields 292 | /// 293 | [GqlFieldName("person")] 294 | Person Person(); 295 | /// 296 | /// 297 | /// Projection of fields to select from the object 298 | [GqlFieldName("person")] 299 | TReturn Person(Expression> selection); 300 | [GqlFieldName("movieId")] 301 | int MovieId { get; } 302 | /// 303 | /// This shortcut will return a selection of all fields 304 | /// 305 | [GqlFieldName("movie")] 306 | Movie Movie(); 307 | /// 308 | /// 309 | /// Projection of fields to select from the object 310 | [GqlFieldName("movie")] 311 | TReturn Movie(Expression> selection); 312 | } 313 | public interface PersonPagination 314 | { 315 | /// 316 | /// total records to match search 317 | /// 318 | [GqlFieldName("total")] 319 | int Total { get; } 320 | /// 321 | /// total pages based on page size 322 | /// 323 | [GqlFieldName("pageCount")] 324 | int PageCount { get; } 325 | /// 326 | /// collection of people 327 | /// 328 | /// This shortcut will return a selection of all fields 329 | /// 330 | [GqlFieldName("people")] 331 | List People(); 332 | /// 333 | /// collection of people 334 | /// 335 | /// 336 | /// Projection of fields to select from the object 337 | [GqlFieldName("people")] 338 | List People(Expression> selection); 339 | } 340 | public interface Mutation 341 | { 342 | /// 343 | /// Add a new Movie object 344 | /// 345 | /// 346 | /// Projection of fields to select from the object 347 | [GqlFieldName("addMovie")] 348 | TReturn AddMovie(string name, double? rating, List details, int? genre, DateTime released, Expression> selection); 349 | /// 350 | /// 351 | /// Projection of fields to select from the object 352 | [GqlFieldName("addActor")] 353 | TReturn AddActor(string firstName, string lastName, int? movieId, Expression> selection); 354 | /// 355 | /// 356 | /// Projection of fields to select from the object 357 | [GqlFieldName("addActor2")] 358 | TReturn AddActor2(string firstName, string lastName, int? movieId, Expression> selection); 359 | } 360 | public class Detail 361 | { 362 | [GqlFieldName("description")] 363 | public string Description { get; set; } 364 | } 365 | 366 | } --------------------------------------------------------------------------------