├── .gitignore ├── src ├── dotnet-gqlgen │ ├── .vscode │ │ ├── tasks.json │ │ └── launch.json │ ├── GqlFieldNameAttribute.cs │ ├── SchemaException.cs │ ├── FieldConsumer.cs │ ├── dotnet-gqlgen.sln │ ├── queryTypes.cshtml │ ├── dotnet-gqlgen.csproj │ ├── resultTypes.cshtml │ ├── IntroSpectionQuery.cs │ ├── GraphQLSchema.g4 │ ├── SchemaCompiler.cs │ ├── CSharpKeywords.cs │ ├── client.cshtml │ ├── SchemaVisitor.cs │ ├── IntroSpectionCompiler.cs │ ├── Generator.cs │ ├── SchemaInfo.cs │ └── BaseGraphQLClient.cs ├── dotnet-gqlgen-console │ ├── dotnet-gqlgen-console.csproj │ └── Program.cs └── tests │ └── DotNetGraphQLQueryGen.Tests │ ├── DotNetGraphQLQueryGen.Tests.csproj │ ├── schema.graphql │ ├── Generated │ ├── TestHttpClient.cs │ └── GeneratedTypes.cs │ ├── VisitorTests.cs │ └── TestMakeQuery.cs ├── .github └── workflows │ └── dotnetcore.yml ├── .vscode ├── tasks.json └── launch.json ├── LICENSE ├── CHANGELOG.md ├── DotNetGraphQLQueryGen.sln └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | package-lock.json 3 | .idea/ 4 | obj/ 5 | .DS_Store 6 | .antlr/ 7 | 8 | # User-specific files 9 | *.suo 10 | *.user 11 | *.userosscache 12 | *.sln.docstates 13 | 14 | # Visual Studio 2015 cache/options directory 15 | .vs/ -------------------------------------------------------------------------------- /src/dotnet-gqlgen/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "shell", 8 | "args": ["build"], 9 | "problemMatcher": "$msCompile" 10 | }, 11 | ] 12 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /.github/workflows/dotnetcore.yml: -------------------------------------------------------------------------------- 1 | name: Build .NET 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Setup .NET 6.0.x 17 | uses: actions/setup-dotnet@v2 18 | with: 19 | dotnet-version: 6.0.x 20 | - name: Install dependencies 21 | run: dotnet restore 22 | - name: Build 23 | run: dotnet build --configuration Release --no-restore 24 | - name: Test 25 | run: dotnet test --no-restore --verbosity normal 26 | -------------------------------------------------------------------------------- /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/dotnet-gqlgen-console/dotnet-gqlgen-console.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | dotnet_gqlgen 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/dotnet-gqlgen/.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": "Launch ", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | "program": "${workspaceFolder}/bin/Debug/net6.0/dotnet-gqlgen.dll", 13 | "args": [], 14 | "env": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | }, 17 | "cwd": "${workspaceFolder}", 18 | "console": "internalConsole", 19 | "stopAtEntry": false 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /src/tests/DotNetGraphQLQueryGen.Tests/DotNetGraphQLQueryGen.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tests for dotnet-gqlgen 5 | net6.0 6 | true 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "command": "dotnet", 9 | "type": "shell", 10 | "args": [ 11 | "build", 12 | // Ask dotnet build to generate full paths for file names. 13 | "/property:GenerateFullPaths=true", 14 | // Do not generate summary otherwise it leads to duplicate errors in Problems panel 15 | "/consoleloggerparameters:NoSummary" 16 | ], 17 | "group": "build", 18 | "presentation": { 19 | "reveal": "silent" 20 | }, 21 | "problemMatcher": "$msCompile" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /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/dotnet-gqlgen.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.002.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-gqlgen", "dotnet-gqlgen.csproj", "{8D6CB732-6785-4581-A588-CF76354F2D77}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {8D6CB732-6785-4581-A588-CF76354F2D77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {8D6CB732-6785-4581-A588-CF76354F2D77}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {8D6CB732-6785-4581-A588-CF76354F2D77}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {8D6CB732-6785-4581-A588-CF76354F2D77}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {3D4244F7-367C-4AE9-A9BC-6995532C47A8} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /.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}/src/dotnet-gqlgen/bin/Debug/netcoreapp2.2/dotnet-gqlgen.dll", 13 | "args": [ 14 | "../tests/DotNetGraphQLQueryGen.Tests/schema.graphql", 15 | "-n", 16 | "Generated", 17 | "-c", 18 | "TestHttpClient", 19 | "-m", 20 | "Date=DateTime" 21 | ], 22 | "cwd": "${workspaceFolder}/src/dotnet-gqlgen", 23 | "console": "internalConsole", 24 | "stopAtEntry": false 25 | }, 26 | { 27 | "name": ".NET Core Attach", 28 | "type": "coreclr", 29 | "request": "attach", 30 | "processId": "${command:pickProcess}" 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /src/dotnet-gqlgen/queryTypes.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | DisableEncoding = true; 3 | } 4 | @model ModelType 5 | @using System.IO 6 | @using dotnet_gqlgen; 7 | 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq.Expressions; 11 | using DotNetGqlClient; 12 | @Model.Usings 13 | 14 | /// 15 | /// Generated classes used for making GraphQL API calls with a typed interface. 16 | @if (!Model.NoGeneratedTimestamp) 17 | { 18 | @:/// 19 | @:/// Generated on @DateTime.UtcNow from @Model.SchemaFile @Model.CmdArgs 20 | } 21 | /// 22 | 23 | namespace @Model.Namespace 24 | { 25 | 26 | @foreach(var gqlType in Model.Types.Values.OrderBy(t => t.QueryName, StringComparer.Ordinal)) 27 | { 28 | if (!string.IsNullOrEmpty(gqlType.Description)) 29 | { 30 | @:/// 31 | @gqlType.DescriptionForComment(4) 32 | @:/// 33 | } 34 | 35 | @:public abstract class @gqlType.QueryName : @gqlType.Name 36 | @:{ 37 | @foreach(var field in gqlType.Fields.OrderBy(f => f.Name, StringComparer.Ordinal)) 38 | { 39 | @if (!field.ShouldBeProperty && !gqlType.IsInput) 40 | { 41 | var shortcutOutput = field.OutputMethodSignature(true, false); 42 | 43 | @if (gqlType != Model.Mutation && shortcutOutput != null) 44 | { 45 | @field.OutputMethodSignature(true, false) 46 | } 47 | 48 | @field.OutputMethodSignature(false, true) 49 | } 50 | } 51 | @:} 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /src/dotnet-gqlgen/dotnet-gqlgen.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | dotnet_gqlgen 6 | true 7 | 0.7.5 8 | 9 | 10 | 11 | README.md 12 | LICENSE 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | $(DefaultItemExcludes);output\**\* 36 | 37 | 38 | 39 | 40 | MSBuild:Compile 41 | GraphQLSchema.Grammer 42 | False 43 | True 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.5.0 2 | - Use an operation name in the built request to help with logging 3 | - Add overrides for naming your operation in `QueryAysnc`/`MutateAsync` 4 | 5 | # 0.4.0 6 | - Fix issue with GQL field names that are a single character 7 | - Generate classes for the schema types so if you select the full type we can create the classes 8 | - Split the Query methods into separate generated class from the schema type classes 9 | - Breaking - when supplying `--scalar_mapping`/`-m` arg, types are now split by `;` 10 | 11 | # 0.3.0 12 | - Support generating from the GraphQL endpoint with schema introspection query or from a JSON saved result of the query 13 | - Better support for fields that return a scalar but have arguments - no selection argument generated 14 | 15 | # 0.2.0 16 | - Fixed issues with enum support 17 | - Better support for fields/mutations that take arrays as arguments 18 | - #4 Support schema with default values - note it doesn't do anything with them. This seems like the responibility of the server you're calling to implement those. 19 | - Fix #5 support schema with `extend type` types. The fields are added to the exisiting type 20 | - Fix #6 schema keywords can be used a ids (field names, argument names etc) 21 | - Fix #8 support optional commas in schema file 22 | - #7 correctly generate multi line doc strings 23 | - #7 correctly ignore `#` comments 24 | 25 | # 0.1.0 26 | - Inital version, given a GraphQL schema file we generate 27 | - C# interfaces to write strongly typed queries against 28 | - C# classes for any Input types so we can actually use the classes and initialise with values 29 | - Generate a sample client for users to start implemented their own auth (if required) 30 | - Use scalars from schema to make better choices on generated interfaces 31 | - Support using typed objects instead of anonymous only -------------------------------------------------------------------------------- /src/dotnet-gqlgen/resultTypes.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | DisableEncoding = true; 3 | } 4 | @model ModelType 5 | @using System.IO 6 | @using dotnet_gqlgen; 7 | 8 | using System; 9 | using System.Collections.Generic; 10 | using DotNetGqlClient; 11 | @Model.Usings 12 | 13 | /// 14 | /// Generated classes used for making GraphQL API calls with a typed interface. 15 | /// Also used as result classes with the shortcut select all field methods 16 | @if (!Model.NoGeneratedTimestamp) 17 | { 18 | @:/// 19 | @:/// Generated on @DateTime.UtcNow from @Model.SchemaFile @Model.CmdArgs 20 | } 21 | /// 22 | 23 | namespace @Model.Namespace 24 | { 25 | 26 | @foreach(var kvp in Model.Enums.OrderBy(e => e.Key, StringComparer.Ordinal)) 27 | { 28 | @:public enum @kvp.Key { 29 | @foreach(var field in kvp.Value.OrderBy(t => t.Name, StringComparer.Ordinal)) 30 | { 31 | @:[GqlFieldName("@field.Name")] 32 | @:@field.DotNetName, 33 | } 34 | @:} 35 | } 36 | 37 | @foreach(var gqlType in Model.Types.Values.OrderBy(t => t.Name, StringComparer.Ordinal)) 38 | { 39 | if (!string.IsNullOrEmpty(gqlType.Description)) 40 | { 41 | @:/// 42 | @gqlType.DescriptionForComment(4) 43 | @:/// 44 | } 45 | @* We make interfaces as classes for now *@ 46 | @:public class @gqlType.Name 47 | @:{ 48 | @foreach(var field in gqlType.Fields.OrderBy(f => f.Name, StringComparer.Ordinal)) 49 | { 50 | @if (field.ShouldBeProperty || gqlType.IsInput) 51 | { 52 | @if (!string.IsNullOrEmpty(field.Description)) 53 | { 54 | @:/// 55 | @field.DescriptionForComment() 56 | @:/// 57 | } 58 | @:[GqlFieldName("@field.Name")] 59 | @:public @field.DotNetType @field.DotNetName { get; set; } 60 | } 61 | } 62 | @:} 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /src/dotnet-gqlgen/IntroSpectionQuery.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace dotnet_gqlgen 6 | { 7 | public static class IntroSpectionQuery 8 | { 9 | public static string Query = @" 10 | query IntrospectionQuery { 11 | __schema { 12 | queryType { name } 13 | mutationType { name } 14 | subscriptionType { name } 15 | types { 16 | ...FullType 17 | } 18 | directives { 19 | name 20 | description 21 | locations 22 | args { 23 | ...InputValue 24 | } 25 | } 26 | } 27 | } 28 | 29 | fragment FullType on __Type { 30 | kind 31 | name 32 | description 33 | fields(includeDeprecated: true) { 34 | name 35 | description 36 | args { 37 | ...InputValue 38 | } 39 | type { 40 | ...TypeRef 41 | } 42 | isDeprecated 43 | deprecationReason 44 | } 45 | inputFields { 46 | ...InputValue 47 | } 48 | interfaces { 49 | ...TypeRef 50 | } 51 | enumValues(includeDeprecated: true) { 52 | name 53 | description 54 | isDeprecated 55 | deprecationReason 56 | } 57 | possibleTypes { 58 | ...TypeRef 59 | } 60 | } 61 | 62 | fragment InputValue on __InputValue { 63 | name 64 | description 65 | type { ...TypeRef } 66 | defaultValue 67 | } 68 | 69 | fragment TypeRef on __Type { 70 | kind 71 | name 72 | ofType { 73 | kind 74 | name 75 | ofType { 76 | kind 77 | name 78 | ofType { 79 | kind 80 | name 81 | ofType { 82 | kind 83 | name 84 | ofType { 85 | kind 86 | name 87 | ofType { 88 | kind 89 | name 90 | ofType { 91 | kind 92 | name 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } 99 | } 100 | } 101 | "; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/dotnet-gqlgen/GraphQLSchema.g4: -------------------------------------------------------------------------------- 1 | grammar GraphQLSchema; 2 | 3 | // this is a simple way to allow keywords as idents 4 | idKeyword : ENUM | INPUT | SCALAR | TYPE | EXTEND | SCHEMA | FALSE | TRUE | ID; 5 | 6 | TRUE : 'true'; 7 | FALSE : 'false'; 8 | EXTEND : 'extend'; 9 | SCHEMA : 'schema'; 10 | TYPE : 'type'; 11 | SCALAR : 'scalar'; 12 | INPUT : 'input'; 13 | ENUM : 'enum'; 14 | ID : [a-z_A-Z] [a-z_A-Z0-9-]*; 15 | 16 | DIGIT : [0-9]; 17 | STRING_CHARS: [a-zA-Z0-9 \t`~!@#$%^&*()_+={}|\\:\"'\u005B\u005D;<>?,./-]; 18 | 19 | int : '-'? DIGIT+; 20 | decimal : '-'? DIGIT+'.'DIGIT+; 21 | boolean : TRUE | FALSE; 22 | string : '"' ( '"' | ~('\n'|'\r') | STRING_CHARS )*? '"'; 23 | constant : string | int | decimal | boolean | idKeyword; // id should be an enum 24 | 25 | // This is our expression language 26 | schema : (schemaDef | typeDef | scalarDef | inputDef | enumDef | directiveDef | interfaceDef)+; 27 | 28 | schemaDef : comment* SCHEMA ws* objectDef; 29 | typeDef : comment* (EXTEND ws+)? TYPE ws+ typeName=idKeyword (ws+ 'implements' ws+ interfaceName=idKeyword)? ws* objectDef; 30 | interfaceDef: comment* 'interface' ws+ typeName=idKeyword ws* objectDef; 31 | scalarDef : comment* SCALAR ws+ typeName=idKeyword ws+; 32 | inputDef : comment* INPUT ws+ typeName=idKeyword ws* '{' ws* inputFields ws* comment* ws* '}' ws*; 33 | enumDef : comment* ENUM ws+ typeName=idKeyword ws* '{' (ws* enumItem ws* comment* ws*)+ '}' ws*; 34 | directiveTarget: 'FIELD' | 'FRAGMENT_SPREAD' | 'INLINE_FRAGMENT'; 35 | directiveDef: comment* 'directive' ws+ '@' name=idKeyword ('(' args=arguments ')')? ws+ 'on' ws+ (directiveTarget (ws+ '|' ws+ directiveTarget)*) ws*; 36 | 37 | inputFields : fieldDef (ws* '=' ws* constant)? (ws* ',')? (ws* fieldDef (ws* '=' ws* constant)? (ws* ',')?)* ws*; 38 | objectDef : '{' ws* fieldDef (ws* ',')? (ws* fieldDef (ws* ',')?)* ws* comment* ws* '}' ws*; 39 | 40 | fieldDef : comment* name=idKeyword ('(' args=arguments ')')? ws* ':' ws* type=dataType; 41 | enumItem : comment* name=idKeyword (ws* '.')?; 42 | arguments : ws* argument (ws* '=' ws* constant)? (ws* ',' ws* argument (ws* '=' ws* constant)?)*; 43 | argument : idKeyword ws* ':' ws* dataType; 44 | 45 | dataType : type=idKeyword required='!'? | '[' arrayType=idKeyword elementTypeRequired='!'? ']' arrayRequired='!'?; 46 | 47 | comment : ws* (singleLineDoc | multiLineDoc | ignoreComment) ws*; 48 | ignoreComment : ('#' ~('\n'|'\r')*) ('\n' | '\r' | EOF); 49 | multiLineDoc : ('"""' ~'"""'* '"""'); 50 | singleLineDoc : ('"' ~('\n'|'\r')* '"'); 51 | 52 | ws : ' ' | '\t' | '\n' | '\r'; 53 | -------------------------------------------------------------------------------- /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-console/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Threading.Tasks; 4 | using McMaster.Extensions.CommandLineUtils; 5 | 6 | namespace dotnet_gqlgen 7 | { 8 | public class Program 9 | { 10 | [Argument(0, Description = "Path to the GraphQL schema file or a GraphQL introspection endpoint")] 11 | [Required] 12 | public string Source { get; } 13 | 14 | [Option(LongName = "header", ShortName = "h", Description = "Headers to pass to GraphQL introspection endpoint. Use \"Authorization=Bearer eyJraWQ,X-API-Key=abc,...\"")] 15 | public string HeaderValues { 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 | 23 | [Option(LongName = "scalar_mapping", ShortName = "m", Description = "Map of custom schema scalar types to dotnet types. Use \"GqlType=DotNetClassName,ID=Guid,...\"")] 24 | public string ScalarMapping { get; } 25 | 26 | [Option(LongName = "output", ShortName = "o", Description = "Output directory")] 27 | public string OutputDir { get; } = "output"; 28 | 29 | [Option(LongName = "usings", ShortName = "u", Description = "Extra using statements to add to generated code.")] 30 | public string Usings { get; } = ""; 31 | 32 | [Option(LongName = "no_generated_timestamp", ShortName = "nt", Description = "Don't add 'Generated on abc from xyz' in generated files")] 33 | public bool NoGeneratedTimestamp { get; } 34 | 35 | [Option(LongName = "unix", ShortName = "un", Description = "Convert windows endings to unix")] 36 | public bool ConvertToUnixLineEnding { get; } 37 | 38 | public static Task Main(string[] args) => CommandLineApplication.ExecuteAsync(args); 39 | 40 | private async Task OnExecute() 41 | { 42 | try 43 | { 44 | await Generator.Generate(new() 45 | { 46 | Source = Source, 47 | HeaderValues = HeaderValues, 48 | Namespace = Namespace, 49 | ClientClassName = ClientClassName, 50 | ScalarMapping = ScalarMapping, 51 | OutputDir = OutputDir, 52 | Usings = Usings, 53 | NoGeneratedTimestamp = NoGeneratedTimestamp, 54 | ConvertToUnixLineEnding = ConvertToUnixLineEnding 55 | }); 56 | } 57 | catch (Exception e) 58 | { 59 | Console.WriteLine("Error: " + e); 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/tests/DotNetGraphQLQueryGen.Tests/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: RootQuery 3 | mutation: Mutation 4 | } 5 | 6 | """ comment """ 7 | scalar Date 8 | 9 | enum Sex { 10 | Female 11 | Male 12 | } 13 | 14 | type RootQuery { 15 | """ Returns list of actors paged 16 | @param page Page number to return [defaults: page = 1] 17 | @param pagesize Number of items per page to return [pagesize = 10] 18 | @param search Optional search string 19 | """ 20 | actorPager(page: Int = 1, pagesize: Int = 10, search: String): PersonPagination 21 | "List of actors" 22 | actors: [Person] 23 | "List of directors" 24 | directors: [Person] 25 | "Return a Movie by its Id" 26 | movie(id: Int!): Movie 27 | "Collection of Movies" 28 | movies: [Movie] 29 | moviesByIds(ids: [Int]!): [Movie] 30 | "Collection of Peoples" 31 | people: [Person] 32 | "Return a Person by its Id" 33 | person(id: Int!): Person 34 | "List of writers" 35 | writers: [Person] 36 | "List of producers with filter support" 37 | producers(filter: FilterBy): [Person] 38 | "Testing returning a scalar" 39 | getDisplayName(id: Int!): String 40 | deleteUser(id: Int!): Int 41 | } 42 | 43 | input FilterBy { 44 | field: String! 45 | value: String! 46 | } 47 | 48 | type SubscriptionType { 49 | name: String 50 | } 51 | 52 | """ 53 | This is a movie entity 54 | """ 55 | type Movie { 56 | id: Int!, 57 | name: String!, 58 | "Enum of Genre" 59 | genre: Int! 60 | released: Date! 61 | "Actors in the movie" 62 | actors: [Person!]! 63 | "Writers in the movie" 64 | writers: [Person!]! 65 | director: Person! 66 | directorId: Int! 67 | rating: Float 68 | "Just testing using gql schema keywords here" 69 | type: Int 70 | } 71 | 72 | type Actor { 73 | personId: Int! 74 | person: Person! 75 | movieId: Int! 76 | movie: Movie! 77 | } 78 | 79 | type Person { 80 | id: Int! 81 | firstName: String! 82 | lastName: String! 83 | "Movies they acted in" 84 | actorIn: [Movie] 85 | "Movies they wrote" 86 | writerOf: [Movie] 87 | directorOf: [Movie] 88 | died: Date 89 | isDeleted: Boolean! 90 | "Show the person's age" 91 | age: Int! 92 | "Person's name" 93 | name: String! 94 | } 95 | 96 | extend type Person { 97 | dob: Date! 98 | } 99 | 100 | type Writer { 101 | personId: Int! 102 | person: Person! 103 | movieId: Int! 104 | movie: Movie! 105 | } 106 | 107 | type PersonPagination { 108 | "total records to match search" 109 | total: Int! 110 | "total pages based on page size" 111 | pageCount: Int! 112 | "collection of people" 113 | people: [Person!] 114 | y: Int! # make sure we support single char field names 115 | } 116 | 117 | input Detail { 118 | description: String!, 119 | someNumber: Int = 9 120 | x: Float! 121 | } 122 | 123 | type Mutation { 124 | "Add a new Movie object" 125 | addMovie(name: String!, rating: Float = 5, details: [Detail], genre: Int, released: Date): Movie 126 | addActor(firstName: String, lastName: String, movieId: Int): Person 127 | addActor2(firstName: String, lastName: String, movieId: Int): Person 128 | # Not required for now 129 | #removePerson(id: Int!): Int 130 | } 131 | 132 | """comments""" 133 | directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT -------------------------------------------------------------------------------- /src/dotnet-gqlgen/CSharpKeywords.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace dotnet_gqlgen 4 | { 5 | static class CSharpKeywords 6 | { 7 | /// 8 | /// List of C# reserved identifiers. 9 | /// Note that contextual keyword can be used as var name 10 | /// 11 | /// 12 | private static readonly HashSet reservedIdentifiers = new HashSet 13 | { 14 | // reserved identifiers 15 | "abstract", 16 | "as", 17 | "base", 18 | "bool", 19 | "break", 20 | "byte", 21 | "case", 22 | "catch", 23 | "char", 24 | "checked", 25 | "class", 26 | "const", 27 | "continue", 28 | "decimal", 29 | "default", 30 | "delegate", 31 | "do", 32 | "double", 33 | "else", 34 | "enum", 35 | "event", 36 | "explicit", 37 | "extern", 38 | "false", 39 | "finally", 40 | "fixed", 41 | "float", 42 | "for", 43 | "foreach", 44 | "goto", 45 | "if", 46 | "implicit", 47 | "in", 48 | "int", 49 | "interface", 50 | "internal", 51 | "is", 52 | "lock", 53 | "long", 54 | "namespace", 55 | "new", 56 | "null", 57 | "object", 58 | "operator", 59 | "out", 60 | "override", 61 | "params", 62 | "private", 63 | "protected", 64 | "public", 65 | "readonly", 66 | "ref", 67 | "return", 68 | "sbyte", 69 | "sealed", 70 | "short", 71 | "sizeof", 72 | "stackalloc", 73 | "static", 74 | "string", 75 | "struct", 76 | "switch", 77 | "this", 78 | "throw", 79 | "true", 80 | "try", 81 | "typeof", 82 | "uint", 83 | "ulong", 84 | "unchecked", 85 | "unsafe", 86 | "ushort", 87 | "using", 88 | "virtual", 89 | "void", 90 | "volatile", 91 | "while" 92 | }; 93 | 94 | /// 95 | /// Checks if value is a C# keyword 96 | /// 97 | public static bool IsReservedIdentifier(string value) 98 | { 99 | return reservedIdentifiers.Contains(value); 100 | } 101 | 102 | /// 103 | /// If identifier is a reserved one then will escape it by prefixing with a @ 104 | /// 105 | public static string EscapeIdentifier(string identifier) 106 | { 107 | return IsReservedIdentifier(identifier) ? $"@{identifier}" : identifier; 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/tests/DotNetGraphQLQueryGen.Tests/Generated/TestHttpClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using System.Net.Http; 5 | using System.Net.Http.Headers; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using DotNetGqlClient; 9 | using Newtonsoft.Json; 10 | 11 | /// 12 | /// Generated interfaces for making GraphQL API calls with a typed interface. 13 | /// 14 | /// Generated on 9/5/20 1:58:19 pm from ../tests/DotNetGraphQLQueryGen.Tests/schema.graphql 15 | /// 16 | 17 | namespace Generated 18 | { 19 | public class GqlError 20 | { 21 | public string Message { get; set; } 22 | } 23 | public class GqlResult 24 | { 25 | public List Errors { get; set; } 26 | public TQuery Data { get; set; } 27 | } 28 | 29 | public class TestHttpClient : BaseGraphQLClient 30 | { 31 | private Uri apiUrl; 32 | private readonly HttpClient client; 33 | 34 | protected TestHttpClient() 35 | { 36 | this.typeMappings = new Dictionary { 37 | { "string" , "String" }, 38 | { "String" , "String" }, 39 | { "int" , "Int!" }, 40 | { "Int32" , "Int!" }, 41 | { "double" , "Float!" }, 42 | { "bool" , "Boolean!" }, 43 | { "DateTime" , "Date" }, 44 | }; 45 | } 46 | 47 | public TestHttpClient(HttpClient client) 48 | : this(client.BaseAddress, client) 49 | { 50 | } 51 | 52 | public TestHttpClient(Uri apiUrl, HttpClient client) : this() 53 | { 54 | this.apiUrl = apiUrl; 55 | this.client = client; 56 | this.client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 57 | } 58 | 59 | protected virtual async Task> ProcessResult(QueryRequest gql) 60 | { 61 | // gql is a GraphQL doc e.g. { query MyQuery { stuff { id name } } } 62 | // you can replace the following with what ever HTTP library you use 63 | // don't forget to implement your authentication if required 64 | var req = new HttpRequestMessage { 65 | RequestUri = this.apiUrl, 66 | Method = HttpMethod.Post, 67 | }; 68 | req.Content = new StringContent(JsonConvert.SerializeObject(gql), Encoding.UTF8, "application/json"); 69 | // you will need to implement any auth 70 | // req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); 71 | var res = await client.SendAsync(req); 72 | res.EnsureSuccessStatusCode(); 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(query); 81 | return await ProcessResult(gql); 82 | } 83 | 84 | public async Task> MutateAsync(Expression> query) 85 | { 86 | var gql = base.MakeQuery(query, "TestMutation", true); 87 | return await ProcessResult(gql); 88 | } 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/dotnet-gqlgen/client.cshtml: -------------------------------------------------------------------------------- 1 | @using System.IO 2 | @{ 3 | DisableEncoding = true; 4 | } 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq.Expressions; 8 | using System.Net.Http; 9 | using System.Net.Http.Headers; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | using DotNetGqlClient; 13 | using Newtonsoft.Json; 14 | 15 | /// 16 | /// Generated interfaces for making GraphQL API calls with a typed interface. 17 | @if (!Model.NoGeneratedTimestamp) 18 | { 19 | @:/// 20 | @:/// Generated on @DateTime.UtcNow from @Model.SchemaFile @Model.CmdArgs 21 | } 22 | /// 23 | 24 | namespace @Model.Namespace 25 | { 26 | public class GqlError 27 | { 28 | public string Message { get; set; } 29 | } 30 | public class GqlResult 31 | { 32 | public List Errors { get; set; } 33 | public TQuery Data { get; set; } 34 | } 35 | 36 | public class @Model.ClientClassName : BaseGraphQLClient 37 | { 38 | private Uri apiUrl; 39 | private readonly HttpClient client; 40 | 41 | protected @(Model.ClientClassName)() 42 | { 43 | this.typeMappings = new Dictionary { 44 | @foreach(var kvp in Model.Mappings) { 45 | @:{ "@kvp.Key" , "@kvp.Value" }, 46 | } 47 | }; 48 | } 49 | 50 | public @(Model.ClientClassName)(HttpClient client) 51 | : this(client.BaseAddress, client) 52 | { 53 | } 54 | 55 | public @(Model.ClientClassName)(Uri apiUrl, HttpClient client) : this() 56 | { 57 | this.apiUrl = apiUrl; 58 | this.client = client; 59 | this.client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 60 | } 61 | 62 | protected virtual async Task> ProcessResult(QueryRequest gql) 63 | { 64 | // gql is a GraphQL doc e.g. { query MyQuery { stuff { id name } } } 65 | // you can replace the following with what ever HTTP library you use 66 | // don't forget to implement your authentication if required 67 | var req = new HttpRequestMessage { 68 | RequestUri = this.apiUrl, 69 | Method = HttpMethod.Post, 70 | }; 71 | req.Content = new StringContent(JsonConvert.SerializeObject(gql), Encoding.UTF8, "application/json"); 72 | // you will need to implement any auth 73 | // req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); 74 | var res = await client.SendAsync(req); 75 | res.EnsureSuccessStatusCode(); 76 | var strResult = await res.Content.ReadAsStringAsync(); 77 | var data = JsonConvert.DeserializeObject>(strResult); 78 | return data; 79 | } 80 | 81 | public async Task> QueryAsync(Expression> query) 82 | { 83 | var gql = base.MakeQuery<@Model.Query.QueryName, TQuery>(query); 84 | return await ProcessResult(gql); 85 | } 86 | 87 | public async Task> MutateAsync(Expression> query) 88 | { 89 | var gql = base.MakeQuery<@Model.Mutation.QueryName, TQuery>(query, null, true); 90 | return await ProcessResult(gql); 91 | } 92 | 93 | public async Task> QueryAsync(string operationName, Expression> query) 94 | { 95 | var gql = base.MakeQuery<@Model.Query.QueryName, TQuery>(query, operationName); 96 | return await ProcessResult(gql); 97 | } 98 | 99 | public async Task> MutateAsync(string operationName, Expression> query) 100 | { 101 | var gql = base.MakeQuery<@Model.Mutation.QueryName, TQuery>(query, operationName, true); 102 | return await ProcessResult(gql); 103 | } 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /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.Equal(2, results.Inputs.Count); 23 | var queryTypeName = results.Schema.First(s => s.Name == "query").TypeName; 24 | 25 | var queryType = results.Types[queryTypeName]; 26 | Assert.Equal(12, 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(7).Name); 31 | Assert.Equal("Person", queryType.Fields.ElementAt(7).TypeName); 32 | Assert.False(queryType.Fields.ElementAt(7).IsArray); 33 | Assert.Single(queryType.Fields.ElementAt(7).Args); 34 | Assert.Equal("id", queryType.Fields.ElementAt(7).Args.First().Name); 35 | Assert.Equal("Int", queryType.Fields.ElementAt(7).Args.First().TypeName); 36 | Assert.True(queryType.Fields.ElementAt(7).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.Equal(2, results.Inputs.Count); 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(10, 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 | } -------------------------------------------------------------------------------- /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 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet-gqlgen-console", "src\dotnet-gqlgen-console\dotnet-gqlgen-console.csproj", "{A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Debug|x64 = Debug|x64 20 | Debug|x86 = Debug|x86 21 | Release|Any CPU = Release|Any CPU 22 | Release|x64 = Release|x64 23 | Release|x86 = Release|x86 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 29 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC}.Debug|x64.ActiveCfg = Debug|Any CPU 32 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC}.Debug|x64.Build.0 = Debug|Any CPU 33 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC}.Debug|x86.ActiveCfg = Debug|Any CPU 34 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC}.Debug|x86.Build.0 = Debug|Any CPU 35 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC}.Release|x64.ActiveCfg = Release|Any CPU 38 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC}.Release|x64.Build.0 = Release|Any CPU 39 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC}.Release|x86.ActiveCfg = Release|Any CPU 40 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC}.Release|x86.Build.0 = Release|Any CPU 41 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Debug|x64.ActiveCfg = Debug|Any CPU 44 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Debug|x64.Build.0 = Debug|Any CPU 45 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Debug|x86.ActiveCfg = Debug|Any CPU 46 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Debug|x86.Build.0 = Debug|Any CPU 47 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Release|x64.ActiveCfg = Release|Any CPU 50 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Release|x64.Build.0 = Release|Any CPU 51 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Release|x86.ActiveCfg = Release|Any CPU 52 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C}.Release|x86.Build.0 = Release|Any CPU 53 | {A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}.Debug|x64.ActiveCfg = Debug|Any CPU 56 | {A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}.Debug|x64.Build.0 = Debug|Any CPU 57 | {A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}.Debug|x86.ActiveCfg = Debug|Any CPU 58 | {A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}.Debug|x86.Build.0 = Debug|Any CPU 59 | {A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}.Release|Any CPU.ActiveCfg = Release|Any CPU 60 | {A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}.Release|Any CPU.Build.0 = Release|Any CPU 61 | {A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}.Release|x64.ActiveCfg = Release|Any CPU 62 | {A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}.Release|x64.Build.0 = Release|Any CPU 63 | {A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}.Release|x86.ActiveCfg = Release|Any CPU 64 | {A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A}.Release|x86.Build.0 = Release|Any CPU 65 | EndGlobalSection 66 | GlobalSection(NestedProjects) = preSolution 67 | {0112D762-344C-4545-9377-86D255E5AEAB} = {E65B3177-BF31-4B95-A594-06CF381474C7} 68 | {FFEBA61F-81B5-44C3-8178-1723A7BDB8AC} = {0112D762-344C-4545-9377-86D255E5AEAB} 69 | {8F67E2C5-6152-4DDD-BF6F-BDF8836F549C} = {E65B3177-BF31-4B95-A594-06CF381474C7} 70 | {A2C4B63C-C73D-4BF3-A8D0-EFABAAA0393A} = {E65B3177-BF31-4B95-A594-06CF381474C7} 71 | EndGlobalSection 72 | EndGlobal 73 | -------------------------------------------------------------------------------- /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.GetText(); 25 | var args = (List)VisitArguments(context.args); 26 | var type = context.type.type?.GetText(); 27 | var arrayType = context.type.arrayType?.GetText(); 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?.GetText(); 48 | var arrayType = arg.dataType().arrayType?.GetText(); 49 | args.Add(new Arg(this.schemaInfo) 50 | { 51 | Name = arg.idKeyword().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.GetText(), fields.Select(f => new EnumInfo(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.GetText(), 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.inputFields()); 108 | schemaInfo.Inputs.Add(context.typeName.GetText(), new TypeInfo(fields, context.typeName.GetText(), 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 | // you can extend type to add fields to it so the type might already be in the schema 122 | if (schemaInfo.Types.ContainsKey(context.typeName.GetText())) 123 | schemaInfo.Types[context.typeName.GetText()].Fields.AddRange(fields); 124 | else 125 | schemaInfo.Types.Add(context.typeName.GetText(), new TypeInfo(fields, context.typeName.GetText(), desc)); 126 | return result; 127 | } 128 | } 129 | public override object VisitInterfaceDef(GraphQLSchemaParser.InterfaceDefContext context) 130 | { 131 | var docComment = context.comment().LastOrDefault(); 132 | var desc = docComment != null ? (string)VisitComment(docComment) : null; 133 | 134 | var fields = new List(); 135 | using (new FieldConsumer(this, fields)) 136 | { 137 | var result = base.Visit(context.objectDef()); 138 | // you can extend type to add fields to it so the type might already be in the schema 139 | if (schemaInfo.Types.ContainsKey(context.typeName.GetText())) 140 | schemaInfo.Types[context.typeName.GetText()].Fields.AddRange(fields); 141 | else 142 | schemaInfo.Types.Add(context.typeName.GetText(), new TypeInfo(fields, context.typeName.GetText(), desc, false, true)); 143 | return result; 144 | } 145 | } 146 | public override object VisitScalarDef(GraphQLSchemaParser.ScalarDefContext context) 147 | { 148 | var result = base.VisitScalarDef(context); 149 | schemaInfo.Scalars.Add(context.typeName.GetText()); 150 | return result; 151 | } 152 | } 153 | } -------------------------------------------------------------------------------- /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 Newtonsoft.Json; 7 | using Xunit; 8 | using Xunit.Abstractions; 9 | 10 | namespace CoreData.Model.Tests 11 | { 12 | public class TestClient : TestHttpClient 13 | { 14 | internal QueryRequest MakeQuery(Expression> p, bool mutation = false) 15 | { 16 | return base.MakeQuery(p, "TestQuery", mutation); 17 | } 18 | internal QueryRequest MakeMutation(Expression> p, bool mutation = false) 19 | { 20 | return base.MakeQuery(p, "TestQuery", mutation); 21 | } 22 | } 23 | 24 | public class TestTypedQuery 25 | { 26 | public TestTypedQuery(ITestOutputHelper testOutputHelper) 27 | { 28 | } 29 | 30 | [Fact] 31 | public void SimpleArgs() 32 | { 33 | var client = new TestClient(); 34 | var query = client.MakeQuery(q => new { 35 | Movies = q.Movies(s => new { 36 | s.Id, 37 | }), 38 | }); 39 | Assert.Equal($@"query TestQuery {{ 40 | Movies: movies {{ 41 | Id: id 42 | }} 43 | }}", query.Query, ignoreLineEndingDifferences: true); 44 | } 45 | 46 | [Fact] 47 | public void SimpleQuery() 48 | { 49 | var client = new TestClient(); 50 | var query = client.MakeQuery(q => new { 51 | Actors = q.Actors(s => new { 52 | s.Id, 53 | DirectorOf = s.DirectorOf(), 54 | }), 55 | }); 56 | Assert.Equal($@"query TestQuery {{ 57 | Actors: actors {{ 58 | Id: id 59 | DirectorOf: directorOf {{ 60 | Id: id 61 | Name: name 62 | Genre: genre 63 | Released: released 64 | DirectorId: directorId 65 | Rating: rating 66 | Type: type 67 | }} 68 | }} 69 | }}", query.Query, ignoreLineEndingDifferences: true); 70 | } 71 | 72 | [Fact] 73 | public void MethodWithScalarReturn() 74 | { 75 | var client = new TestClient(); 76 | var query = client.MakeQuery(_ => new { 77 | displayName = _.GetDisplayName(1) 78 | }); 79 | Assert.Equal($@"query TestQuery {{ 80 | displayName: getDisplayName(id: 1) 81 | }}", query.Query, ignoreLineEndingDifferences: true); 82 | } 83 | 84 | 85 | [Fact] 86 | public void TypedClass() 87 | { 88 | var client = new TestClient(); 89 | var query = client.MakeQuery(q => new MyResult 90 | { 91 | Movies = q.Movies(s => new MovieResult 92 | { 93 | Id = s.Id, 94 | }), 95 | }); 96 | Assert.Equal($@"query TestQuery {{ 97 | Movies: movies {{ 98 | Id: id 99 | }} 100 | }}", query.Query, ignoreLineEndingDifferences: true); 101 | } 102 | 103 | [Fact] 104 | public void TestMutationWithDate() 105 | { 106 | var client = new TestClient(); 107 | var query = client.MakeMutation(q => new 108 | { 109 | Movie = q.AddMovie("movie", 5.5, null, null, new DateTime(2019, 10, 30, 17, 55, 23), s => new 110 | { 111 | s.Id, 112 | }), 113 | }, true); 114 | Assert.Equal($@"mutation TestQuery {{ 115 | Movie: addMovie(name: ""movie"", rating: 5.5, released: ""2019-10-30T17:55:23.0000000"") {{ 116 | Id: id 117 | }} 118 | }}", query.Query, ignoreLineEndingDifferences: true); 119 | } 120 | 121 | [Fact] 122 | public void TestArrayArg() 123 | { 124 | var client = new TestClient(); 125 | var idList = new List { 1, 2, 5 }; 126 | var query = client.MakeQuery(q => new 127 | { 128 | Movies = q.MoviesByIds(idList, s => new 129 | { 130 | s.Id, 131 | }), 132 | }); 133 | Assert.Equal($@"query TestQuery ($a0: [Int]) {{ 134 | Movies: moviesByIds(ids: $a0) {{ 135 | Id: id 136 | }} 137 | }}", query.Query, ignoreLineEndingDifferences: true); 138 | 139 | Assert.Equal(@"{""a0"":[1,2,5]}", JsonConvert.SerializeObject(query.Variables), ignoreLineEndingDifferences: true); 140 | } 141 | 142 | [Fact] 143 | public void TestArrayArgExprInited() 144 | { 145 | var client = new TestClient(); 146 | var query = client.MakeQuery(q => new 147 | { 148 | Movies = q.MoviesByIds(new List { 1, 2, 5 }, s => new 149 | { 150 | s.Id, 151 | }), 152 | }); 153 | Assert.Equal($@"query TestQuery ($a0: [Int]) {{ 154 | Movies: moviesByIds(ids: $a0) {{ 155 | Id: id 156 | }} 157 | }}", query.Query, ignoreLineEndingDifferences: true); 158 | 159 | Assert.Equal(@"{""a0"":[1,2,5]}", JsonConvert.SerializeObject(query.Variables), ignoreLineEndingDifferences: true); 160 | } 161 | 162 | [Fact] 163 | public void TestComplexValueArg() 164 | { 165 | var client = new TestClient(); 166 | var query = client.MakeQuery(q => new 167 | { 168 | Producers = q.Producers(new FilterBy { Field = "lastName", Value = "Lucas" }, s => new 169 | { 170 | s.Id, 171 | s.LastName 172 | }), 173 | }); 174 | Assert.Equal($@"query TestQuery {{ 175 | Producers: producers(filter: {{ field: ""lastName"", value: ""Lucas"" }}) {{ 176 | Id: id 177 | LastName: lastName 178 | }} 179 | }}", query.Query, ignoreLineEndingDifferences: true); 180 | } 181 | 182 | [Fact] 183 | public void TestErrorOnInvalidPropertySelection() 184 | { 185 | Assert.Throws(() => { 186 | var client = new TestClient(); 187 | var query = client.MakeQuery(q => new 188 | { 189 | Movie = q.Movies(s => new 190 | { 191 | // we generate gql we don't select the .Value the value will be serialised in the object we're creating 192 | s.Rating.Value, 193 | }), 194 | }); 195 | }); 196 | } 197 | [Fact] 198 | public void TestErrorOnInvalidPropertySelection2() 199 | { 200 | Assert.Throws(() => { 201 | var client = new TestClient(); 202 | var query = client.MakeQuery(q => new 203 | { 204 | Movie = q.Movies(s => new 205 | { 206 | // we generate gql we don't select the .Value the value will be serialised in the object we're creating 207 | s.Director().Died 208 | }), 209 | }); 210 | }); 211 | } 212 | } 213 | 214 | public class MovieResult 215 | { 216 | public int Id { get; set; } 217 | } 218 | 219 | public class MyResult 220 | { 221 | public List Movies { get; set; } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/dotnet-gqlgen/IntroSpectionCompiler.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text.RegularExpressions; 7 | 8 | namespace dotnet_gqlgen 9 | { 10 | public enum OperationType 11 | { 12 | Query, 13 | Mutation, 14 | // Subscription // Doesn't seem to be supported in SchemaInfo 15 | } 16 | 17 | /// 18 | /// Transforms introspection results to 19 | /// For more information about introspection files, see 20 | /// 21 | public class IntrospectionCompiler 22 | { 23 | /// 24 | /// Filters out any item that matches the below regex 25 | /// Note: type names starting with __ are introspection related queries 26 | /// 27 | private readonly string omitTypeRegex = "^__|CacheControlScope"; 28 | 29 | private class IntrospectionResult 30 | { 31 | public JToken Data { get; set; } 32 | public string Error { get; set; } 33 | public string Message { get; set; } 34 | } 35 | 36 | public static SchemaInfo Compile(string introSpectionText, Dictionary typeMappings = null) 37 | { 38 | var result = JsonConvert.DeserializeObject(introSpectionText); 39 | if (!string.IsNullOrEmpty(result?.Error)) 40 | { 41 | throw new Exception(result.Message ?? result.Error); 42 | } 43 | return new IntrospectionCompiler().ParseSchema(result?.Data, typeMappings); 44 | } 45 | 46 | private SchemaInfo ParseSchema(JToken data, Dictionary typeMappings) 47 | { 48 | var schemaInfo = new SchemaInfo(typeMappings); 49 | 50 | var schema = data?["__schema"]; 51 | if (schema != null) 52 | { 53 | // Extract operation types from the schema and add to the schema info 54 | AddOperationTypes(schemaInfo, schema); 55 | AddTypes(schemaInfo, schema); 56 | } 57 | return schemaInfo; 58 | } 59 | 60 | private void AddOperationTypes(SchemaInfo schemaInfo, JToken schema) 61 | { 62 | // Transform operation types to fields 63 | var operationTypeFields = Enum.GetNames(typeof(OperationType)) 64 | .Select(ot => char.ToLowerInvariant(ot[0]) + ot.Substring(1)) 65 | .Select(otGql => new { otGql, typeObj = schema[$"{otGql}Type"] }) 66 | .Where(res => res.typeObj != null) 67 | .Select(res => new Field(schemaInfo) { Name = res.otGql, TypeName = res.typeObj.ReadName() }); 68 | 69 | schemaInfo.Schema.AddRange(operationTypeFields); 70 | } 71 | 72 | private void AddTypes(SchemaInfo schemaInfo, JToken schema) 73 | { 74 | JArray allTypes = schema["types"] as JArray; 75 | Regex omitTypeCheck = !string.IsNullOrEmpty(omitTypeRegex) ? 76 | new Regex(omitTypeRegex, RegexOptions.Compiled) : null; 77 | 78 | // Get a filtered list of types 79 | IEnumerable typesToParse = omitTypeCheck != null ? 80 | allTypes.Where(type => !omitTypeCheck.IsMatch(type.ReadName())) : 81 | allTypes; 82 | 83 | foreach (var type in typesToParse) 84 | { 85 | var name = type.ReadName(); 86 | var kind = type.ReadKind(); 87 | 88 | switch (kind) 89 | { 90 | case "ENUM": 91 | schemaInfo.Enums.Add(name, type["enumValues"].Select(e => new EnumInfo(e.ReadName())).ToList()); 92 | break; 93 | 94 | case "SCALAR": 95 | if (!schemaInfo.gqlToDotnetTypeMappings.ContainsKey(name)) 96 | { 97 | Console.WriteLine($"WARNING: Scalar type '{name}' not found in mappings"); 98 | } 99 | break; 100 | 101 | case "INPUT_OBJECT": 102 | { 103 | var inputFields = type["inputFields"].Select(i => GetField(schemaInfo, i)); 104 | var typeInfo = new TypeInfo(inputFields, name, type.ReadDescription(), true); 105 | 106 | schemaInfo.Inputs.Add(name, typeInfo); 107 | } 108 | break; 109 | 110 | case "OBJECT": 111 | { 112 | var fields = type["fields"].Select(i => GetField(schemaInfo, i)); 113 | var typeInfo = new TypeInfo(fields, name, type.ReadDescription()); 114 | schemaInfo.Types.Add(name, typeInfo); 115 | } 116 | break; 117 | 118 | default: 119 | Console.WriteLine($"Warning, no handler for '{kind}'"); 120 | break; 121 | } 122 | } 123 | } 124 | 125 | private Field GetField(SchemaInfo schemaInfo, JToken fieldToken) 126 | { 127 | var typeToken = fieldToken["type"]; 128 | var argsToken = fieldToken["args"] as JArray; 129 | return new Field(schemaInfo) 130 | { 131 | Args = GetArgs(schemaInfo, argsToken), 132 | Name = fieldToken.ReadName(), 133 | Description = fieldToken.ReadDescription(), 134 | TypeName = GetValueType(typeToken), 135 | IsNonNullable = IsNonNullable(typeToken), 136 | IsArray = IsArray(typeToken) 137 | }; 138 | } 139 | 140 | private List GetArgs(SchemaInfo schemaInfo, JArray argsToken) 141 | { 142 | var argsList = new List(); 143 | if (argsToken != null && argsToken.Count > 0) 144 | { 145 | argsList.AddRange(argsToken.Select(at => new Arg(schemaInfo) 146 | { 147 | Name = at.ReadName(), 148 | Description = at.ReadDescription(), 149 | TypeName = GetValueType(at["type"]), 150 | IsNonNullable = IsNonNullable(at["type"]), 151 | IsArray = IsArray(at["type"]), 152 | Required = IsNonNullable(at["type"]) 153 | })); 154 | } 155 | return argsList; 156 | } 157 | 158 | private string GetValueType(JToken typeToken) 159 | { 160 | var ofType = typeToken["ofType"]; 161 | return (ofType != null && ofType.HasValues) ? 162 | GetValueType(ofType) : typeToken.ReadName(); 163 | } 164 | 165 | private bool IsNonNullable(JToken typeToken) => typeToken.ReadKind() == "NON_NULL"; 166 | 167 | private bool IsArray(JToken typeToken) => typeToken.ReadKind() == "LIST"; 168 | } 169 | 170 | internal static class IntroSpectionExtensions 171 | { 172 | public static string ReadString(this JToken token, string fieldName) => (string)token[fieldName]; 173 | 174 | public static string ReadName(this JToken token) => token.ReadString("name"); 175 | 176 | public static string ReadKind(this JToken token) => token.ReadString("kind"); 177 | 178 | public static string ReadDescription(this JToken token) => token.ReadString("description")?.Trim(); 179 | } 180 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dotnet GraphQL Query generator 2 | 3 | Given a GraphQL schema file (or a GraphQL endpoint where we can do a schema introspection), this tool will generate interfaces and classes to enable strongly typed querying from C# to a GraphQL API. 4 | 5 | Example 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 | ## Query Generator Command 49 | 50 | To generate from a graphql schema file:\ 51 |     `dotnet run -- schema.graphql -m Date=DateTime` 52 | 53 | To generate from a live GraphQL server (*note:* introspection may not be supported or enabled on your server):\ 54 |     `dotnet run -- http://myapi.app/gql -m Date=DateTime -h "Authorization=Bearer eyAfd..."` 55 | 56 | For all command line options, use --help or -? 57 | 58 | Both commands will generate the files [GraphQLClient.cs](#graphqlclient.cs) and [GeneratedTypes.cs](#generatedtypes.cs). 59 | 60 | ### GeneratedTypes.cs 61 | Based on the example schema.graphql the `GeneratedTypes.cs` file will have the following content: 62 | 63 | ```c# 64 | public interface RootQuery 65 | { 66 | [GqlFieldName("actors")] 67 | List Actors(); 68 | 69 | [GqlFieldName("actors")] 70 | List Actors(Expression> selection); 71 | 72 | [GqlFieldName("directors")] 73 | List Directors(); 74 | 75 | [GqlFieldName("directors")] 76 | List Directors(Expression> selection); 77 | 78 | [GqlFieldName("movie")] 79 | Movie Movie(); 80 | 81 | [GqlFieldName("movie")] 82 | TReturn Movie(int id, Expression> selection); 83 | 84 | [GqlFieldName("movies")] 85 | List Movies(); 86 | 87 | [GqlFieldName("movies")] 88 | List Movies(Expression> selection); 89 | } 90 | 91 | public interface Movie 92 | { 93 | [GqlFieldName("id")] 94 | int Id { get; } 95 | [GqlFieldName("name")] 96 | string Name { get; } 97 | [GqlFieldName("genre")] 98 | int Genre { get; } 99 | [GqlFieldName("released")] 100 | DateTime Released { get; } 101 | [GqlFieldName("actors")] 102 | List Actors(); 103 | [GqlFieldName("actors")] 104 | List Actors(Expression> selection); 105 | [GqlFieldName("writers")] 106 | List Writers(); 107 | [GqlFieldName("writers")] 108 | List Writers(Expression> selection); 109 | [GqlFieldName("director")] 110 | Person Director(); 111 | [GqlFieldName("director")] 112 | TReturn Director(Expression> selection); 113 | [GqlFieldName("rating")] 114 | double Rating { get; } 115 | } 116 | 117 | public interface Person 118 | { 119 | [GqlFieldName("id")] 120 | int Id { get; } 121 | [GqlFieldName("dob")] 122 | DateTime Dob { get; } 123 | [GqlFieldName("actorIn")] 124 | List ActorIn(); 125 | [GqlFieldName("actorIn")] 126 | List ActorIn(Expression> selection); 127 | [GqlFieldName("writerOf")] 128 | List WriterOf(); 129 | [GqlFieldName("writerOf")] 130 | List WriterOf(Expression> selection); 131 | [GqlFieldName("directorOf")] 132 | List DirectorOf(); 133 | [GqlFieldName("directorOf")] 134 | List DirectorOf(Expression> selection); 135 | [GqlFieldName("died")] 136 | DateTime Died { get; } 137 | [GqlFieldName("age")] 138 | int Age { get; } 139 | [GqlFieldName("name")] 140 | string Name { get; } 141 | } 142 | 143 | public interface Mutation 144 | { 145 | [GqlFieldName("addPerson")] 146 | TReturn AddPerson(DateTime dob, string name, Expression> selection); 147 | } 148 | ``` 149 | 150 | ### GraphQLClient.cs 151 | 152 | The generated `GraphQLClient` class instance acts as a session to send GraphQL requests. 153 | 154 | ```c# 155 | var httpClient = new HttpClient { BaseAddress = new Uri("http://myapi.app/gql") }; 156 | var client = new GraphQLClient(httpClient); 157 | ``` 158 | 159 | For custom headers like Authorization, configure them in the HttpClient. These will be used during the requests. 160 | ```c# 161 | var httpClient = new HttpClient { BaseAddress = new Uri("http://myapi.app/") }; 162 | httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "eyAf3s..."); 163 | 164 | var client = new GraphQLClient(new Uri("gql", UriKind.Relative), httpClient); 165 | ``` 166 | 167 | To use the GraphQLClient, copy the following files into your project. These will all be in the output folder: 168 | 1. GraphQLClient.cs - a simple client you can modify or extend 169 | 2. GeneratedResultTypes.cs - generated types from the schema 170 | 2. GeneratedQueryTypes.cs - extensions to allow the linq style querying 171 | 3. BaseGraphQLClient.cs - a base client that GraphQLClient inherits from 172 | 4. GqlFieldNameAttribute.cs - required by the generated code 173 | 174 | Ideally move this to a dependency style library and tool in the future. 175 | 176 | ## Request Methods 177 | 178 | `GraphQLClient` exposes 2 methods: 179 | 180 | * [Query](#query)\ 181 | `async Task> QueryAsync(Expression> query)` 182 | * [Mutation](#mutation)\ 183 | `async Task> MutateAsync(Expression> query)` 184 | 185 | ### Query 186 | 187 | ```c# 188 | // queries 189 | var result = await client.QueryAsync(q => new { 190 | Movie = q.Movie(2, m => new { 191 | m.Id, 192 | m.Name, 193 | ReleaseDate = m.Released, 194 | Director = m.Director(d => new { 195 | d.Name 196 | }) 197 | }), 198 | Actors = q.Actors(a => new { 199 | a.Name, 200 | a.Dob 201 | }) 202 | }); 203 | ``` 204 | 205 | This looks similar to the GraphQL. And now `result.Data` is a strongly type object with the expected types. 206 | 207 | If a field in the schema is an object (can have a selection performed on it) it will be exposed in the generated code as a method where you pass in any field arguments first and then the selection. 208 | 209 | The GraphQL created by the above will look like this: 210 | ``` 211 | { 212 | query { 213 | Movie: movie(id: 2) { 214 | Id: id 215 | Name: name 216 | ReleaseDate: released 217 | Director: director { 218 | Name: name 219 | } 220 | } 221 | Actors: actors { 222 | Name: name 223 | Dob: dob 224 | } 225 | } 226 | } 227 | ``` 228 | 229 | ### Mutations 230 | 231 | 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 232 | 233 | ```c# 234 | // mutations 235 | var mutationResult = await client.MutateAsync(m => new { 236 | NewPerson = m.AddPerson(new DateTime(1801, 7, 3), "John Johny", p => new { p.Id }) 237 | }); 238 | ``` 239 | 240 | `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 anonymous 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. 241 | 242 | The above has `mutationResult` strongly typed. E.g. `mutationResult.Data.NewPerson` will only have an `Id` field. 243 | 244 | ## Input types 245 | 246 | 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 separate a `GqlScalar=DotnetType` list. 247 | -------------------------------------------------------------------------------- /src/dotnet-gqlgen/Generator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Reflection; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using Newtonsoft.Json; 10 | using RazorLight; 11 | 12 | namespace dotnet_gqlgen 13 | { 14 | public class GeneratorOptions 15 | { 16 | public string Source { get; set; } 17 | public string HeaderValues { get; set; } 18 | public string Namespace { get; set; } = "Generated"; 19 | public string ClientClassName { get; set; } = "GraphQLClient"; 20 | public string ScalarMapping { get; set; } 21 | public string OutputDir { get; set; } = "output"; 22 | public string Usings { get; set; } = ""; 23 | public bool NoGeneratedTimestamp { get; set; } 24 | public bool ConvertToUnixLineEnding { get; set; } = true; 25 | } 26 | 27 | public class ModelType 28 | { 29 | public string Namespace { get; set; } 30 | public string SchemaFile { get; set; } 31 | public Dictionary Types { get; set; } 32 | public Dictionary> Enums { get; set; } 33 | public TypeInfo Mutation { get; set; } 34 | public string CmdArgs { get; set; } 35 | public string Usings { get; set; } 36 | public bool NoGeneratedTimestamp { get; set; } 37 | } 38 | 39 | public static class Generator 40 | { 41 | public static async Task Generate(GeneratorOptions options) 42 | { 43 | if (string.IsNullOrWhiteSpace(options.Source)) throw new ArgumentException($"{nameof(options.Source)} is required"); 44 | 45 | var dotnetToGqlTypeMappings = new Dictionary 46 | { 47 | { "string", "String" }, 48 | { "String", "String" }, 49 | { "int", "Int!" }, 50 | { "Int32", "Int!" }, 51 | { "double", "Float!" }, 52 | { "bool", "Boolean!" }, 53 | }; 54 | 55 | Uri uriResult; 56 | bool isGraphQlEndpoint = Uri.TryCreate(options.Source, UriKind.Absolute, out uriResult) 57 | && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps); 58 | 59 | string schemaText = null; 60 | bool isIntroSpectionFile = false; 61 | 62 | if (isGraphQlEndpoint) 63 | { 64 | Console.WriteLine($"Loading from {options.Source}..."); 65 | using (var httpClient = new HttpClient()) 66 | { 67 | foreach (var header in SplitMultiValueArgument(options.HeaderValues)) 68 | { 69 | httpClient.DefaultRequestHeaders.Add(header.Key, header.Value); 70 | } 71 | 72 | Dictionary request = new Dictionary(); 73 | request["query"] = IntroSpectionQuery.Query; 74 | request["operationName"] = "IntrospectionQuery"; 75 | 76 | var response = httpClient 77 | .PostAsync(options.Source, 78 | new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json")).GetAwaiter().GetResult(); 79 | 80 | schemaText = await response.Content.ReadAsStringAsync(); 81 | isIntroSpectionFile = true; 82 | } 83 | } 84 | else 85 | { 86 | Console.WriteLine($"Loading {options.Source}..."); 87 | schemaText = await File.ReadAllTextAsync(options.Source); 88 | isIntroSpectionFile = Path.GetExtension(options.Source).Equals(".json", StringComparison.OrdinalIgnoreCase); 89 | } 90 | 91 | var mappings = new Dictionary(); 92 | if (!string.IsNullOrEmpty(options.ScalarMapping)) 93 | { 94 | SplitMultiValueArgument(options.ScalarMapping).ToList().ForEach(i => 95 | { 96 | dotnetToGqlTypeMappings[i.Value] = i.Key; 97 | mappings[i.Key] = i.Value; 98 | }); 99 | } 100 | 101 | // parse into AST 102 | var typeInfo = !isIntroSpectionFile ? SchemaCompiler.Compile(schemaText, mappings) : IntrospectionCompiler.Compile(schemaText, mappings); 103 | 104 | Console.WriteLine($"Generating types in namespace {options.Namespace}, outputting to {options.ClientClassName}.cs"); 105 | 106 | var rootType = typeof(Generator); 107 | 108 | // pass the schema to the template 109 | var engine = new RazorLightEngineBuilder() 110 | .UseEmbeddedResourcesProject(rootType) 111 | .UseMemoryCachingProvider() 112 | .Build(); 113 | 114 | var allTypes = typeInfo.Types.Concat(typeInfo.Inputs).ToDictionary(k => k.Key, v => v.Value); 115 | 116 | string resultTypes = await engine.CompileRenderAsync("resultTypes.cshtml", new ModelType 117 | { 118 | Namespace = options.Namespace, 119 | SchemaFile = options.Source, 120 | Types = allTypes, 121 | Enums = typeInfo.Enums, 122 | Mutation = typeInfo.Mutation, 123 | CmdArgs = $"-n {options.Namespace} -c {options.ClientClassName} -m {options.ScalarMapping} -u {options.Usings.Replace("\n", "\\n")}", 124 | Usings = options.Usings, 125 | NoGeneratedTimestamp = options.NoGeneratedTimestamp 126 | }); 127 | Directory.CreateDirectory(options.OutputDir); 128 | await NormalizeAndWriteIfChanged($"{options.OutputDir}/GeneratedResultTypes.cs", resultTypes, options.ConvertToUnixLineEnding); 129 | 130 | string queryTypes = await engine.CompileRenderAsync("queryTypes.cshtml", new ModelType 131 | { 132 | Namespace = options.Namespace, 133 | SchemaFile = options.Source, 134 | Types = allTypes, 135 | Mutation = typeInfo.Mutation, 136 | CmdArgs = $"-n {options.Namespace} -c {options.ClientClassName} -m {options.ScalarMapping} -u {options.Usings.Replace("\n", "\\n")}", 137 | Usings = options.Usings, 138 | NoGeneratedTimestamp = options.NoGeneratedTimestamp 139 | }); 140 | Directory.CreateDirectory(options.OutputDir); 141 | await NormalizeAndWriteIfChanged($"{options.OutputDir}/GeneratedQueryTypes.cs", queryTypes, options.ConvertToUnixLineEnding); 142 | 143 | resultTypes = await engine.CompileRenderAsync("client.cshtml", new 144 | { 145 | Namespace = options.Namespace, 146 | SchemaFile = options.Source, 147 | Query = typeInfo.Query, 148 | Mutation = typeInfo.Mutation, 149 | ClientClassName = options.ClientClassName, 150 | Mappings = dotnetToGqlTypeMappings, 151 | CmdArgs = $"-n {options.Namespace} -c {options.ClientClassName} -m {options.ScalarMapping}", 152 | options.NoGeneratedTimestamp 153 | }); 154 | await NormalizeAndWriteIfChanged($"{options.OutputDir}/{options.ClientClassName}.cs", resultTypes, options.ConvertToUnixLineEnding); 155 | 156 | await WriteResourceToFile(rootType, "BaseGraphQLClient.cs", $"{options.OutputDir}/BaseGraphQLClient.cs", options.ConvertToUnixLineEnding); 157 | await WriteResourceToFile(rootType, "GqlFieldNameAttribute.cs", $"{options.OutputDir}/GqlFieldNameAttribute.cs", options.ConvertToUnixLineEnding); 158 | 159 | Console.WriteLine($"Done."); 160 | } 161 | 162 | private static async Task NormalizeAndWriteIfChanged(string file, string text, bool convertToUnixLineEnding) 163 | { 164 | var normalizedText = convertToUnixLineEnding ? text.Replace("\r\n", "\n") : text; 165 | if (File.Exists(file) && string.Equals(await File.ReadAllTextAsync(file), normalizedText)) return; 166 | 167 | await File.WriteAllTextAsync(file, normalizedText); 168 | } 169 | 170 | private static async Task WriteResourceToFile(Type rootType, string resourceName, string outputLocation, bool convertToUnixLineEnding) 171 | { 172 | var assembly = rootType.GetTypeInfo().Assembly; 173 | await using var resourceStream = assembly.GetManifestResourceStream($"{rootType.Namespace}.{resourceName}")!; 174 | using var streamReader = new StreamReader(resourceStream); 175 | 176 | var text = await streamReader.ReadToEndAsync(); 177 | await NormalizeAndWriteIfChanged(outputLocation, text, convertToUnixLineEnding); 178 | } 179 | 180 | /// 181 | /// Splits an argument value like "value1=v1;value2=v2" into a dictionary. 182 | /// 183 | /// Very simple splitter. Eg can't handle semi-colon's or equal signs in values 184 | private static Dictionary SplitMultiValueArgument(string arg) 185 | { 186 | if (string.IsNullOrEmpty(arg)) 187 | { 188 | return new Dictionary(); 189 | } 190 | 191 | return arg 192 | .Split(';') 193 | .Select(h => h.Split('=')) 194 | .Where(hs => hs.Length >= 2) 195 | .ToDictionary(key => key[0], value => value[1]); 196 | } 197 | } 198 | } -------------------------------------------------------------------------------- /src/dotnet-gqlgen/SchemaInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace dotnet_gqlgen 7 | { 8 | public class SchemaInfo 9 | { 10 | public Dictionary gqlToDotnetTypeMappings = new Dictionary { 11 | {"String", "string"}, 12 | {"ID", "string"}, 13 | {"Int", "int?"}, 14 | {"Float", "double?"}, 15 | {"Boolean", "bool?"}, 16 | {"String!", "string"}, 17 | {"ID!", "string"}, 18 | {"Int!", "int"}, 19 | {"Float!", "double"}, 20 | {"Boolean!", "bool"}, 21 | }; 22 | 23 | public SchemaInfo(Dictionary typeMappings) 24 | { 25 | if (typeMappings != null) 26 | { 27 | foreach (var item in typeMappings) 28 | { 29 | // overrides 30 | this.gqlToDotnetTypeMappings[item.Key] = item.Value; 31 | } 32 | } 33 | Schema = new List(); 34 | Types = new Dictionary(); 35 | Inputs = new Dictionary(); 36 | Enums = new Dictionary>(); 37 | Scalars = new List(); 38 | } 39 | 40 | public List Schema { get; } 41 | /// 42 | /// Return the query type info. 43 | /// 44 | public TypeInfo Query => Types[Schema.First(f => f.Name == "query").TypeName]; 45 | 46 | /// 47 | /// Return the mutation type info. 48 | /// 49 | public TypeInfo Mutation 50 | { 51 | get 52 | { 53 | var typeName = Schema.FirstOrDefault(f => f.Name == "mutation")?.TypeName; 54 | if (typeName != null) 55 | return Types[typeName]; 56 | return null; 57 | } 58 | } 59 | public Dictionary Types { get; } 60 | public Dictionary Inputs { get; } 61 | public Dictionary> Enums { get; set; } 62 | public List Scalars { get; } 63 | 64 | internal bool HasDotNetType(string typeName) 65 | { 66 | return gqlToDotnetTypeMappings.ContainsKey(typeName) || Types.ContainsKey(typeName) || Inputs.ContainsKey(typeName) || Enums.ContainsKey(typeName); 67 | } 68 | 69 | internal string GetDotNetType(string typeName) 70 | { 71 | if (gqlToDotnetTypeMappings.ContainsKey(typeName)) 72 | return gqlToDotnetTypeMappings[typeName]; 73 | if (Types.ContainsKey(typeName)) 74 | return Types[typeName].Name; 75 | if (Enums.ContainsKey(typeName)) 76 | return typeName; 77 | return Inputs[typeName].Name; 78 | } 79 | internal bool IsEnum(string typeName) 80 | { 81 | return Enums.ContainsKey(typeName); 82 | } 83 | } 84 | 85 | public class EnumInfo 86 | { 87 | public EnumInfo(string name) 88 | { 89 | Name = name; 90 | } 91 | 92 | public string Name { get; } 93 | public string DotNetName => Name[0].ToString().ToUpper() + string.Join("", Name.Skip(1)); 94 | } 95 | 96 | 97 | public class TypeInfo 98 | { 99 | public TypeInfo(IEnumerable fields, string name, string description, bool isInput = false, bool isInterface = false) 100 | { 101 | Fields = fields.ToList(); 102 | Name = name; 103 | Description = description; 104 | IsInput = isInput; 105 | IsInterface = isInterface; 106 | } 107 | 108 | public List Fields { get; } 109 | public string Name { get; } 110 | public string QueryName => $"Query{Name}"; 111 | public string Description { get; } 112 | public string DescriptionForComment(int indent = 8) 113 | { 114 | return string.Join("\n", Description.Split("\n").Select(l => l.Trim()).Where(l => l.Count() > 0).Select(l => $"/// {l}".PadLeft(indent + l.Length + 4))) + "\n"; 115 | } 116 | 117 | public bool IsInput { get; } 118 | public bool IsInterface { get; } 119 | } 120 | 121 | public class Field 122 | { 123 | private readonly SchemaInfo schemaInfo; 124 | 125 | public Field(SchemaInfo schemaInfo) 126 | { 127 | Args = new List(); 128 | this.schemaInfo = schemaInfo; 129 | } 130 | public virtual string Name { get; set; } 131 | public string TypeName { get; set; } 132 | public bool IsArray { get; set; } 133 | public bool IsScalar 134 | { 135 | get 136 | { 137 | return (!schemaInfo.Types.ContainsKey(TypeName) && !schemaInfo.Inputs.ContainsKey(TypeName)) || schemaInfo.Scalars.Contains(TypeName); 138 | } 139 | } 140 | public bool IsNonNullable { get; set; } 141 | public List Args { get; set; } 142 | public string Description { get; set; } 143 | public string DescriptionForComment(int indent = 8) 144 | { 145 | return string.Join("\n", Description.Split("\n").Select(l => l.Trim()).Where(l => l.Count() > 0).Select(l => $"/// {l}".PadLeft(indent + l.Length + 4))) + "\n"; 146 | } 147 | 148 | public string DotNetName => Name[0].ToString().ToUpper() + string.Join("", Name.Skip(1)); 149 | public string DotNetType 150 | { 151 | get 152 | { 153 | var t = DotNetTypeSingle; 154 | if (IsNonNullable) 155 | t = t.Trim('?'); 156 | else if (!t.EndsWith('?') && schemaInfo.IsEnum(t)) 157 | t = t + "?"; 158 | return IsArray ? $"List<{t}>" : t; 159 | } 160 | } 161 | public string DotNetTypeSingle 162 | { 163 | get 164 | { 165 | if (!schemaInfo.HasDotNetType(TypeName)) 166 | { 167 | if (schemaInfo.Scalars.Contains(TypeName)) 168 | return TypeName; 169 | throw new SchemaException($"Unknown dotnet type for schema type '{TypeName}'. Please provide a mapping for any custom scalar types defined in the schema"); 170 | } 171 | return schemaInfo.GetDotNetType(TypeName); 172 | } 173 | } 174 | 175 | public bool ShouldBeProperty 176 | { 177 | get 178 | { 179 | return (Args.Count == 0 && !schemaInfo.Types.ContainsKey(TypeName) && !schemaInfo.Inputs.ContainsKey(TypeName)) || schemaInfo.Scalars.Contains(TypeName); 180 | } 181 | } 182 | 183 | /// 184 | /// Outputs the method signature with all arguments and a object selection argument if applicable 185 | /// 186 | /// 187 | public string OutputMethodSignature(bool onlyRequiredArgs, bool withSelection) 188 | { 189 | // if we are not outputing withSelection and have only required args and return a 190 | // scalar, skip it otherwise we'll have duplicate method 191 | if (!withSelection && IsScalar && Args.All(a => a.Required)) 192 | return null; 193 | 194 | var typeName = !withSelection ? DotNetTypeSingle : "TReturn"; 195 | 196 | var sb = new StringBuilder(" public abstract "); 197 | sb.Append(IsScalar ? $"{DotNetType} " : IsArray ? $"List<{typeName}> " : $"{typeName} "); 198 | sb.Append(DotNetName).Append(IsScalar || !withSelection ? "(" : $"<{typeName}>("); 199 | var argsOut = ArgsOutput(onlyRequiredArgs); 200 | sb.Append(argsOut); 201 | if (withSelection && !IsScalar) 202 | { 203 | if (argsOut.Length > 0) 204 | sb.Append(", "); 205 | sb.Append($"Expression> selection"); 206 | } 207 | sb.Insert(0, BuildCommentDoc(withSelection)); 208 | 209 | sb.AppendLine($");"); 210 | 211 | return sb.ToString(); 212 | } 213 | 214 | private string BuildCommentDoc(bool withSelection) 215 | { 216 | var sb = new StringBuilder(); 217 | sb.AppendLine(" /// "); 218 | if (Description != null) 219 | sb.Append($"{DescriptionForComment(8)}"); 220 | if (!IsScalar && !withSelection) 221 | { 222 | if (Description != null) 223 | sb.AppendLine($" ///"); 224 | sb.AppendLine($" /// This shortcut will return a selection of all fields"); 225 | } 226 | sb.AppendLine(" /// "); 227 | if (!IsScalar && withSelection) 228 | sb.AppendLine(@" /// Projection of fields to select from the object"); 229 | sb.AppendLine($" [GqlFieldName(\"{Name}\")]"); 230 | return sb.ToString(); 231 | } 232 | 233 | public string ArgsOutput(bool onlyRequiredArgs) 234 | { 235 | if (!Args.Any()) 236 | return ""; 237 | var result = string.Join(", ", Args.Where(a => !onlyRequiredArgs || a.Required).Select(a => $"{(a.Required ? a.DotNetType.Trim('?') : a.DotNetType)} {a.Name}")); 238 | return result; 239 | } 240 | 241 | public override string ToString() 242 | { 243 | return $"{Name}:{(IsArray ? '[' + TypeName + ']': TypeName)}"; 244 | } 245 | } 246 | 247 | public class Arg : Field 248 | { 249 | public Arg(SchemaInfo schemaInfo) : base(schemaInfo) 250 | { 251 | } 252 | 253 | public bool Required { get; set; } 254 | 255 | public override string Name 256 | { 257 | get => CSharpKeywords.EscapeIdentifier(base.Name); 258 | set => base.Name = value; 259 | } 260 | } 261 | } -------------------------------------------------------------------------------- /src/tests/DotNetGraphQLQueryGen.Tests/Generated/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 9/5/20 1:58:19 pm from ../tests/DotNetGraphQLQueryGen.Tests/schema.graphql -n Generated -c TestHttpClient -m Date=DateTime 11 | /// 12 | 13 | namespace Generated 14 | { 15 | 16 | public enum Sex { 17 | [GqlFieldName("Female")] 18 | Female, 19 | [GqlFieldName("Male")] 20 | Male, 21 | } 22 | 23 | public interface RootQuery 24 | { 25 | /// 26 | /// Returns list of actors paged 27 | /// @param page Page number to return [defaults: page = 1] 28 | /// @param pagesize Number of items per page to return [pagesize = 10] 29 | /// @param search Optional search string 30 | /// 31 | /// This shortcut will return a selection of all fields 32 | /// 33 | [GqlFieldName("actorPager")] 34 | PersonPagination ActorPager(); 35 | /// 36 | /// Returns list of actors paged 37 | /// @param page Page number to return [defaults: page = 1] 38 | /// @param pagesize Number of items per page to return [pagesize = 10] 39 | /// @param search Optional search string 40 | /// 41 | /// Projection of fields to select from the object 42 | [GqlFieldName("actorPager")] 43 | TReturn ActorPager(int? page, int? pagesize, string search, Expression> selection); 44 | /// 45 | /// List of actors 46 | /// 47 | /// This shortcut will return a selection of all fields 48 | /// 49 | [GqlFieldName("actors")] 50 | List Actors(); 51 | /// 52 | /// List of actors 53 | /// 54 | /// Projection of fields to select from the object 55 | [GqlFieldName("actors")] 56 | List Actors(Expression> selection); 57 | /// 58 | /// List of directors 59 | /// 60 | /// This shortcut will return a selection of all fields 61 | /// 62 | [GqlFieldName("directors")] 63 | List Directors(); 64 | /// 65 | /// List of directors 66 | /// 67 | /// Projection of fields to select from the object 68 | [GqlFieldName("directors")] 69 | List Directors(Expression> selection); 70 | /// 71 | /// Return a Movie by its Id 72 | /// 73 | /// This shortcut will return a selection of all fields 74 | /// 75 | [GqlFieldName("movie")] 76 | Movie Movie(int id); 77 | /// 78 | /// Return a Movie by its Id 79 | /// 80 | /// Projection of fields to select from the object 81 | [GqlFieldName("movie")] 82 | TReturn Movie(int id, Expression> selection); 83 | /// 84 | /// Collection of Movies 85 | /// 86 | /// This shortcut will return a selection of all fields 87 | /// 88 | [GqlFieldName("movies")] 89 | List Movies(); 90 | /// 91 | /// Collection of Movies 92 | /// 93 | /// Projection of fields to select from the object 94 | [GqlFieldName("movies")] 95 | List Movies(Expression> selection); 96 | /// 97 | /// This shortcut will return a selection of all fields 98 | /// 99 | [GqlFieldName("moviesByIds")] 100 | List MoviesByIds(List ids); 101 | /// 102 | /// 103 | /// Projection of fields to select from the object 104 | [GqlFieldName("moviesByIds")] 105 | List MoviesByIds(List ids, Expression> selection); 106 | /// 107 | /// Collection of Peoples 108 | /// 109 | /// This shortcut will return a selection of all fields 110 | /// 111 | [GqlFieldName("people")] 112 | List People(); 113 | /// 114 | /// Collection of Peoples 115 | /// 116 | /// Projection of fields to select from the object 117 | [GqlFieldName("people")] 118 | List People(Expression> selection); 119 | /// 120 | /// Return a Person by its Id 121 | /// 122 | /// This shortcut will return a selection of all fields 123 | /// 124 | [GqlFieldName("person")] 125 | Person Person(int id); 126 | /// 127 | /// Return a Person by its Id 128 | /// 129 | /// Projection of fields to select from the object 130 | [GqlFieldName("person")] 131 | TReturn Person(int id, Expression> selection); 132 | /// 133 | /// List of writers 134 | /// 135 | /// This shortcut will return a selection of all fields 136 | /// 137 | [GqlFieldName("writers")] 138 | List Writers(); 139 | /// 140 | /// List of writers 141 | /// 142 | /// Projection of fields to select from the object 143 | [GqlFieldName("writers")] 144 | List Writers(Expression> selection); 145 | /// 146 | /// List of producers with filter support 147 | /// 148 | /// This shortcut will return a selection of all fields 149 | /// 150 | [GqlFieldName("producers")] 151 | List Producers(); 152 | /// 153 | /// List of producers with filter support 154 | /// 155 | /// Projection of fields to select from the object 156 | [GqlFieldName("producers")] 157 | List Producers(FilterBy filter, Expression> selection); 158 | /// 159 | /// Testing returning a scalar 160 | /// 161 | [GqlFieldName("getDisplayName")] 162 | string GetDisplayName(int id); 163 | /// 164 | /// 165 | [GqlFieldName("deleteUser")] 166 | int? DeleteUser(int id); 167 | } 168 | public interface SubscriptionType 169 | { 170 | [GqlFieldName("name")] 171 | string Name { get; } 172 | } 173 | /// 174 | /// This is a movie entity 175 | /// 176 | public interface Movie 177 | { 178 | [GqlFieldName("id")] 179 | int Id { get; } 180 | [GqlFieldName("name")] 181 | string Name { get; } 182 | /// 183 | /// Enum of Genre 184 | /// 185 | [GqlFieldName("genre")] 186 | int Genre { get; } 187 | [GqlFieldName("released")] 188 | DateTime Released { get; } 189 | /// 190 | /// Actors in the movie 191 | /// 192 | /// This shortcut will return a selection of all fields 193 | /// 194 | [GqlFieldName("actors")] 195 | List Actors(); 196 | /// 197 | /// Actors in the movie 198 | /// 199 | /// Projection of fields to select from the object 200 | [GqlFieldName("actors")] 201 | List Actors(Expression> selection); 202 | /// 203 | /// Writers in the movie 204 | /// 205 | /// This shortcut will return a selection of all fields 206 | /// 207 | [GqlFieldName("writers")] 208 | List Writers(); 209 | /// 210 | /// Writers in the movie 211 | /// 212 | /// Projection of fields to select from the object 213 | [GqlFieldName("writers")] 214 | List Writers(Expression> selection); 215 | /// 216 | /// This shortcut will return a selection of all fields 217 | /// 218 | [GqlFieldName("director")] 219 | Person Director(); 220 | /// 221 | /// 222 | /// Projection of fields to select from the object 223 | [GqlFieldName("director")] 224 | TReturn Director(Expression> selection); 225 | [GqlFieldName("directorId")] 226 | int DirectorId { get; } 227 | [GqlFieldName("rating")] 228 | double? Rating { get; } 229 | /// 230 | /// Just testing using gql schema keywords here 231 | /// 232 | [GqlFieldName("type")] 233 | int? Type { get; } 234 | } 235 | public interface Actor 236 | { 237 | [GqlFieldName("personId")] 238 | int PersonId { get; } 239 | /// 240 | /// This shortcut will return a selection of all fields 241 | /// 242 | [GqlFieldName("person")] 243 | Person Person(); 244 | /// 245 | /// 246 | /// Projection of fields to select from the object 247 | [GqlFieldName("person")] 248 | TReturn Person(Expression> selection); 249 | [GqlFieldName("movieId")] 250 | int MovieId { get; } 251 | /// 252 | /// This shortcut will return a selection of all fields 253 | /// 254 | [GqlFieldName("movie")] 255 | Movie Movie(); 256 | /// 257 | /// 258 | /// Projection of fields to select from the object 259 | [GqlFieldName("movie")] 260 | TReturn Movie(Expression> selection); 261 | } 262 | public interface Person 263 | { 264 | [GqlFieldName("id")] 265 | int Id { get; } 266 | [GqlFieldName("firstName")] 267 | string FirstName { get; } 268 | [GqlFieldName("lastName")] 269 | string LastName { get; } 270 | /// 271 | /// Movies they acted in 272 | /// 273 | /// This shortcut will return a selection of all fields 274 | /// 275 | [GqlFieldName("actorIn")] 276 | List ActorIn(); 277 | /// 278 | /// Movies they acted in 279 | /// 280 | /// Projection of fields to select from the object 281 | [GqlFieldName("actorIn")] 282 | List ActorIn(Expression> selection); 283 | /// 284 | /// Movies they wrote 285 | /// 286 | /// This shortcut will return a selection of all fields 287 | /// 288 | [GqlFieldName("writerOf")] 289 | List WriterOf(); 290 | /// 291 | /// Movies they wrote 292 | /// 293 | /// Projection of fields to select from the object 294 | [GqlFieldName("writerOf")] 295 | List WriterOf(Expression> selection); 296 | /// 297 | /// This shortcut will return a selection of all fields 298 | /// 299 | [GqlFieldName("directorOf")] 300 | List DirectorOf(); 301 | /// 302 | /// 303 | /// Projection of fields to select from the object 304 | [GqlFieldName("directorOf")] 305 | List DirectorOf(Expression> selection); 306 | [GqlFieldName("died")] 307 | DateTime Died { get; } 308 | [GqlFieldName("isDeleted")] 309 | bool IsDeleted { get; } 310 | /// 311 | /// Show the person's age 312 | /// 313 | [GqlFieldName("age")] 314 | int Age { get; } 315 | /// 316 | /// Person's name 317 | /// 318 | [GqlFieldName("name")] 319 | string Name { get; } 320 | [GqlFieldName("dob")] 321 | DateTime Dob { get; } 322 | } 323 | public interface Writer 324 | { 325 | [GqlFieldName("personId")] 326 | int PersonId { get; } 327 | /// 328 | /// This shortcut will return a selection of all fields 329 | /// 330 | [GqlFieldName("person")] 331 | Person Person(); 332 | /// 333 | /// 334 | /// Projection of fields to select from the object 335 | [GqlFieldName("person")] 336 | TReturn Person(Expression> selection); 337 | [GqlFieldName("movieId")] 338 | int MovieId { get; } 339 | /// 340 | /// This shortcut will return a selection of all fields 341 | /// 342 | [GqlFieldName("movie")] 343 | Movie Movie(); 344 | /// 345 | /// 346 | /// Projection of fields to select from the object 347 | [GqlFieldName("movie")] 348 | TReturn Movie(Expression> selection); 349 | } 350 | public interface PersonPagination 351 | { 352 | /// 353 | /// total records to match search 354 | /// 355 | [GqlFieldName("total")] 356 | int Total { get; } 357 | /// 358 | /// total pages based on page size 359 | /// 360 | [GqlFieldName("pageCount")] 361 | int PageCount { get; } 362 | /// 363 | /// collection of people 364 | /// 365 | /// This shortcut will return a selection of all fields 366 | /// 367 | [GqlFieldName("people")] 368 | List People(); 369 | /// 370 | /// collection of people 371 | /// 372 | /// Projection of fields to select from the object 373 | [GqlFieldName("people")] 374 | List People(Expression> selection); 375 | } 376 | public interface Mutation 377 | { 378 | /// 379 | /// Add a new Movie object 380 | /// 381 | /// Projection of fields to select from the object 382 | [GqlFieldName("addMovie")] 383 | TReturn AddMovie(string name, double? rating, List details, int? genre, DateTime released, Expression> selection); 384 | /// 385 | /// 386 | /// Projection of fields to select from the object 387 | [GqlFieldName("addActor")] 388 | TReturn AddActor(string firstName, string lastName, int? movieId, Expression> selection); 389 | /// 390 | /// 391 | /// Projection of fields to select from the object 392 | [GqlFieldName("addActor2")] 393 | TReturn AddActor2(string firstName, string lastName, int? movieId, Expression> selection); 394 | } 395 | public class FilterBy 396 | { 397 | [GqlFieldName("field")] 398 | public string Field { get; set; } 399 | [GqlFieldName("value")] 400 | public string Value { get; set; } 401 | } 402 | public class Detail 403 | { 404 | [GqlFieldName("description")] 405 | public string Description { get; set; } 406 | [GqlFieldName("someNumber")] 407 | public int? SomeNumber { get; set; } 408 | } 409 | 410 | } -------------------------------------------------------------------------------- /src/dotnet-gqlgen/BaseGraphQLClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | using System.Reflection; 7 | using System.Text; 8 | using Newtonsoft.Json; 9 | 10 | namespace DotNetGqlClient 11 | { 12 | public abstract class BaseGraphQLClient 13 | { 14 | private uint argNum = 0; 15 | protected Dictionary typeMappings; 16 | 17 | private class MemberInitDictionary : Dictionary { } 18 | 19 | protected QueryRequest MakeQuery(Expression> query, string operationName = null, bool mutation = false) 20 | { 21 | argNum = 0; 22 | var args = new List(); 23 | var gql = new StringBuilder(); 24 | var variables = new Dictionary(); 25 | 26 | if (query.NodeType != ExpressionType.Lambda) 27 | throw new ArgumentException($"Must provide a LambdaExpression", "query"); 28 | var lambda = (LambdaExpression)query; 29 | 30 | if (lambda.Body.NodeType != ExpressionType.New && lambda.Body.NodeType != ExpressionType.MemberInit) 31 | throw new ArgumentException($"LambdaExpression must return a NewExpression or MemberInitExpression"); 32 | 33 | foreach (var selections in GetObjectSelection(lambda.Body, args, variables)) 34 | { 35 | gql.AppendLine(selections); 36 | } 37 | 38 | // prepend operationname as it may have arguments 39 | operationName = string.IsNullOrEmpty(operationName) ? "GraphQLClient" : operationName; 40 | gql.Insert(0, $"{(mutation ? "mutation" : "query")} {operationName} {(args.Any() ? $"({string.Join(",", args)})" : "")} {{\n"); 41 | 42 | gql.Append(@"}"); 43 | return new QueryRequest 44 | { 45 | Query = gql.ToString(), 46 | Variables = variables, 47 | OperationName = operationName, 48 | }; 49 | } 50 | 51 | private IEnumerable GetObjectSelection(Expression exp, List args, Dictionary variables) 52 | { 53 | if (exp.NodeType == ExpressionType.New) 54 | { 55 | var newExp = (NewExpression)exp; 56 | for (int i = 0; i < newExp.Arguments.Count; i++) 57 | { 58 | var fieldVal = newExp.Arguments[i]; 59 | var fieldProp = newExp.Members[i]; 60 | yield return $"{fieldProp.Name}: {GetFieldSelection(fieldVal, args, variables)}"; 61 | } 62 | } 63 | else if (exp.NodeType == ExpressionType.MemberInit) 64 | { 65 | var mi = (MemberInitExpression)exp; 66 | for (int i = 0; i < mi.Bindings.Count; i++) 67 | { 68 | var valExp = ((MemberAssignment)mi.Bindings[i]).Expression; 69 | var fieldVal = mi.Bindings[i].Member; 70 | yield return $"{fieldVal.Name}: {GetFieldSelection(valExp, args, variables)}"; 71 | } 72 | } 73 | else 74 | { 75 | throw new ArgumentException($"Selection {exp.NodeType} \"{exp}\" must be a NewExpression or MemberInitExpression"); 76 | } 77 | } 78 | 79 | private string GetFieldSelection(Expression field, List args, Dictionary variables) 80 | { 81 | if (field.NodeType == ExpressionType.MemberAccess) 82 | { 83 | var memberExp = (MemberExpression)field; 84 | // we only support 1 level field selection as we are just generating gql not doing post processing 85 | // e.g. client.MakeQuery(q => new 86 | // { 87 | // Movie = q.Movies(s => new 88 | // { 89 | // s.Rating.Value, 90 | // s.Director().Died 91 | // }), 92 | // }); 93 | // both of those selections are invalid. You just selection s.Rating and the return value type is float? 94 | // and for the director died date you select it like gql s.Director(d => new { d.Died }) 95 | // TODO we could generate s.Director().Died into the line above 96 | if (memberExp.Expression.NodeType != ExpressionType.Parameter) 97 | 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"); 98 | var member = memberExp.Member; 99 | var attribute = member.GetCustomAttributes(typeof(GqlFieldNameAttribute)).Cast().FirstOrDefault(); 100 | if (attribute != null) 101 | return attribute.Name; 102 | return member.Name; 103 | } 104 | else if (field.NodeType == ExpressionType.Call) 105 | { 106 | var call = (MethodCallExpression)field; 107 | return GetSelectionFromMethod(call, args, variables); 108 | } 109 | else if (field.NodeType == ExpressionType.Quote) 110 | { 111 | return GetFieldSelection(((UnaryExpression)field).Operand, args, variables); 112 | } 113 | else 114 | { 115 | throw new ArgumentException($"Field expression should be a call or member access expression", "field"); 116 | } 117 | } 118 | 119 | private string GetSelectionFromMethod(MethodCallExpression call, List operationArgs, Dictionary variables) 120 | { 121 | var select = new StringBuilder(); 122 | 123 | var attribute = call.Method.GetCustomAttributes(typeof(GqlFieldNameAttribute)).Cast().FirstOrDefault(); 124 | if (attribute != null) 125 | select.Append(attribute.Name); 126 | else 127 | select.Append(call.Method.Name); 128 | 129 | 130 | IEnumerable selection; 131 | if (call.Arguments.Count == 0) 132 | { 133 | selection = (call.Method.ReturnType.IsEnumerableOrArray() 134 | ? GetDefaultSelection(call.Method.ReturnType.GetGenericArguments().First()) 135 | : GetDefaultSelection(call.Method.ReturnType)).ToList(); 136 | } 137 | else 138 | { 139 | var selectorExp = call.Arguments.Last(); 140 | 141 | if (selectorExp.NodeType == ExpressionType.Quote) 142 | selectorExp = ((UnaryExpression)selectorExp).Operand; 143 | 144 | if (selectorExp.NodeType == ExpressionType.Lambda) 145 | selectorExp = ((LambdaExpression)selectorExp).Body; 146 | else 147 | selectorExp = null; // supposed scalar awaited as return 148 | 149 | 150 | var argVals = new List(); 151 | var parameters = call.Method.GetParameters(); 152 | var arguments = call.Arguments; 153 | 154 | var count = selectorExp != null 155 | ? arguments.Count - 1 156 | : arguments.Count; 157 | 158 | for (int i = 0; i < count; i++) 159 | { 160 | var arg = call.Arguments[i]; 161 | var param = parameters.ElementAt(i); 162 | 163 | (object argVal, Type argType) = ArgToTypeAndValue(arg); 164 | if (argVal == null) 165 | continue; 166 | 167 | argVals.Add($"{param.Name}: {ArgValToString(operationArgs, variables, param, argType, argVal)}"); 168 | } 169 | 170 | if (argVals.Count > 0) 171 | select.Append($"({string.Join(", ", argVals)})"); 172 | 173 | selection = selectorExp != null 174 | ? GetObjectSelection(selectorExp, operationArgs, variables) 175 | : Enumerable.Empty(); 176 | } 177 | 178 | var selectionArray = selection.ToArray(); 179 | if (selectionArray.Length > 0) 180 | { 181 | select.Append(" {" + Environment.NewLine); 182 | foreach (var line in selectionArray) 183 | { 184 | select.AppendLine(line); 185 | } 186 | select.Append("}"); 187 | } 188 | 189 | return select.ToString(); 190 | } 191 | 192 | private (object argVal, Type argType) ArgToTypeAndValue(Expression arg) 193 | { 194 | Type argType; 195 | object argVal; 196 | 197 | switch (arg.NodeType) 198 | { 199 | case ExpressionType.Convert: 200 | return ArgToTypeAndValue(((UnaryExpression)arg).Operand); 201 | case ExpressionType.Constant: 202 | var constArg = (ConstantExpression)arg; 203 | argType = constArg.Type; 204 | argVal = constArg.Value; 205 | if (argType.IsEnum) 206 | { 207 | var attr = arg.Type.GetField(Enum.GetName(arg.Type, constArg.Value)).GetCustomAttributes().FirstOrDefault(); 208 | argVal = attr?.Name ?? argVal; 209 | } 210 | break; 211 | case ExpressionType.MemberAccess when ((MemberExpression)arg).Expression is ConstantExpression ce: 212 | var mac = (MemberExpression)arg; 213 | argType = mac.Type; 214 | argVal = mac.Member.MemberType == MemberTypes.Field 215 | ? ((FieldInfo)mac.Member).GetValue(ce.Value) 216 | : ((PropertyInfo)mac.Member).GetValue(ce.Value); 217 | break; 218 | case ExpressionType.MemberAccess when ((MemberExpression)arg).Expression.GetType().Name == "FieldExpression": 219 | var maf = (MemberExpression)arg; 220 | var fieldName = maf.Member.Name; 221 | (object fieldVal, Type fieldType) = ArgToTypeAndValue(maf.Expression); 222 | if (maf.Member.MemberType == MemberTypes.Field) 223 | { 224 | FieldInfo fi = fieldType.GetField(fieldName); 225 | argType = fi.FieldType; 226 | argVal = fi.GetValue(fieldVal); 227 | } 228 | else 229 | { 230 | PropertyInfo pi = fieldType.GetProperty(fieldName); 231 | argType = pi.PropertyType; 232 | argVal = pi.GetValue(fieldVal); 233 | } 234 | break; 235 | case ExpressionType.MemberInit: 236 | var memberInitDict = new MemberInitDictionary(); 237 | foreach (var binding in ((MemberInitExpression)arg).Bindings) 238 | { 239 | var attr = binding.Member.GetCustomAttributes().FirstOrDefault(); 240 | var name = attr?.Name ?? binding.Member.Name; 241 | var expr = ((MemberAssignment)binding).Expression; 242 | memberInitDict.Add(name, ArgToTypeAndValue(expr)); 243 | } 244 | argVal = memberInitDict; 245 | argType = argVal.GetType(); 246 | break; 247 | case ExpressionType.New: 248 | case ExpressionType.NewArrayInit: 249 | case ExpressionType.ListInit: 250 | argVal = Expression.Lambda(arg).Compile().DynamicInvoke(); 251 | argType = argVal.GetType(); 252 | break; 253 | default: 254 | throw new Exception($"Unsupported argument type {arg.NodeType}"); 255 | } 256 | 257 | return (argVal, argType); 258 | } 259 | 260 | private string ArgValToString(List operationArgs, Dictionary variables, ParameterInfo param, Type argType, object val) 261 | { 262 | var type = Nullable.GetUnderlyingType(argType) ?? argType; 263 | switch (Type.GetTypeCode(type)) 264 | { 265 | case TypeCode.DateTime: return $"\"{(DateTime)val:o}\""; 266 | case TypeCode.String: return $"\"{val}\""; 267 | case TypeCode.Double: return ((double)val).ToString(CultureInfo.InvariantCulture); 268 | case TypeCode.Decimal: return ((decimal)val).ToString(CultureInfo.InvariantCulture); 269 | case TypeCode.Single: return ((float)val).ToString(CultureInfo.InvariantCulture); 270 | case TypeCode.Boolean: return val.ToString().ToLower(); 271 | default: 272 | if (type == typeof(Guid)) 273 | { 274 | return $"\"{val}\""; 275 | } 276 | if (type == typeof(MemberInitDictionary)) 277 | { 278 | var memberInitDict = (MemberInitDictionary)val; 279 | var memberFields = memberInitDict.Select((a) => $"{a.Key}: {ArgValToString(operationArgs, variables, param, a.Value.Item2, a.Value.Item1)}"); 280 | return $"{{ {string.Join(", ", memberFields)} }}"; 281 | } 282 | if (type.IsEnumerableOrArray()) 283 | { 284 | var argName = $"a{argNum++}"; 285 | var arrType = argType.GetEnumerableOrArrayType(); 286 | var isNullable = arrType.IsNullableType(); 287 | if (isNullable) 288 | arrType = arrType.GetGenericArguments()[0]; 289 | if (!typeMappings.ContainsKey(arrType.Name)) 290 | throw new ArgumentException($"Can't find GQL type for Dotnet type '{arrType.Name}'"); 291 | operationArgs.Add($"${argName}: [{(isNullable ? typeMappings[arrType.Name].Trim('!') : typeMappings[arrType.Name])}]"); 292 | variables.Add($"{argName}", val); 293 | return $"${argName}"; 294 | } 295 | return val.ToString(); 296 | } 297 | } 298 | 299 | private static IEnumerable GetDefaultSelection(Type returnType) 300 | { 301 | foreach (var field in returnType.GetProperties()) 302 | { 303 | var name = field.Name; 304 | var attribute = field.GetCustomAttributes(typeof(GqlFieldNameAttribute)).Cast().FirstOrDefault(); 305 | if (attribute != null) 306 | name = attribute.Name; 307 | 308 | yield return $"{field.Name}: {name}"; 309 | } 310 | } 311 | 312 | } 313 | 314 | public class QueryRequest 315 | { 316 | // Name of the query or mutation you want to run in the Query (if it contains many) 317 | [JsonProperty("operationName")] 318 | public string OperationName { get; set; } 319 | /// 320 | /// GraphQL query document 321 | /// 322 | /// 323 | [JsonProperty("query")] 324 | public string Query { get; set; } 325 | [JsonProperty("variables")] 326 | public Dictionary Variables { get; set; } 327 | } 328 | 329 | public static class TypeExtensions 330 | { 331 | /// 332 | /// Returns true if this type is an Enumerable<> or an array 333 | /// 334 | /// 335 | /// 336 | public static bool IsEnumerableOrArray(this Type source) 337 | { 338 | if (source == typeof(string) || source == typeof(byte[])) 339 | return false; 340 | 341 | if (source.GetTypeInfo().IsArray) 342 | { 343 | return true; 344 | } 345 | var isEnumerable = false; 346 | if (source.GetTypeInfo().IsGenericType && !source.IsNullableType()) 347 | { 348 | isEnumerable = IsGenericTypeEnumerable(source); 349 | } 350 | return isEnumerable; 351 | } 352 | 353 | private static bool IsGenericTypeEnumerable(Type source) 354 | { 355 | bool isEnumerable = (source.GetTypeInfo().IsGenericType && source.GetGenericTypeDefinition() == typeof(IEnumerable<>) || source.GetTypeInfo().IsGenericType && source.GetGenericTypeDefinition() == typeof(IQueryable<>)); 356 | if (!isEnumerable) 357 | { 358 | foreach (var intType in source.GetInterfaces()) 359 | { 360 | isEnumerable = IsGenericTypeEnumerable(intType); 361 | if (isEnumerable) 362 | break; 363 | } 364 | } 365 | 366 | return isEnumerable; 367 | } 368 | 369 | /// 370 | /// Return the array element type or the generic type for a IEnumerable 371 | /// Specifically does not treat string as IEnumerable and will not return byte for byte[] 372 | /// 373 | /// 374 | /// 375 | public static Type GetEnumerableOrArrayType(this Type type) 376 | { 377 | if (type == typeof(string) || type == typeof(byte[]) || type == typeof(byte)) 378 | { 379 | return null; 380 | } 381 | if (type.IsArray) 382 | { 383 | return type.GetElementType(); 384 | } 385 | if (type.GetTypeInfo().IsGenericType && ( 386 | type.GetGenericTypeDefinition() == typeof(IEnumerable<>) || 387 | type.GetGenericTypeDefinition() == typeof(IList<>) || 388 | type.GetGenericTypeDefinition() == typeof(IReadOnlyList<>) || 389 | type.GetGenericTypeDefinition() == typeof(List<>) || 390 | type.GetGenericTypeDefinition() == typeof(ICollection<>) || 391 | type.GetGenericTypeDefinition() == typeof(IReadOnlyCollection<>))) 392 | { 393 | return type.GetGenericArguments()[0]; 394 | } 395 | foreach (var intType in type.GetInterfaces()) 396 | { 397 | if (intType.IsEnumerableOrArray()) 398 | { 399 | return intType.GetEnumerableOrArrayType(); 400 | } 401 | } 402 | return null; 403 | } 404 | 405 | public static bool IsNullableType(this Type t) 406 | { 407 | return t.GetTypeInfo().IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>); 408 | } 409 | } 410 | } 411 | --------------------------------------------------------------------------------