├── .editorconfig
├── .gitignore
├── Directory.Build.props
├── GraphQlClientGenerator.sln
├── GraphQlLogo.png
├── License.md
├── README.md
├── src
├── GraphQlClientGenerator.Console
│ ├── Commands.cs
│ ├── GraphQlCSharpFileHelper.cs
│ ├── GraphQlClientGenerator.Console.csproj
│ ├── Program.cs
│ └── ProgramOptions.cs
└── GraphQlClientGenerator
│ ├── BaseClasses.cs
│ ├── CSharpHelper.cs
│ ├── Extensions.cs
│ ├── GenerationContext.cs
│ ├── GraphQlClientGenerator.csproj
│ ├── GraphQlClientSourceGenerator.cs
│ ├── GraphQlGenerator.cs
│ ├── GraphQlGeneratorConfiguration.cs
│ ├── GraphQlIntrospection.cs
│ ├── GraphQlIntrospectionSchema.cs
│ ├── GraphQlTypeKind.cs
│ ├── ICodeFileEmitter.cs
│ ├── IScalarFieldTypeMappingProvider.cs
│ ├── KeyValueParameterParser.cs
│ ├── MultipleFileGenerationContext.cs
│ ├── NamingHelper.cs
│ ├── OutputType.cs
│ ├── Properties
│ └── AssemblyInfo.cs
│ ├── RegexScalarFieldTypeMappingProvider.cs
│ └── SingleFileGenerationContext.cs
└── test
└── GraphQlClientGenerator.Test
├── CompilationHelper.cs
├── ExpectedMultipleFilesContext
├── Avatar
├── Avatar.FileScoped
├── Home
├── Home.FileScoped
├── IncludeDirective
├── IncludeDirective.FileScoped
├── MutationQueryBuilder
└── MutationQueryBuilder.FileScoped
├── GlobalFixture.cs
├── GraphQlClientGenerator.Test.csproj
├── GraphQlClientSourceGeneratorTest.cs
├── GraphQlGeneratorTest.cs
├── GraphQlIntrospectionSchemaTest.cs
├── NamingHelperTest.cs
├── RegexCustomScalarFieldTypeMappingRules
├── TestSchemas
├── TestSchema
├── TestSchema2
├── TestSchema3
├── TestSchemaWithDeprecatedFields
├── TestSchemaWithNestedListsOfComplexObjects
└── TestSchemaWithUnions
└── VerifierExpectations
├── GraphQlClientSourceGeneratorTest.SourceGenerationWithRegexCustomScalarFieldTypeMappingProvider.verified.txt
├── GraphQlClientSourceGeneratorTest.SourceGeneration_scalarFieldTypeMappingProviderTypeName=False.verified.txt
├── GraphQlClientSourceGeneratorTest.SourceGeneration_scalarFieldTypeMappingProviderTypeName=True.verified.txt
├── GraphQlGeneratorTest.DeprecatedAttributes.verified.txt
├── GraphQlGeneratorTest.GenerateDataClasses.verified.txt
├── GraphQlGeneratorTest.GenerateDataClassesWithTypeConfiguration_csharpVersion=CSharp12.verified.txt
├── GraphQlGeneratorTest.GenerateDataClassesWithTypeConfiguration_csharpVersion=Compatible.verified.txt
├── GraphQlGeneratorTest.GenerateFormatMasks.verified.txt
├── GraphQlGeneratorTest.GenerateFullClientCSharpFile.verified.txt
├── GraphQlGeneratorTest.GenerateQueryBuilder.verified.txt
├── GraphQlGeneratorTest.NewCSharpSyntaxWithClassPrefixAndSuffix.verified.txt
├── GraphQlGeneratorTest.WithNestedListsOfComplexObjects.verified.txt
├── GraphQlGeneratorTest.WithNullableReferencesAndPropertyNullabilityBySchema.verified.txt
└── GraphQlGeneratorTest.WithUnions.verified.txt
/.editorconfig:
--------------------------------------------------------------------------------
1 | ; Top-most EditorConfig file
2 | root = true
3 |
4 | [*.cs]
5 | indent_style = space
6 | indent_size = 4
7 | max_line_length = 220
8 | trim_trailing_whitespace = true
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ##############################################################################
2 | # General
3 | ##############################################################################
4 |
5 | # OS junk files
6 | [Tt]humbs.db
7 | *.DS_Store
8 |
9 | config.json
10 | project.lock.json
11 |
12 | # Visual Studio 2015 cache/options directory
13 | .vs/
14 |
15 | # Visual Studio / MonoDevelop
16 | *.[Oo]bj
17 | *.exe
18 | *.dll
19 | *.pdb
20 | *.user
21 | *.aps
22 | *.pch
23 | *.vspscc
24 | *.vssscc
25 | *_i.c
26 | *_p.c
27 | *.ncb
28 | *.suo
29 | *.tlb
30 | *.tlh
31 | *.bak
32 | *.ilk
33 | *.log
34 | *.lib
35 | *.sbr
36 | *.sdf
37 | *.opensdf
38 | *.resources
39 | *.res
40 | ipch/
41 | obj/
42 | [Bb]in
43 | [Dd]ebug*/
44 | [Rr]elease*/
45 | Ankh.NoLoad
46 | *.gpState
47 | *.received.*
48 |
49 | # Tooling
50 | _ReSharper*/
51 | *.resharper
52 | [Tt]est[Rr]esult*
53 | *.orig
54 | *.rej
55 |
56 | # NuGet packages
57 | !.nuget/*
58 | [Pp]ackages/*
59 | ![Pp]ackages/repositories.config
60 |
61 | # Temporary Files
62 | ~.*
63 | ~$*
64 |
65 | docker-compose.yml
66 |
67 | # Autotools-generated files
68 | /Makefile
69 | Makefile.in
70 | aclocal.m4
71 | autom4te.cache
72 | /build/
73 | config.cache
74 | config.guess
75 | config.h
76 | config.h.in
77 | config.log
78 | config.status
79 | config.sub
80 | configure
81 | configure.scan
82 | cygconfig.h
83 | depcomp
84 | install-sh
85 | libtool
86 | ltmain.sh
87 | missing
88 | mkinstalldirs
89 | releases
90 | stamp-h
91 | stamp-h1
92 | stamp-h.in
93 | /test-driver
94 | *~
95 | *.swp
96 | *.o
97 | .deps
98 |
99 | # Libtool
100 | libtool.m4
101 | lt~obsolete.m4
102 | ltoptions.m4
103 | ltsugar.m4
104 | ltversion.m4
105 |
106 | # Dolt (libtool replacement)
107 | doltlibtool
108 | doltcompile
109 |
110 | # pkg-config
111 | *.pc
112 |
113 | # Emacs
114 | semantic.cache
115 |
116 | # gtags
117 | GPATH
118 | GRTAGS
119 | GSYMS
120 | GTAGS
121 |
122 | # Doxygen
123 | docs/doxygen*
124 | docs/perlmod*
125 |
126 |
127 | ##############################################################################
128 | # Mono-specific patterns
129 | ##############################################################################
130 |
131 | .dirstamp
132 | compile
133 | mono.h
134 | mono-*.tar.*
135 | tmpinst-dir.stamp
136 | msvc/scripts/inputs/
137 | extensions-config.h
138 |
139 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | latest
5 | Copyright 2017-2025
6 | Husqvik
7 | Tibber
8 | 0.9.30
9 | MIT
10 | https://github.com/Husqvik/GraphQlClientGenerator
11 | GraphQlLogo.png
12 | https://github.com/Husqvik/GraphQlClientGenerator
13 | git
14 | true
15 | snupkg
16 | README.md
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/GraphQlClientGenerator.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.4.33122.133
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQlClientGenerator.Console", "src\GraphQlClientGenerator.Console\GraphQlClientGenerator.Console.csproj", "{D8D86371-7E15-4275-8968-2FB431EBBDE3}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C48B93CC-CA04-4372-AA5C-A8DAFA295BCF}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQlClientGenerator", "src\GraphQlClientGenerator\GraphQlClientGenerator.csproj", "{7CC22B24-8C9F-46DA-BE7C-A07BF5D73834}"
11 | EndProject
12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{4C764379-22AF-4939-9189-9F6C617752F1}"
13 | EndProject
14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQlClientGenerator.Test", "test\GraphQlClientGenerator.Test\GraphQlClientGenerator.Test.csproj", "{E068E024-2E58-41AD-A617-37DFA973F2E3}"
15 | EndProject
16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4F6A0502-F97A-49DD-ABF1-E37DF50519E4}"
17 | ProjectSection(SolutionItems) = preProject
18 | Directory.Build.props = Directory.Build.props
19 | GraphQlLogo.png = GraphQlLogo.png
20 | License.md = License.md
21 | README.md = README.md
22 | EndProjectSection
23 | EndProject
24 | Global
25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
26 | Debug|Any CPU = Debug|Any CPU
27 | Release|Any CPU = Release|Any CPU
28 | EndGlobalSection
29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
30 | {D8D86371-7E15-4275-8968-2FB431EBBDE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
31 | {D8D86371-7E15-4275-8968-2FB431EBBDE3}.Debug|Any CPU.Build.0 = Debug|Any CPU
32 | {D8D86371-7E15-4275-8968-2FB431EBBDE3}.Release|Any CPU.ActiveCfg = Release|Any CPU
33 | {D8D86371-7E15-4275-8968-2FB431EBBDE3}.Release|Any CPU.Build.0 = Release|Any CPU
34 | {7CC22B24-8C9F-46DA-BE7C-A07BF5D73834}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
35 | {7CC22B24-8C9F-46DA-BE7C-A07BF5D73834}.Debug|Any CPU.Build.0 = Debug|Any CPU
36 | {7CC22B24-8C9F-46DA-BE7C-A07BF5D73834}.Release|Any CPU.ActiveCfg = Release|Any CPU
37 | {7CC22B24-8C9F-46DA-BE7C-A07BF5D73834}.Release|Any CPU.Build.0 = Release|Any CPU
38 | {E068E024-2E58-41AD-A617-37DFA973F2E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
39 | {E068E024-2E58-41AD-A617-37DFA973F2E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
40 | {E068E024-2E58-41AD-A617-37DFA973F2E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
41 | {E068E024-2E58-41AD-A617-37DFA973F2E3}.Release|Any CPU.Build.0 = Release|Any CPU
42 | EndGlobalSection
43 | GlobalSection(SolutionProperties) = preSolution
44 | HideSolutionNode = FALSE
45 | EndGlobalSection
46 | GlobalSection(NestedProjects) = preSolution
47 | {D8D86371-7E15-4275-8968-2FB431EBBDE3} = {C48B93CC-CA04-4372-AA5C-A8DAFA295BCF}
48 | {7CC22B24-8C9F-46DA-BE7C-A07BF5D73834} = {C48B93CC-CA04-4372-AA5C-A8DAFA295BCF}
49 | {E068E024-2E58-41AD-A617-37DFA973F2E3} = {4C764379-22AF-4939-9189-9F6C617752F1}
50 | EndGlobalSection
51 | GlobalSection(ExtensibilityGlobals) = postSolution
52 | SolutionGuid = {7BFDF2E3-B56D-4752-9A28-8AE0E3BC47E2}
53 | EndGlobalSection
54 | EndGlobal
55 |
--------------------------------------------------------------------------------
/GraphQlLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Husqvik/GraphQlClientGenerator/b5212b8f6b0c83ab01d0d0b854c7fdb5a650f051/GraphQlLogo.png
--------------------------------------------------------------------------------
/License.md:
--------------------------------------------------------------------------------
1 | Copyright © 2017-2025 Husqvik
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | GraphQL C# client generator
2 | =======================
3 |
4 | [](https://ci.appveyor.com/project/Husqvik/graphql-client-generator)
5 | [](https://www.nuget.org/packages/GraphQlClientGenerator)
6 |
7 | This simple console app generates C# GraphQL query builder and data classes for simple, compiler checked, usage of a GraphQL API.
8 |
9 | ----------
10 |
11 | Generator app usage
12 | -------------
13 |
14 | ```console
15 | GraphQlClientGenerator.Console --serviceUrl --outputPath --namespace [--header ]
16 | ```
17 |
18 | Nuget package
19 | -------------
20 | Installation:
21 | ```console
22 | Install-Package GraphQlClientGenerator
23 | ```
24 |
25 | dotnet tool
26 | -------------
27 | ```console
28 | dotnet tool install GraphQlClientGenerator.Tool --global
29 | graphql-client-generator --serviceUrl --outputPath --namespace [--header ]
30 | ```
31 |
32 | Code
33 | -------------
34 | Code example for class generation:
35 | ```csharp
36 | var schema = await GraphQlGenerator.RetrieveSchema("https://my-graphql-api/gql");
37 | var generator = new GraphQlGenerator();
38 | var generatedClasses = generator.GenerateFullClientCSharpFile(schema);
39 | ```
40 |
41 | or using full blown setup:
42 |
43 | ```csharp
44 | var schema = await GraphQlGenerator.RetrieveSchema("https://my-graphql-api/gql");
45 | var configuration = new GraphQlGeneratorConfiguration { TargetNamespace = "MyGqlApiClient", ... };
46 | var generator = new GraphQlGenerator(configuration);
47 | var builder = new StringBuilder();
48 | using var writer = new StringWriter(builder);
49 | var generationContext = new SingleFileGenerationContext(schema, writer) { LogMessage = Console.WriteLine };
50 | generator.Generate(generationContext);
51 | var csharpCode = builder.ToString();
52 | ```
53 |
54 | C# 9 source generator
55 | -------------
56 | C# 9 introduced source generators that can be attached to compilation process. Generated classes will be automatically included in project.
57 |
58 | Project file example:
59 | ```xml
60 |
61 | Exe
62 | net9.0
63 |
64 |
65 | https://api.tibber.com/v1-beta/gql
66 |
67 | $(RootNamespace)
68 | Consumption:ConsumptionEntry|Production:ProductionEntry|RootMutation:TibberMutation|Query:Tibber
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | ```
86 |
87 | Query builder usage
88 | -------------
89 | ```csharp
90 | var builder =
91 | new QueryQueryBuilder()
92 | .WithMe(
93 | new MeQueryBuilder()
94 | .WithAllScalarFields()
95 | .WithHome(
96 | new HomeQueryBuilder()
97 | .WithAllScalarFields()
98 | .WithSubscription(
99 | new SubscriptionQueryBuilder()
100 | .WithStatus()
101 | .WithValidFrom())
102 | .WithSignupStatus(
103 | new SignupStatusQueryBuilder().WithAllFields())
104 | .WithDisaggregation(
105 | new DisaggregationQueryBuilder().WithAllFields()),
106 | "b420001d-189b-44c0-a3d5-d62452bfdd42")
107 | .WithEnergyStatements ("2016-06", "2016-10"));
108 |
109 | var query = builder.Build(Formatting.Indented);
110 | ```
111 | results into
112 | ```graphql
113 | query {
114 | me {
115 | id
116 | firstName
117 | lastName
118 | fullName
119 | ssn
120 | email
121 | language
122 | tone
123 | home (id: "b420001d-189b-44c0-a3d5-d62452bfdd42") {
124 | id
125 | avatar
126 | timeZone
127 | subscription {
128 | status
129 | validFrom
130 | }
131 | signupStatus {
132 | registrationStartedTimestamp
133 | registrationCompleted
134 | registrationCompletedTimestamp
135 | checkCurrentSupplierPassed
136 | supplierSwitchConfirmationPassed
137 | startDatePassed
138 | firstReadingReceived
139 | firstBillingDone
140 | firstBillingTimestamp
141 | }
142 | disaggregation {
143 | year
144 | month
145 | fixedConsumptionKwh
146 | fixedConsumptionKwhPercent
147 | heatingConsumptionKwh
148 | heatingConsumptionKwhPercent
149 | behaviorConsumptionKwh
150 | behaviorConsumptionKwhPercent
151 | }
152 | }
153 | energyStatements(from: "2016-06", to: "2016-10")
154 | }
155 | }
156 | ```
157 |
158 | Mutation
159 | -------------
160 | ```csharp
161 | var mutation =
162 | new MutationQueryBuilder()
163 | .WithUpdateHome(
164 | new HomeQueryBuilder().WithAllScalarFields(),
165 | new UpdateHomeInput { HomeId = Guid.Empty, AppNickname = "My nickname", Type = HomeType.House, NumberOfResidents = 4, Size = 160, AppAvatar = HomeAvatar.Floorhouse1, PrimaryHeatingSource = HeatingSource.Electricity }
166 | )
167 | .Build(Formatting.Indented, 2);
168 | ```
169 | result:
170 | ```graphql
171 | mutation {
172 | updateHome (input: {
173 | homeId: "00000000-0000-0000-0000-000000000000"
174 | appNickname: "My nickname"
175 | appAvatar: FLOORHOUSE1
176 | size: 160
177 | type: HOUSE
178 | numberOfResidents: 4
179 | primaryHeatingSource: ELECTRICITY
180 | }) {
181 | id
182 | timeZone
183 | appNickname
184 | appAvatar
185 | size
186 | type
187 | numberOfResidents
188 | primaryHeatingSource
189 | hasVentilationSystem
190 | }
191 | }
192 | ```
193 |
194 | Field exclusion
195 | -------------
196 | Sometimes there is a need to select almost all fields of a queried object except few. In that case `Except` methods can be used often in conjunction with `WithAllFields` or `WithAllScalarFields`.
197 | ```csharp
198 | new ViewerQueryBuilder()
199 | .WithHomes(
200 | new HomeQueryBuilder()
201 | .WithAllScalarFields()
202 | .ExceptPrimaryHeatingSource()
203 | .ExceptMainFuseSize()
204 | )
205 | .Build(Formatting.Indented);
206 | ```
207 | result:
208 | ```graphql
209 | query {
210 | homes {
211 | id
212 | timeZone
213 | appNickname
214 | appAvatar
215 | size
216 | type
217 | numberOfResidents
218 | hasVentilationSystem
219 | }
220 | }
221 |
222 | ```
223 |
224 | Aliases
225 | -------------
226 | Queried fields can be freely renamed to match target data classes using GraphQL aliases.
227 | ```csharp
228 | new ViewerQueryBuilder("MyQuery")
229 | .WithHome(
230 | new HomeQueryBuilder()
231 | .WithType()
232 | .WithSize()
233 | .WithAddress(new AddressQueryBuilder().WithAddress1("primaryAddressText").WithCountry(), "primaryAddress"),
234 | Guid.NewGuid(),
235 | "primaryHome")
236 | .WithHome(
237 | new HomeQueryBuilder()
238 | .WithType()
239 | .WithSize()
240 | .WithAddress(new AddressQueryBuilder().WithAddress1("secondaryAddressText").WithCountry(), "secondaryAddress"),
241 | Guid.NewGuid(),
242 | "secondaryHome")
243 | .Build(Formatting.Indented);
244 | ```
245 | result:
246 | ```graphql
247 | query MyQuery {
248 | primaryHome: home (id: "120efe4a-6839-45fc-beed-27455d29212f") {
249 | type
250 | size
251 | primaryAddress: address {
252 | primaryAddressText: address1
253 | country
254 | }
255 | }
256 | secondaryHome: home (id: "0c735830-be56-4a3d-a8cb-d0189037f221") {
257 | type
258 | size
259 | secondaryAddress: address {
260 | secondaryAddressText: address1
261 | country
262 | }
263 | }
264 | }
265 | ```
266 |
267 | Query parameters
268 | -------------
269 | ```csharp
270 | var homeIdParameter = new GraphQlQueryParameter("homeId", "ID", homeId);
271 |
272 | var builder =
273 | new TibberQueryBuilder()
274 | .WithViewer(
275 | new ViewerQueryBuilder()
276 | .WithHome(new HomeQueryBuilder().WithAllScalarFields(), homeIdParameter)
277 | )
278 | .WithParameter(homeIdParameter);
279 | ```
280 | result:
281 | ```graphql
282 | query ($homeId: ID = "c70dcbe5-4485-4821-933d-a8a86452737b") {
283 | viewer{
284 | home(id: $homeId) {
285 | id
286 | timeZone
287 | appNickname
288 | appAvatar
289 | size
290 | type
291 | numberOfResidents
292 | primaryHeatingSource
293 | hasVentilationSystem
294 | mainFuseSize
295 | }
296 | }
297 | }
298 | ```
299 |
300 | Directives
301 | -------------
302 | ```csharp
303 | var includeDirectParameter = new GraphQlQueryParameter("direct", "Boolean", true);
304 | var includeDirective = new IncludeDirective(includeDirectParameter);
305 | var skipDirective = new SkipDirective(true);
306 |
307 | var builder =
308 | new TibberQueryBuilder()
309 | .WithViewer(
310 | new ViewerQueryBuilder()
311 | .WithName(include: includeDirective)
312 | .WithAccountType(skip: skipDirective)
313 | .WithHomes(new HomeQueryBuilder().WithId(), skip: skipDirective)
314 | )
315 | .WithParameter(includeDirectParameter);
316 | ```
317 | result:
318 | ```graphql
319 | query (
320 | $direct: Boolean = true) {
321 | viewer {
322 | name @include(if: $direct)
323 | accountType @skip(if: true)
324 | homes @skip(if: true) {
325 | id
326 | }
327 | }
328 | }
329 | ```
330 |
331 | Inline fragments
332 | -------------
333 | ```csharp
334 | var builder =
335 | new RootQueryBuilder("InlineFragments")
336 | .WithUnion(
337 | new UnionTypeQueryBuilder()
338 | .WithConcreteType1Fragment(new ConcreteType1QueryBuilder().WithAllFields())
339 | .WithConcreteType2Fragment(new ConcreteType2QueryBuilder().WithAllFields())
340 | .WithConcreteType3Fragment(
341 | new ConcreteType3QueryBuilder()
342 | .WithName()
343 | .WithConcreteType3Field("alias")
344 | .WithFunction("my value", "myResult1")
345 | )
346 | )
347 | .WithInterface(
348 | new NamedTypeQueryBuilder()
349 | .WithName()
350 | .WithConcreteType3Fragment(
351 | new ConcreteType3QueryBuilder()
352 | .WithName()
353 | .WithConcreteType3Field()
354 | .WithFunction("my value")
355 | ),
356 | Guid.Empty
357 | );
358 | ```
359 | result:
360 | ```graphql
361 | query InlineFragments {
362 | union {
363 | __typename
364 | ... on ConcreteType1 {
365 | name
366 | concreteType1Field
367 | }
368 | ... on ConcreteType2 {
369 | name
370 | concreteType2Field
371 | }
372 | ... on ConcreteType3 {
373 | __typename
374 | name
375 | alias: concreteType3Field
376 | myResult1: function(value: "my value")
377 | }
378 | }
379 | interface(parameter: "00000000-0000-0000-0000-000000000000") {
380 | name
381 | ... on ConcreteType3 {
382 | __typename
383 | name
384 | concreteType3Field
385 | function(value: "my value")
386 | }
387 | }
388 | }
389 | ```
390 |
391 | Custom scalar types
392 | -------------
393 | GraphQL supports custom scalar types. By default these are mapped to `object` type. To ensure appropriate .NET types are generated for data class properties custom mapping interface can be used:
394 |
395 | ```csharp
396 | var configuration = new GraphQlGeneratorConfiguration();
397 | configuration.ScalarFieldTypeMappingProvider = new MyCustomScalarFieldTypeMappingProvider();
398 |
399 | public class MyCustomScalarFieldTypeMappingProvider : IScalarFieldTypeMappingProvider
400 | {
401 | public ScalarFieldTypeDescription GetCustomScalarFieldType(ScalarFieldTypeProviderContext context)
402 | {
403 | var unwrappedType = context.FieldType.UnwrapIfNonNull();
404 |
405 | return
406 | unwrappedType.Name switch
407 | {
408 | "Byte" => new ScalarFieldTypeDescription { NetTypeName = GenerationContext.GetNullableNetTypeName(context, "byte", false), FormatMask = null },
409 | "DateTime" => new ScalarFieldTypeDescription { NetTypeName = GenerationContext.GetNullableNetTypeName(context, "DateTime", false), FormatMask = null },
410 | _ => DefaultScalarFieldTypeMappingProvider.GetFallbackFieldType(context)
411 | };
412 | }
413 | }
414 | ```
415 |
416 | Generated class example:
417 | ```csharp
418 | public class OrderType
419 | {
420 | public DateTime? CreatedDateTimeUtc { get; set; }
421 | public byte? SomeSmallNumber { get; set; }
422 | }
423 | ```
424 |
425 | vs.
426 |
427 | ```csharp
428 | public class OrderType
429 | {
430 | public object CreatedDateTimeUtc { get; set; }
431 | public object SomeSmallNumber { get; set; }
432 | }
433 | ```
434 | ### C# 9 source generator custom types
435 |
436 | Source generator supports `RegexScalarFieldTypeMappingProvider` rules using JSON configuration file. Example:
437 | ```json
438 | [
439 | {
440 | "patternBaseType": ".+",
441 | "patternValueType": ".+",
442 | "patternValueName": "^((timestamp)|(.*(f|F)rom)|(.*(t|T)o))$",
443 | "netTypeName": "DateTimeOffset",
444 | "isReferenceType": false,
445 | "formatMask": "O"
446 | }
447 | ]
448 | ```
449 | All pattern values must be specified. `Null` values are not accepted.
450 |
451 | The file must be named `RegexScalarFieldTypeMappingProvider.gql.config.json` and included as additional file.
452 |
453 | ```xml
454 |
455 |
456 |
457 | ```
458 |
--------------------------------------------------------------------------------
/src/GraphQlClientGenerator.Console/Commands.cs:
--------------------------------------------------------------------------------
1 | using System.CommandLine;
2 | using System.CommandLine.Builder;
3 | using System.CommandLine.IO;
4 | using System.CommandLine.NamingConventionBinder;
5 | using System.CommandLine.Parsing;
6 |
7 | namespace GraphQlClientGenerator.Console;
8 |
9 | internal static class Commands
10 | {
11 | public static readonly Parser Parser =
12 | new CommandLineBuilder(SetupGenerateCommand())
13 | .UseDefaults()
14 | .UseExceptionHandler((exception, invocationContext) =>
15 | {
16 | System.Console.ForegroundColor = ConsoleColor.Red;
17 | invocationContext.Console.Error.WriteLine($"An error occurred:{Environment.NewLine}{exception}");
18 | System.Console.ResetColor();
19 | invocationContext.ExitCode = 2;
20 | })
21 | .Build();
22 |
23 | private static RootCommand SetupGenerateCommand()
24 | {
25 | var serviceUrlOption = new Option(["--serviceUrl", "-u"], "GraphQL service URL used for retrieving schema metadata");
26 | var schemaFileOption = new Option(["--schemaFileName", "-s"], "Path to schema metadata file in JSON format");
27 |
28 | var classMappingOption =
29 | new Option(
30 | "--classMapping",
31 | "Format: {GraphQlTypeName}:{C#ClassName}; allows to define custom class names for specific GraphQL types");
32 |
33 | classMappingOption.AddValidator(
34 | option =>
35 | option.ErrorMessage =
36 | KeyValueParameterParser.TryGetCustomClassMapping(option.Tokens.Select(t => t.Value), out _, out var errorMessage)
37 | ? null
38 | : errorMessage);
39 |
40 | var headerOption = new Option("--header", "Format: {Header}:{Value}; allows to enter custom headers required to fetch GraphQL metadata");
41 | headerOption.AddValidator(
42 | option =>
43 | option.ErrorMessage =
44 | KeyValueParameterParser.TryGetCustomHeaders(option.Tokens.Select(t => t.Value), out _, out var errorMessage)
45 | ? null
46 | : errorMessage);
47 |
48 | var regexScalarFieldTypeMappingConfigurationOption =
49 | new Option("--regexScalarFieldTypeMappingConfigurationFile", $"File name specifying rules for \"{nameof(RegexScalarFieldTypeMappingProvider)}\"");
50 |
51 | var command =
52 | new RootCommand
53 | {
54 | new Option(["--outputPath", "-o"], "Output path; include file name for single file output type; folder name for one class per file output type") { IsRequired = true },
55 | new Option(["--namespace", "-n"], "Root namespace all classes and other members are generated into") { IsRequired = true },
56 | serviceUrlOption,
57 | schemaFileOption,
58 | new Option("--httpMethod", () => "POST", "GraphQL schema metadata retrieval HTTP method"),
59 | headerOption,
60 | new Option("--classPrefix", "Class prefix; value \"Test\" extends class name to \"TestTypeName\""),
61 | new Option("--classSuffix", "Class suffix, for instance for version control; value \"V2\" extends class name to \"TypeNameV2\""),
62 | new Option("--csharpVersion", () => CSharpVersion.Compatible, "C# version compatibility"),
63 | new Option("--codeDocumentationType", () => CodeDocumentationType.Disabled, "Specifies code documentation generation option"),
64 | new Option("--memberAccessibility", () => MemberAccessibility.Public, "Class and interface access level"),
65 | new Option("--outputType", () => OutputType.SingleFile, "Specifies generated classes organization"),
66 | new Option("--partialClasses", () => false, "Mark classes as \"partial\""),
67 | classMappingOption,
68 | new Option("--booleanTypeMapping", () => BooleanTypeMapping.Boolean, "Specifies the .NET type generated for GraphQL built-in Boolean data type"),
69 | new Option("--floatTypeMapping", () => FloatTypeMapping.Decimal, "Specifies the .NET type generated for GraphQL built-in Float data type"),
70 | new Option("--idTypeMapping", () => IdTypeMapping.Guid, "Specifies the .NET type generated for GraphQL built-in ID data type"),
71 | new Option("--integerTypeMapping", () => IntegerTypeMapping.Int32, "Specifies the .NET type generated for GraphQL built-in Integer data type"),
72 | new Option("--jsonPropertyAttribute", () => JsonPropertyGenerationOption.CaseInsensitive, "Specifies the condition for using \"JsonPropertyAttribute\""),
73 | new Option("--enumValueNaming", () => EnumValueNamingOption.CSharp, "Use \"Original\" to avoid pretty C# name conversion for maximum deserialization compatibility"),
74 | new Option("--dataClassMemberNullability", () => DataClassMemberNullability.AlwaysNullable, "Specifies whether data class scalar properties generated always nullable (for better type reuse) or respect the GraphQL schema"),
75 | new Option("--generationOrder", () => GenerationOrder.DefinedBySchema, "Specifies whether order of generated C# classes/enums respect the GraphQL schema or is enforced to alphabetical for easier change tracking"),
76 | new Option("--inputObjectMode", () => InputObjectMode.Rich, "Specifies whether input objects are generated as POCOs or they have support of GraphQL parameter references and explicit null values"),
77 | new Option("--includeDeprecatedFields", () => false, "Includes deprecated fields in generated query builders and data classes"),
78 | new Option("--nullableReferences", () => false, "Enables nullable references"),
79 | new Option("--fileScopedNamespaces", () => false, "Specifies whether file-scoped namespaces should be used in generated files (C# 10 or later)"),
80 | new Option("--ignoreServiceUrlCertificateErrors", () => false, "Ignores HTTPS errors when retrieving GraphQL metadata from an URL; typically when using self signed certificates"),
81 | regexScalarFieldTypeMappingConfigurationOption
82 | };
83 |
84 | command.TreatUnmatchedTokensAsErrors = true;
85 | command.Name = "GraphQlClientGenerator.Console";
86 | command.Description = "A tool for generating C# GraphQL query builders and data classes";
87 | command.Handler = CommandHandler.Create(GraphQlCSharpFileHelper.GenerateClientSourceCode);
88 | command.AddValidator(
89 | option =>
90 | option.ErrorMessage =
91 | option.FindResultFor(serviceUrlOption) is not null && option.FindResultFor(schemaFileOption) is not null
92 | ? "\"serviceUrl\" and \"schemaFileName\" parameters are mutually exclusive. "
93 | : null);
94 |
95 | command.AddValidator(
96 | option =>
97 | option.ErrorMessage =
98 | option.FindResultFor(serviceUrlOption) is null && option.FindResultFor(schemaFileOption) is null
99 | ? "Either \"serviceUrl\" or \"schemaFileName\" parameter must be specified. "
100 | : null);
101 |
102 | command.AddValidator(
103 | option =>
104 | {
105 | var regexScalarFieldTypeMappingConfigurationFileName = option.FindResultFor(regexScalarFieldTypeMappingConfigurationOption)?.GetValueOrDefault();
106 | if (regexScalarFieldTypeMappingConfigurationFileName is null)
107 | return;
108 |
109 | try
110 | {
111 | RegexScalarFieldTypeMappingProvider.ParseRulesFromJson(File.ReadAllText(regexScalarFieldTypeMappingConfigurationFileName));
112 | }
113 | catch (Exception exception)
114 | {
115 | option.ErrorMessage = exception.Message;
116 | }
117 | });
118 |
119 | return command;
120 | }
121 | }
--------------------------------------------------------------------------------
/src/GraphQlClientGenerator.Console/GraphQlCSharpFileHelper.cs:
--------------------------------------------------------------------------------
1 | using System.CommandLine;
2 |
3 | namespace GraphQlClientGenerator.Console;
4 |
5 | internal static class GraphQlCSharpFileHelper
6 | {
7 | public static async Task GenerateClientSourceCode(IConsole console, ProgramOptions options)
8 | {
9 | GraphQlSchema schema;
10 |
11 | if (String.IsNullOrWhiteSpace(options.ServiceUrl))
12 | {
13 | var schemaJson = await File.ReadAllTextAsync(options.SchemaFileName);
14 | console.WriteLine($"GraphQL schema file {options.SchemaFileName} loaded ({schemaJson.Length:N0} B). ");
15 | schema = GraphQlGenerator.DeserializeGraphQlSchema(schemaJson);
16 | }
17 | else
18 | {
19 | if (!KeyValueParameterParser.TryGetCustomHeaders(options.Header, out var headers, out var headerParsingErrorMessage))
20 | throw new InvalidOperationException(headerParsingErrorMessage);
21 |
22 | using var httpClientHandler = GraphQlGenerator.CreateDefaultHttpClientHandler();
23 | if (options.IgnoreServiceUrlCertificateErrors)
24 | httpClientHandler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
25 |
26 | schema = await GraphQlGenerator.RetrieveSchema(new HttpMethod(options.HttpMethod), options.ServiceUrl, headers, httpClientHandler, GraphQlWellKnownDirective.None);
27 | console.WriteLine($"GraphQL Schema retrieved from {options.ServiceUrl}. ");
28 | }
29 |
30 | var generatorConfiguration =
31 | new GraphQlGeneratorConfiguration
32 | {
33 | TargetNamespace = options.Namespace,
34 | CSharpVersion = options.CSharpVersion,
35 | ClassPrefix = options.ClassPrefix,
36 | ClassSuffix = options.ClassSuffix,
37 | CodeDocumentationType = options.CodeDocumentationType,
38 | GeneratePartialClasses = options.PartialClasses,
39 | MemberAccessibility = options.MemberAccessibility,
40 | IdTypeMapping = options.IdTypeMapping,
41 | FloatTypeMapping = options.FloatTypeMapping,
42 | IntegerTypeMapping = options.IntegerTypeMapping,
43 | BooleanTypeMapping = options.BooleanTypeMapping,
44 | JsonPropertyGeneration = options.JsonPropertyAttribute,
45 | EnumValueNaming = options.EnumValueNaming,
46 | DataClassMemberNullability = options.DataClassMemberNullability,
47 | GenerationOrder = options.GenerationOrder,
48 | InputObjectMode = options.InputObjectMode,
49 | IncludeDeprecatedFields = options.IncludeDeprecatedFields,
50 | EnableNullableReferences = options.NullableReferences,
51 | FileScopedNamespaces = options.FileScopedNamespaces
52 | };
53 |
54 | if (!KeyValueParameterParser.TryGetCustomClassMapping(options.ClassMapping, out var customMapping, out var customMappingParsingErrorMessage))
55 | throw new InvalidOperationException(customMappingParsingErrorMessage);
56 |
57 | foreach (var kvp in customMapping)
58 | generatorConfiguration.CustomClassNameMapping.Add(kvp);
59 |
60 | if (!String.IsNullOrEmpty(options.RegexScalarFieldTypeMappingConfigurationFile))
61 | {
62 | generatorConfiguration.ScalarFieldTypeMappingProvider =
63 | new RegexScalarFieldTypeMappingProvider(
64 | RegexScalarFieldTypeMappingProvider.ParseRulesFromJson(await File.ReadAllTextAsync(options.RegexScalarFieldTypeMappingConfigurationFile)));
65 |
66 | console.WriteLine($"Scalar field type mapping configuration file {options.RegexScalarFieldTypeMappingConfigurationFile} loaded. ");
67 | }
68 |
69 | var generator = new GraphQlGenerator(generatorConfiguration);
70 |
71 | if (options.OutputType is OutputType.SingleFile)
72 | {
73 | await File.WriteAllTextAsync(options.OutputPath, generator.GenerateFullClientCSharpFile(schema, console.WriteLine));
74 | console.WriteLine($"File {options.OutputPath} generated successfully ({new FileInfo(options.OutputPath).Length:N0} B). ");
75 | }
76 | else
77 | {
78 | var projectFileInfo =
79 | options.OutputPath is not null && options.OutputPath.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase)
80 | ? new FileInfo(options.OutputPath)
81 | : null;
82 |
83 | var codeFileEmitter = new FileSystemEmitter(projectFileInfo?.DirectoryName ?? options.OutputPath);
84 | var multipleFileGenerationContext =
85 | new MultipleFileGenerationContext(schema, codeFileEmitter, projectFileInfo?.Name)
86 | {
87 | LogMessage = console.WriteLine
88 | };
89 |
90 | generator.Generate(multipleFileGenerationContext);
91 | }
92 | }
93 | }
--------------------------------------------------------------------------------
/src/GraphQlClientGenerator.Console/GraphQlClientGenerator.Console.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | GraphQL C# Client Generator Console App
5 | A simple strongly typed C# GraphQL client generator console app
6 | Exe
7 | net8.0;net9.0
8 | GraphQL C# Client Generator Console App
9 | GraphQlClientGenerator.Tool
10 | GraphQL Client Generator Tool Console
11 |
12 |
15 |
16 | true
17 | graphql-client-generator
18 | latest
19 | enable
20 |
21 |
22 |
23 |
24 |
25 | true
26 | all
27 |
28 |
29 |
30 |
31 | true
32 | true
33 | $(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | True
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/GraphQlClientGenerator.Console/Program.cs:
--------------------------------------------------------------------------------
1 | using System.CommandLine.Parsing;
2 | using GraphQlClientGenerator.Console;
3 |
4 | return await Commands.Parser.InvokeAsync(args);
--------------------------------------------------------------------------------
/src/GraphQlClientGenerator.Console/ProgramOptions.cs:
--------------------------------------------------------------------------------
1 | namespace GraphQlClientGenerator.Console;
2 |
3 | public class ProgramOptions
4 | {
5 | public string OutputPath { get; set; }
6 | public string Namespace { get; set; }
7 | public bool FileScopedNamespaces { get; set; }
8 | public bool IgnoreServiceUrlCertificateErrors { get; set; }
9 | public string ServiceUrl { get; set; }
10 | public string SchemaFileName { get; set; }
11 | public string HttpMethod { get; set; }
12 | public ICollection Header { get; set; }
13 | public string ClassPrefix { get; set; }
14 | public string ClassSuffix { get; set; }
15 | public CSharpVersion CSharpVersion { get; set; }
16 | public CodeDocumentationType CodeDocumentationType { get; set; }
17 | public MemberAccessibility MemberAccessibility { get; set; }
18 | public OutputType OutputType { get; set; }
19 | public bool PartialClasses { get; set; }
20 | public ICollection ClassMapping { get; set; }
21 | public string RegexScalarFieldTypeMappingConfigurationFile { get; set; }
22 | public IdTypeMapping IdTypeMapping { get; set; }
23 | public FloatTypeMapping FloatTypeMapping { get; set; }
24 | public IntegerTypeMapping IntegerTypeMapping { get; set; }
25 | public BooleanTypeMapping BooleanTypeMapping { get; set; }
26 | public JsonPropertyGenerationOption JsonPropertyAttribute { get; set; }
27 | public EnumValueNamingOption EnumValueNaming { get; set; }
28 | public DataClassMemberNullability DataClassMemberNullability { get; set; }
29 | public GenerationOrder GenerationOrder { get; set; }
30 | public InputObjectMode InputObjectMode { get; set; }
31 | public bool IncludeDeprecatedFields { get; set; }
32 | public bool NullableReferences { get; set; }
33 | }
--------------------------------------------------------------------------------
/src/GraphQlClientGenerator/CSharpHelper.cs:
--------------------------------------------------------------------------------
1 | using System.Globalization;
2 |
3 | namespace GraphQlClientGenerator;
4 |
5 | public static class CSharpHelper
6 | {
7 | private static readonly HashSet CSharpKeywords =
8 | [
9 | "abstract",
10 | "as",
11 | "base",
12 | "bool",
13 | "break",
14 | "byte",
15 | "case",
16 | "catch",
17 | "char",
18 | "checked",
19 | "class",
20 | "const",
21 | "continue",
22 | "decimal",
23 | "default",
24 | "delegate",
25 | "do",
26 | "double",
27 | "else",
28 | "enum",
29 | "event",
30 | "explicit",
31 | "extern",
32 | "false",
33 | "finally",
34 | "fixed",
35 | "float",
36 | "for",
37 | "foreach",
38 | "goto",
39 | "if",
40 | "implicit",
41 | "in",
42 | "int",
43 | "interface",
44 | "internal",
45 | "is",
46 | "lock",
47 | "long",
48 | "namespace",
49 | "new",
50 | "null",
51 | "object",
52 | "operator",
53 | "out",
54 | "override",
55 | "params",
56 | "private",
57 | "protected",
58 | "public",
59 | "readonly",
60 | "ref",
61 | "return",
62 | "sbyte",
63 | "sealed",
64 | "short",
65 | "sizeof",
66 | "stackalloc",
67 | "static",
68 | "string",
69 | "struct",
70 | "switch",
71 | "this",
72 | "throw",
73 | "true",
74 | "try",
75 | "typeof",
76 | "uint",
77 | "ulong",
78 | "unchecked",
79 | "unsafe",
80 | "ushort",
81 | "using",
82 | "void",
83 | "volatile",
84 | "while"
85 | ];
86 |
87 | public static string EnsureCSharpQuoting(string name) => CSharpKeywords.Contains(name) ? $"@{name}" : name;
88 |
89 | public static bool IsValidIdentifier(string value)
90 | {
91 | var nextMustBeStartChar = true;
92 |
93 | if (value.Length == 0)
94 | return false;
95 |
96 | foreach (var ch in value)
97 | {
98 | var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(ch);
99 | switch (unicodeCategory)
100 | {
101 | case UnicodeCategory.UppercaseLetter:
102 | case UnicodeCategory.LowercaseLetter:
103 | case UnicodeCategory.TitlecaseLetter:
104 | case UnicodeCategory.ModifierLetter:
105 | case UnicodeCategory.LetterNumber:
106 | case UnicodeCategory.OtherLetter:
107 | nextMustBeStartChar = false;
108 | break;
109 |
110 | case UnicodeCategory.NonSpacingMark:
111 | case UnicodeCategory.SpacingCombiningMark:
112 | case UnicodeCategory.ConnectorPunctuation:
113 | case UnicodeCategory.DecimalDigitNumber:
114 | if (nextMustBeStartChar && ch != '_')
115 | return false;
116 |
117 | nextMustBeStartChar = false;
118 | break;
119 | default:
120 | return false;
121 | }
122 | }
123 |
124 | return true;
125 | }
126 |
127 | public static bool IsValidNamespace(string @namespace)
128 | {
129 | if (String.IsNullOrWhiteSpace(@namespace))
130 | return false;
131 |
132 | var namespaceElements = @namespace.Split('.');
133 | return namespaceElements.All(e => IsValidIdentifier(e.Trim()));
134 | }
135 |
136 | public static void ValidateClassName(string className)
137 | {
138 | if (!IsValidIdentifier(className))
139 | throw new InvalidOperationException($"Resulting class name \"{className}\" is not valid. ");
140 | }
141 |
142 | internal static bool IsTargetTypedNewSupported(this CSharpVersion cSharpVersion) =>
143 | cSharpVersion >= CSharpVersion.CSharp12;
144 |
145 | internal static bool IsCollectionExpressionSupported(this CSharpVersion cSharpVersion) =>
146 | cSharpVersion >= CSharpVersion.CSharp12;
147 |
148 | internal static bool IsSystemTextJsonSupported(this CSharpVersion cSharpVersion) =>
149 | cSharpVersion >= CSharpVersion.CSharp12;
150 |
151 | internal static bool IsFieldKeywordSupported(this CSharpVersion cSharpVersion) =>
152 | cSharpVersion >= CSharpVersion.CSharp12;
153 | }
--------------------------------------------------------------------------------
/src/GraphQlClientGenerator/Extensions.cs:
--------------------------------------------------------------------------------
1 | namespace GraphQlClientGenerator;
2 |
3 | public static class Extensions
4 | {
5 | public static GraphQlFieldType UnwrapIfNonNull(this GraphQlFieldType graphQlType) =>
6 | graphQlType.Kind == GraphQlTypeKind.NonNull ? graphQlType.OfType : graphQlType;
7 |
8 | internal static bool IsComplex(this GraphQlTypeKind graphQlTypeKind) =>
9 | graphQlTypeKind is GraphQlTypeKind.Object or GraphQlTypeKind.Interface or GraphQlTypeKind.Union;
10 |
11 | internal static IEnumerable GetComplexTypes(this GraphQlSchema schema) =>
12 | schema.Types.Where(t => t.Kind.IsComplex() && !t.IsBuiltIn());
13 |
14 | internal static IEnumerable GetInputObjectTypes(this GraphQlSchema schema) =>
15 | schema.Types.Where(t => t.Kind == GraphQlTypeKind.InputObject && !t.IsBuiltIn());
16 |
17 | internal static bool IsBuiltIn(this GraphQlType graphQlType) => graphQlType.Name is not null && graphQlType.Name.StartsWith("__");
18 |
19 | internal static string ToSetterAccessibilityPrefix(this PropertyAccessibility accessibility) =>
20 | accessibility switch
21 | {
22 | PropertyAccessibility.Public => null,
23 | PropertyAccessibility.Protected => "protected ",
24 | PropertyAccessibility.Internal => "internal ",
25 | PropertyAccessibility.ProtectedInternal => "protected internal ",
26 | PropertyAccessibility.Private => "private ",
27 | _ => throw new NotSupportedException()
28 | };
29 |
30 | internal static string EscapeXmlElementText(this string text) => text?.Replace("&", "&").Replace("<", "<").Replace(">", ">");
31 | }
--------------------------------------------------------------------------------
/src/GraphQlClientGenerator/GraphQlClientGenerator.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | GraphQL C# Client Generator
5 | A simple strongly typed C# GraphQL client generator library
6 | true
7 | $(NoWarn);1591;RS1035
8 | netstandard2.0
9 | GraphQL C# Client Generator
10 | GraphQL Client Generator
11 |
12 |
15 |
16 | true
17 | true
18 | latest
19 | enable
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | true
31 | true
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | True
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/GraphQlClientGenerator/GraphQlClientSourceGenerator.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using Microsoft.CodeAnalysis;
3 | using Microsoft.CodeAnalysis.CSharp;
4 | using Microsoft.CodeAnalysis.CSharp.Syntax;
5 | using Microsoft.CodeAnalysis.Text;
6 |
7 | namespace GraphQlClientGenerator;
8 |
9 | [Generator]
10 | public class GraphQlClientSourceGenerator : ISourceGenerator
11 | {
12 | private const string ApplicationCode = "GRAPHQLGEN";
13 | private const string FileNameGraphQlClientSource = "GraphQlClient.cs";
14 | private const string FileNameRegexScalarFieldTypeMappingProviderConfiguration = "RegexScalarFieldTypeMappingProvider.gql.config.json";
15 | private const string BuildPropertyKeyPrefix = "build_property.GraphQlClientGenerator_";
16 |
17 | private static readonly DiagnosticDescriptor DescriptorParameterError = CreateDiagnosticDescriptor(DiagnosticSeverity.Error, 1000);
18 | private static readonly DiagnosticDescriptor DescriptorGenerationError = CreateDiagnosticDescriptor(DiagnosticSeverity.Error, 1001);
19 | private static readonly DiagnosticDescriptor DescriptorInfo = CreateDiagnosticDescriptor(DiagnosticSeverity.Info, 3000);
20 |
21 | public void Initialize(GeneratorInitializationContext context)
22 | {
23 | }
24 |
25 | public void Execute(GeneratorExecutionContext context)
26 | {
27 | if (context.Compilation is not CSharpCompilation compilation)
28 | {
29 | context.ReportDiagnostic(Diagnostic.Create(DescriptorParameterError, Location.None, $"incompatible language: {context.Compilation.Language}"));
30 | return;
31 | }
32 |
33 | try
34 | {
35 | context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKey("ServiceUrl"), out var serviceUrl);
36 | var isServiceUrlMissing = String.IsNullOrWhiteSpace(serviceUrl);
37 | var graphQlSchemaFiles = context.AdditionalFiles.Where(f => Path.GetFileName(f.Path).EndsWith(".gql.schema.json", StringComparison.OrdinalIgnoreCase)).ToList();
38 | var regexScalarFieldTypeMappingProviderConfigurationJson =
39 | context.AdditionalFiles
40 | .SingleOrDefault(f => String.Equals(Path.GetFileName(f.Path), FileNameRegexScalarFieldTypeMappingProviderConfiguration, StringComparison.OrdinalIgnoreCase))
41 | ?.GetText()
42 | ?.ToString();
43 |
44 | var regexScalarFieldTypeMappingProviderRules =
45 | regexScalarFieldTypeMappingProviderConfigurationJson is not null
46 | ? RegexScalarFieldTypeMappingProvider.ParseRulesFromJson(regexScalarFieldTypeMappingProviderConfigurationJson)
47 | : null;
48 |
49 | var isSchemaFileSpecified = graphQlSchemaFiles.Any();
50 | if (isServiceUrlMissing && !isSchemaFileSpecified)
51 | {
52 | context.ReportDiagnostic(
53 | Diagnostic.Create(
54 | DescriptorInfo,
55 | Location.None,
56 | "Neither \"GraphQlClientGenerator_ServiceUrl\" parameter nor GraphQL JSON schema additional file specified; terminating. "));
57 |
58 | return;
59 | }
60 |
61 | if (!isServiceUrlMissing && isSchemaFileSpecified)
62 | {
63 | context.ReportDiagnostic(
64 | Diagnostic.Create(
65 | DescriptorParameterError,
66 | Location.None,
67 | "\"GraphQlClientGenerator_ServiceUrl\" parameter and GraphQL JSON schema additional file are mutually exclusive. "));
68 |
69 | return;
70 | }
71 |
72 | context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKey("Namespace"), out var @namespace);
73 | if (String.IsNullOrWhiteSpace(@namespace))
74 | {
75 | var root = (CompilationUnitSyntax)compilation.SyntaxTrees.FirstOrDefault()?.GetRoot();
76 | var namespaceIdentifier = (IdentifierNameSyntax)root?.Members.OfType().FirstOrDefault()?.Name;
77 | if (namespaceIdentifier is null)
78 | {
79 | context.ReportDiagnostic(Diagnostic.Create(DescriptorParameterError, Location.None, "\"GraphQlClientGenerator_Namespace\" required"));
80 | return;
81 | }
82 |
83 | @namespace = namespaceIdentifier.Identifier.ValueText;
84 |
85 | context.ReportDiagnostic(Diagnostic.Create(DescriptorInfo, Location.None, $"\"GraphQlClientGenerator_Namespace\" not specified; using \"{@namespace}\""));
86 | }
87 |
88 | var configuration = new GraphQlGeneratorConfiguration { TargetNamespace = @namespace };
89 |
90 | context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKey(nameof(configuration.ClassPrefix)), out var classPrefix);
91 | configuration.ClassPrefix = classPrefix;
92 |
93 | context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKey(nameof(configuration.ClassSuffix)), out var classSuffix);
94 | configuration.ClassSuffix = classSuffix;
95 |
96 | if (compilation.LanguageVersion >= LanguageVersion.CSharp12)
97 | configuration.CSharpVersion = CSharpVersion.CSharp12;
98 | else if (compilation.LanguageVersion >= LanguageVersion.CSharp6)
99 | configuration.CSharpVersion = CSharpVersion.CSharp6;
100 |
101 | context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKey(nameof(configuration.IncludeDeprecatedFields)), out var includeDeprecatedFieldsRaw);
102 | configuration.IncludeDeprecatedFields = Boolean.TryParse(includeDeprecatedFieldsRaw, out var includeDeprecatedFields) && includeDeprecatedFields;
103 |
104 | context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKey(nameof(configuration.EnableNullableReferences)), out var enableNullableReferencesRaw);
105 | configuration.EnableNullableReferences = Boolean.TryParse(enableNullableReferencesRaw, out var enableNullableReferences) && enableNullableReferences;
106 |
107 | if (configuration.EnableNullableReferences && compilation.Options.NullableContextOptions is NullableContextOptions.Disable)
108 | {
109 | context.ReportDiagnostic(Diagnostic.Create(DescriptorInfo, Location.None, "compilation nullable references disabled"));
110 | configuration.EnableNullableReferences = false;
111 | }
112 |
113 | if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKey("HttpMethod"), out var httpMethod))
114 | httpMethod = "POST";
115 |
116 | SetConfigurationEnumValue(context, nameof(CodeDocumentationType), CodeDocumentationType.XmlSummary, v => configuration.CodeDocumentationType = v);
117 | SetConfigurationEnumValue(context, nameof(FloatTypeMapping), FloatTypeMapping.Decimal, v => configuration.FloatTypeMapping = v);
118 | SetConfigurationEnumValue(context, nameof(BooleanTypeMapping), BooleanTypeMapping.Boolean, v => configuration.BooleanTypeMapping = v);
119 | SetConfigurationEnumValue(context, nameof(IdTypeMapping), IdTypeMapping.Guid, v => configuration.IdTypeMapping = v);
120 | SetConfigurationEnumValue(context, "JsonPropertyGeneration", JsonPropertyGenerationOption.CaseInsensitive, v => configuration.JsonPropertyGeneration = v);
121 | SetConfigurationEnumValue(context, "EnumValueNaming", EnumValueNamingOption.CSharp, v => configuration.EnumValueNaming = v);
122 | SetConfigurationEnumValue(context, nameof(DataClassMemberNullability), DataClassMemberNullability.AlwaysNullable, v => configuration.DataClassMemberNullability = v);
123 | SetConfigurationEnumValue(context, nameof(GenerationOrder), GenerationOrder.DefinedBySchema, v => configuration.GenerationOrder = v);
124 | SetConfigurationEnumValue(context, nameof(InputObjectMode), InputObjectMode.Rich, v => configuration.InputObjectMode = v);
125 |
126 | var outputType = OutputType.SingleFile;
127 | SetConfigurationEnumValue(context, nameof(OutputType), OutputType.SingleFile, v => outputType = v);
128 |
129 | context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKey("CustomClassMapping"), out var customClassMappingRaw);
130 | if (!KeyValueParameterParser.TryGetCustomClassMapping(
131 | customClassMappingRaw?.Split(['|', ';', ' '], StringSplitOptions.RemoveEmptyEntries),
132 | out var customMapping,
133 | out var customMappingParsingErrorMessage))
134 | {
135 | context.ReportDiagnostic(Diagnostic.Create(DescriptorParameterError, Location.None, customMappingParsingErrorMessage));
136 | return;
137 | }
138 |
139 | foreach (var kvp in customMapping)
140 | configuration.CustomClassNameMapping.Add(kvp);
141 |
142 | context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKey("Headers"), out var headersRaw);
143 | if (!KeyValueParameterParser.TryGetCustomHeaders(
144 | headersRaw?.Split(['|'], StringSplitOptions.RemoveEmptyEntries),
145 | out var headers,
146 | out var headerParsingErrorMessage))
147 | {
148 | context.ReportDiagnostic(Diagnostic.Create(DescriptorParameterError, Location.None, headerParsingErrorMessage));
149 | return;
150 | }
151 |
152 | if (context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKey("ScalarFieldTypeMappingProvider"), out var scalarFieldTypeMappingProviderName))
153 | {
154 | if (regexScalarFieldTypeMappingProviderRules is not null)
155 | {
156 | context.ReportDiagnostic(Diagnostic.Create(DescriptorParameterError, Location.None, "\"GraphQlClientGenerator_ScalarFieldTypeMappingProvider\" and RegexScalarFieldTypeMappingProviderConfiguration are mutually exclusive"));
157 | return;
158 | }
159 |
160 | if (String.IsNullOrWhiteSpace(scalarFieldTypeMappingProviderName))
161 | {
162 | context.ReportDiagnostic(Diagnostic.Create(DescriptorParameterError, Location.None, "\"GraphQlClientGenerator_ScalarFieldTypeMappingProvider\" value missing"));
163 | return;
164 | }
165 |
166 | var scalarFieldTypeMappingProviderType = Type.GetType(scalarFieldTypeMappingProviderName);
167 | if (scalarFieldTypeMappingProviderType is null)
168 | {
169 | context.ReportDiagnostic(Diagnostic.Create(DescriptorParameterError, Location.None, $"ScalarFieldTypeMappingProvider \"{scalarFieldTypeMappingProviderName}\" not found"));
170 | return;
171 | }
172 |
173 | var scalarFieldTypeMappingProvider = (IScalarFieldTypeMappingProvider)Activator.CreateInstance(scalarFieldTypeMappingProviderType);
174 | configuration.ScalarFieldTypeMappingProvider = scalarFieldTypeMappingProvider;
175 | }
176 | else if (regexScalarFieldTypeMappingProviderRules?.Count > 0)
177 | configuration.ScalarFieldTypeMappingProvider = new RegexScalarFieldTypeMappingProvider(regexScalarFieldTypeMappingProviderRules);
178 |
179 | var graphQlSchemas = new List<(string TargetFileName, GraphQlSchema Schema)>();
180 | if (isSchemaFileSpecified)
181 | {
182 | foreach (var schemaFile in graphQlSchemaFiles)
183 | {
184 | var targetFileName = $"{Path.GetFileNameWithoutExtension(schemaFile.Path)}.cs";
185 | graphQlSchemas.Add((targetFileName, GraphQlGenerator.DeserializeGraphQlSchema(schemaFile.GetText().ToString())));
186 | }
187 | }
188 | else
189 | {
190 | using var httpClientHandler = GraphQlGenerator.CreateDefaultHttpClientHandler();
191 | var ignoreServiceUrlCertificateErrors =
192 | context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKey("IgnoreServiceUrlCertificateErrors"), out var ignoreServiceUrlCertificateErrorsRaw) &&
193 | !String.IsNullOrWhiteSpace(ignoreServiceUrlCertificateErrorsRaw) && Convert.ToBoolean(ignoreServiceUrlCertificateErrorsRaw);
194 |
195 | if (ignoreServiceUrlCertificateErrors)
196 | httpClientHandler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true;
197 |
198 | var graphQlSchema =
199 | GraphQlGenerator.RetrieveSchema(new HttpMethod(httpMethod), serviceUrl, headers, httpClientHandler, GraphQlWellKnownDirective.None)
200 | .GetAwaiter()
201 | .GetResult();
202 |
203 | graphQlSchemas.Add((FileNameGraphQlClientSource, graphQlSchema));
204 | context.ReportDiagnostic(Diagnostic.Create(DescriptorInfo, Location.None, $"GraphQl schema fetched successfully from {serviceUrl}"));
205 | }
206 |
207 | context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKey(nameof(configuration.FileScopedNamespaces)), out var fileScopedNamespacesRaw);
208 | configuration.FileScopedNamespaces = !String.IsNullOrWhiteSpace(fileScopedNamespacesRaw) && Convert.ToBoolean(fileScopedNamespacesRaw);
209 |
210 | var generator = new GraphQlGenerator(configuration);
211 |
212 | foreach (var (targetFileName, schema) in graphQlSchemas)
213 | {
214 | if (outputType is OutputType.SingleFile)
215 | {
216 | var builder = new StringBuilder();
217 | using (var writer = new StringWriter(builder))
218 | generator.WriteFullClientCSharpFile(schema, writer);
219 |
220 | context.AddSource(targetFileName, SourceText.From(builder.ToString(), Encoding.UTF8));
221 | }
222 | else
223 | {
224 | var multipleFileGenerationContext = new MultipleFileGenerationContext(schema, new SourceGeneratorFileEmitter(context));
225 | generator.Generate(multipleFileGenerationContext);
226 | }
227 | }
228 |
229 | context.ReportDiagnostic(Diagnostic.Create(DescriptorInfo, Location.None, "GraphQlClientGenerator task completed successfully. "));
230 | }
231 | catch (Exception exception)
232 | {
233 | context.ReportDiagnostic(Diagnostic.Create(DescriptorGenerationError, Location.None, exception.Message));
234 | }
235 | }
236 |
237 | private static void SetConfigurationEnumValue(
238 | GeneratorExecutionContext context,
239 | string parameterName,
240 | TEnum defaultValue,
241 | Action valueSetter) where TEnum : Enum
242 | {
243 | context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(BuildPropertyKey(parameterName), out var enumStringValue);
244 | var value =
245 | String.IsNullOrWhiteSpace(enumStringValue)
246 | ? defaultValue
247 | : (TEnum)Enum.Parse(typeof(TEnum), enumStringValue, true);
248 |
249 | valueSetter(value);
250 | }
251 |
252 | private static string BuildPropertyKey(string parameterName) => $"{BuildPropertyKeyPrefix}{parameterName}";
253 |
254 | private static DiagnosticDescriptor CreateDiagnosticDescriptor(DiagnosticSeverity severity, int code) =>
255 | new(
256 | $"{ApplicationCode}{code}",
257 | $"{severity} {ApplicationCode}{code}",
258 | "{0}",
259 | "GraphQlClientGenerator",
260 | severity,
261 | true);
262 | }
263 |
264 | public class SourceGeneratorFileEmitter(GeneratorExecutionContext sourceGeneratorContext) : ICodeFileEmitter
265 | {
266 | public CodeFile CreateFile(string fileName) => new(fileName, new MemoryStream());
267 |
268 | public CodeFileInfo CollectFileInfo(CodeFile codeFile)
269 | {
270 | if (codeFile.Stream is not MemoryStream memoryStream)
271 | throw new ArgumentException($"File was not created by {nameof(SourceGeneratorFileEmitter)}.", nameof(codeFile));
272 |
273 | codeFile.Writer.Flush();
274 | sourceGeneratorContext.AddSource(codeFile.FileName, SourceText.From(Encoding.UTF8.GetString(memoryStream.ToArray()), Encoding.UTF8));
275 | var fileSize = (int)codeFile.Stream.Length;
276 | codeFile.Dispose();
277 | return new CodeFileInfo { FileName = codeFile.FileName, Length = fileSize };
278 | }
279 | }
--------------------------------------------------------------------------------
/src/GraphQlClientGenerator/GraphQlGeneratorConfiguration.cs:
--------------------------------------------------------------------------------
1 | namespace GraphQlClientGenerator;
2 |
3 | public class GraphQlGeneratorConfiguration
4 | {
5 | private string _targetNamespace = "GraphQlApi";
6 |
7 | public CSharpVersion CSharpVersion { get; set; }
8 |
9 | public string ClassPrefix { get; set; }
10 |
11 | public string ClassSuffix { get; set; }
12 |
13 | public string TargetNamespace
14 |
15 | {
16 | get => _targetNamespace;
17 | set
18 | {
19 | if (!CSharpHelper.IsValidNamespace(value))
20 | throw new ArgumentException($"namespace \"{value}\" is not valid. ");
21 |
22 | _targetNamespace = value;
23 | }
24 | }
25 |
26 | ///
27 | /// Allows to define custom class names for specific GraphQL types.
28 | ///
29 | public IDictionary CustomClassNameMapping { get; } = new Dictionary();
30 |
31 | public CodeDocumentationType CodeDocumentationType { get; set; }
32 |
33 | public bool IncludeDeprecatedFields { get; set; }
34 |
35 | public bool EnableNullableReferences { get; set; }
36 |
37 | public bool GeneratePartialClasses { get; set; } = true;
38 |
39 | ///
40 | /// Determines the .NET type generated for GraphQL Integer data type.
41 | ///
42 | /// For using custom .NET data type Custom
option must be used.
43 | public IntegerTypeMapping IntegerTypeMapping { get; set; } = IntegerTypeMapping.Int32;
44 |
45 | ///
46 | /// Determines the .NET type generated for GraphQL Float data type.
47 | ///
48 | /// For using custom .NET data type Custom
option must be used.
49 | public FloatTypeMapping FloatTypeMapping { get; set; }
50 |
51 | ///
52 | /// Determines the .NET type generated for GraphQL Boolean data type.
53 | ///
54 | /// For using custom .NET data type Custom
option must be used.
55 | public BooleanTypeMapping BooleanTypeMapping { get; set; }
56 |
57 | ///
58 | /// Determines the .NET type generated for GraphQL ID data type.
59 | ///
60 | /// For using custom .NET data type Custom
option must be used.
61 | public IdTypeMapping IdTypeMapping { get; set; } = IdTypeMapping.Guid;
62 |
63 | public PropertyGenerationOption PropertyGeneration { get; set; } = PropertyGenerationOption.AutoProperty;
64 |
65 | public JsonPropertyGenerationOption JsonPropertyGeneration { get; set; } = JsonPropertyGenerationOption.CaseInsensitive;
66 |
67 | public EnumValueNamingOption EnumValueNaming { get; set; }
68 |
69 | ///
70 | /// Determines builder class, data class and interfaces accessibility level.
71 | ///
72 | public MemberAccessibility MemberAccessibility { get; set; }
73 |
74 | ///
75 | /// This property is used for mapping GraphQL scalar type into specific .NET type. By default, any custom GraphQL scalar type is mapped into .
76 | ///
77 | public IScalarFieldTypeMappingProvider ScalarFieldTypeMappingProvider { get; set; }
78 |
79 | public bool FileScopedNamespaces { get; set; }
80 |
81 | public DataClassMemberNullability DataClassMemberNullability { get; set; }
82 |
83 | public GenerationOrder GenerationOrder { get; set; }
84 |
85 | public InputObjectMode InputObjectMode { get; set; }
86 |
87 | public GraphQlGeneratorConfiguration() => Reset();
88 |
89 | public void Reset()
90 | {
91 | ClassPrefix = null;
92 | ClassSuffix = null;
93 | CustomClassNameMapping.Clear();
94 | CSharpVersion = CSharpVersion.Compatible;
95 | ScalarFieldTypeMappingProvider = DefaultScalarFieldTypeMappingProvider.Instance;
96 | CodeDocumentationType = CodeDocumentationType.Disabled;
97 | IncludeDeprecatedFields = false;
98 | EnableNullableReferences = false;
99 | FloatTypeMapping = FloatTypeMapping.Decimal;
100 | BooleanTypeMapping = BooleanTypeMapping.Boolean;
101 | IntegerTypeMapping = IntegerTypeMapping.Int32;
102 | IdTypeMapping = IdTypeMapping.Guid;
103 | GeneratePartialClasses = true;
104 | MemberAccessibility = MemberAccessibility.Public;
105 | JsonPropertyGeneration = JsonPropertyGenerationOption.CaseInsensitive;
106 | PropertyGeneration = PropertyGenerationOption.AutoProperty;
107 | EnumValueNaming = EnumValueNamingOption.CSharp;
108 | FileScopedNamespaces = false;
109 | DataClassMemberNullability = DataClassMemberNullability.AlwaysNullable;
110 | GenerationOrder = GenerationOrder.DefinedBySchema;
111 | InputObjectMode = InputObjectMode.Rich;
112 | }
113 | }
114 |
115 | public enum EnumValueNamingOption
116 | {
117 | CSharp,
118 | Original
119 | }
120 |
121 | public enum CSharpVersion
122 | {
123 | Compatible,
124 | CSharp6,
125 | CSharp12
126 | }
127 |
128 | public enum FloatTypeMapping
129 | {
130 | Decimal,
131 | Float,
132 | Double,
133 | Custom
134 | }
135 |
136 | public enum BooleanTypeMapping
137 | {
138 | Boolean,
139 | Custom
140 | }
141 |
142 | public enum IntegerTypeMapping
143 | {
144 | Int16,
145 | Int32,
146 | Int64,
147 | Custom
148 | }
149 |
150 | public enum IdTypeMapping
151 | {
152 | Guid,
153 | String,
154 | Object,
155 | Custom
156 | }
157 |
158 | public enum MemberAccessibility
159 | {
160 | Public,
161 | Internal
162 | }
163 |
164 | public enum JsonPropertyGenerationOption
165 | {
166 | Never,
167 | Always,
168 | UseDefaultAlias,
169 | CaseInsensitive,
170 | CaseSensitive
171 | }
172 |
173 | public enum PropertyGenerationOption
174 | {
175 | AutoProperty,
176 | BackingField
177 | }
178 |
179 | [Flags]
180 | public enum CodeDocumentationType
181 | {
182 | Disabled = 0,
183 | XmlSummary = 1,
184 | DescriptionAttribute = 2
185 | }
186 |
187 | public enum DataClassMemberNullability
188 | {
189 | AlwaysNullable,
190 | DefinedBySchema
191 | }
192 |
193 | public enum GenerationOrder
194 | {
195 | DefinedBySchema,
196 | Alphabetical
197 | }
198 |
199 | public enum InputObjectMode
200 | {
201 | ///
202 | /// Supports GraphQL parameter references and explicit nulls
203 | ///
204 | Rich,
205 | Poco
206 | }
--------------------------------------------------------------------------------
/src/GraphQlClientGenerator/GraphQlIntrospection.cs:
--------------------------------------------------------------------------------
1 | namespace GraphQlClientGenerator;
2 |
3 | public static class GraphQlIntrospection
4 | {
5 | public const string QuerySupportedDirectives =
6 | """
7 | query DirectiveIntrospection {
8 | __schema {
9 | directives {
10 | name
11 | }
12 | }
13 | }
14 | """;
15 |
16 | public static string QuerySchemaMetadata(GraphQlWellKnownDirective directive) =>
17 | $$"""
18 | query FullIntrospection {
19 | __schema {
20 | queryType { name }
21 | mutationType { name }
22 | subscriptionType { name }
23 | types {
24 | ...FullType
25 | }
26 | directives {
27 | name
28 | description
29 | locations
30 | args {
31 | ...InputValue
32 | }
33 | }
34 | }
35 | }
36 |
37 | fragment FullType on __Type {
38 | kind
39 | name
40 | description
41 | fields(includeDeprecated: true) {
42 | name
43 | description
44 | args {
45 | ...InputValue
46 | }
47 | type {
48 | ...TypeRef
49 | }
50 | isDeprecated
51 | deprecationReason
52 | }
53 | inputFields {
54 | ...InputValue
55 | }
56 | interfaces {
57 | ...TypeRef
58 | }
59 | enumValues(includeDeprecated: true) {
60 | name
61 | description
62 | isDeprecated
63 | deprecationReason
64 | }
65 | possibleTypes {
66 | ...TypeRef
67 | }{{(directive.HasFlag(GraphQlWellKnownDirective.OneOf) ? $"{Environment.NewLine}isOneOf" : null)}}
68 | }
69 |
70 | fragment InputValue on __InputValue {
71 | name
72 | description
73 | type { ...TypeRef }
74 | defaultValue
75 | }
76 |
77 | fragment TypeRef on __Type {
78 | kind
79 | name
80 | ofType {
81 | kind
82 | name
83 | ofType {
84 | kind
85 | name
86 | ofType {
87 | kind
88 | name
89 | ofType {
90 | kind
91 | name
92 | ofType {
93 | kind
94 | name
95 | ofType {
96 | kind
97 | name
98 | }
99 | }
100 | }
101 | }
102 | }
103 | }
104 | }
105 | """;
106 | }
107 |
108 | [Flags]
109 | public enum GraphQlWellKnownDirective
110 | {
111 | None = 0,
112 | OneOf = 1
113 | }
--------------------------------------------------------------------------------
/src/GraphQlClientGenerator/GraphQlIntrospectionSchema.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using System.Runtime.Serialization;
3 | using Newtonsoft.Json;
4 |
5 | namespace GraphQlClientGenerator;
6 |
7 | public class GraphQlResult
8 | {
9 | public GraphQlData Data { get; set; }
10 | }
11 |
12 | public class GraphQlData
13 | {
14 | [JsonProperty("__schema")]
15 | public GraphQlSchema Schema { get; set; }
16 | }
17 |
18 | public class GraphQlSchema
19 | {
20 | public GraphQlRequestType QueryType { get; set; }
21 | public IList Types { get; set; }
22 | public IList Directives { get; set; }
23 | public GraphQlRequestType MutationType { get; set; }
24 | public GraphQlRequestType SubscriptionType { get; set; }
25 | }
26 |
27 | [DebuggerDisplay($"{nameof(GraphQlDirective)} ({nameof(Name)}={{{nameof(Name)},nq}}; {nameof(Description)}={{{nameof(Description)},nq}})")]
28 | public class GraphQlDirective : GraphQlTypeBase
29 | {
30 | public string Description { get; set; }
31 | public ICollection Locations { get; set; }
32 | public IList Args { get; set; }
33 | }
34 |
35 | public class GraphQlRequestType
36 | {
37 | public string Name { get; set; }
38 | }
39 |
40 | [DebuggerDisplay($"{nameof(GraphQlType)} ({nameof(Name)}={{{nameof(Name)},nq}}; {nameof(Kind)}={{{nameof(Kind)}}}; {nameof(Description)}={{{nameof(Description)},nq}})")]
41 | public class GraphQlType : GraphQlTypeBase
42 | {
43 | public string Description { get; set; }
44 | public IList Fields { get; set; }
45 | public IList InputFields { get; set; }
46 | public IList Interfaces { get; set; }
47 | public IList EnumValues { get; set; }
48 | public IList PossibleTypes { get; set; }
49 | }
50 |
51 | public abstract class GraphQlValueBase
52 | {
53 | public string Name { get; set; }
54 | public string Description { get; set; }
55 | }
56 |
57 | [DebuggerDisplay($"{nameof(GraphQlEnumValue)} ({nameof(Name)}={{{nameof(Name)},nq}}; {nameof(Description)}={{{nameof(Description)},nq}})")]
58 | public class GraphQlEnumValue : GraphQlValueBase
59 | {
60 | public bool IsDeprecated { get; set; }
61 | public string DeprecationReason { get; set; }
62 | }
63 |
64 | [DebuggerDisplay($"{nameof(GraphQlField)} ({nameof(Name)}={{{nameof(Name)},nq}}; {nameof(Description)}={{{nameof(Description)},nq}})")]
65 | public class GraphQlField : GraphQlEnumValue, IGraphQlMember
66 | {
67 | public IList Args { get; set; }
68 | public GraphQlFieldType Type { get; set; }
69 | }
70 |
71 | [DebuggerDisplay($"{nameof(GraphQlArgument)} ({nameof(Name)}={{{nameof(Name)},nq}}; {nameof(Description)}={{{nameof(Description)},nq}})")]
72 | public class GraphQlArgument : GraphQlValueBase, IGraphQlMember
73 | {
74 | public GraphQlFieldType Type { get; set; }
75 | public object DefaultValue { get; set; }
76 | }
77 |
78 | [DebuggerDisplay($"{nameof(GraphQlFieldType)} ({nameof(Name)}={{{nameof(Name)},nq}}; {nameof(Kind)}={{{nameof(Kind)}}})")]
79 | public class GraphQlFieldType : GraphQlTypeBase
80 | {
81 | public GraphQlFieldType OfType { get; set; }
82 |
83 | private bool Equals(GraphQlFieldType other) =>
84 | Kind == other.Kind && Name == other.Name && (OfType is null && other.OfType is null || OfType is not null && other.OfType is not null && OfType.Equals(other.OfType));
85 |
86 | public override bool Equals(object obj)
87 | {
88 | if (obj is null)
89 | return false;
90 |
91 | if (ReferenceEquals(this, obj))
92 | return true;
93 |
94 | return obj.GetType() == GetType() && Equals((GraphQlFieldType)obj);
95 | }
96 |
97 | public override int GetHashCode()
98 | {
99 | unchecked
100 | {
101 | var hashCode = OfType is null ? 0 : OfType.GetHashCode();
102 | hashCode = (hashCode * 397) ^ (int)Kind;
103 | hashCode = (hashCode * 397) ^ (Name is null ? 0 : Name.GetHashCode());
104 | return hashCode;
105 | }
106 | }
107 | }
108 |
109 | public abstract class GraphQlTypeBase
110 | {
111 | public const string GraphQlTypeScalarBoolean = "Boolean";
112 | public const string GraphQlTypeScalarFloat = "Float";
113 | public const string GraphQlTypeScalarId = "ID";
114 | public const string GraphQlTypeScalarInteger = "Int";
115 | public const string GraphQlTypeScalarString = "String";
116 |
117 | public GraphQlTypeKind Kind { get; set; }
118 | public string Name { get; set; }
119 |
120 | [JsonExtensionData]
121 | public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal);
122 | }
123 |
124 | public interface IGraphQlMember
125 | {
126 | string Name { get; }
127 | string Description { get; }
128 | GraphQlFieldType Type { get; }
129 | }
130 |
131 | public enum GraphQlDirectiveLocation
132 | {
133 | ///
134 | /// Location adjacent to a query operation.
135 | ///
136 | [EnumMember(Value = "QUERY")] Query,
137 | ///
138 | /// Location adjacent to a mutation operation.
139 | ///
140 | [EnumMember(Value = "MUTATION")] Mutation,
141 | ///
142 | /// Location adjacent to a subscription operation.
143 | ///
144 | [EnumMember(Value = "SUBSCRIPTION")] Subscription,
145 | ///
146 | /// Location adjacent to a field.
147 | ///
148 | [EnumMember(Value = "FIELD")] Field,
149 | ///
150 | /// Location adjacent to a fragment definition.
151 | ///
152 | [EnumMember(Value = "FRAGMENT_DEFINITION")] FragmentDefinition,
153 | ///
154 | /// Location adjacent to a fragment spread.
155 | ///
156 | [EnumMember(Value = "FRAGMENT_SPREAD")] FragmentSpread,
157 | ///
158 | /// Location adjacent to an inline fragment.
159 | ///
160 | [EnumMember(Value = "INLINE_FRAGMENT")] InlineFragment,
161 | ///
162 | /// Location adjacent to a variable definition.
163 | ///
164 | [EnumMember(Value = "VARIABLE_DEFINITION")] VariableDefinition,
165 | ///
166 | /// Location adjacent to a schema definition.
167 | ///
168 | [EnumMember(Value = "SCHEMA")] Schema,
169 | ///
170 | /// Location adjacent to a scalar definition.
171 | ///
172 | [EnumMember(Value = "SCALAR")] Scalar,
173 | ///
174 | /// Location adjacent to an object type definition.
175 | ///
176 | [EnumMember(Value = "OBJECT")] Object,
177 | ///
178 | /// Location adjacent to a field definition.
179 | ///
180 | [EnumMember(Value = "FIELD_DEFINITION")] FieldDefinition,
181 | ///
182 | /// Location adjacent to an argument definition.
183 | ///
184 | [EnumMember(Value = "ARGUMENT_DEFINITION")] ArgumentDefinition,
185 | ///
186 | /// Location adjacent to an interface definition.
187 | ///
188 | [EnumMember(Value = "INTERFACE")] Interface,
189 | ///
190 | /// Location adjacent to a union definition.
191 | ///
192 | [EnumMember(Value = "UNION")] Union,
193 | ///
194 | /// Location adjacent to an enum definition.
195 | ///
196 | [EnumMember(Value = "ENUM")] Enum,
197 | ///
198 | /// Location adjacent to an enum value definition.
199 | ///
200 | [EnumMember(Value = "ENUM_VALUE")] EnumValue,
201 | ///
202 | /// Location adjacent to an input object type definition.
203 | ///
204 | [EnumMember(Value = "INPUT_OBJECT")] InputObject,
205 | ///
206 | /// Location adjacent to an input field definition.
207 | ///
208 | [EnumMember(Value = "INPUT_FIELD_DEFINITION")] InputFieldDefinition
209 | }
--------------------------------------------------------------------------------
/src/GraphQlClientGenerator/GraphQlTypeKind.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.Serialization;
2 |
3 | namespace GraphQlClientGenerator;
4 |
5 | public enum GraphQlTypeKind
6 | {
7 | [EnumMember(Value = "SCALAR")] Scalar,
8 | [EnumMember(Value = "ENUM")] Enum,
9 | [EnumMember(Value = "OBJECT")] Object,
10 | [EnumMember(Value = "INPUT_OBJECT")] InputObject,
11 | [EnumMember(Value = "UNION")] Union,
12 | [EnumMember(Value = "INTERFACE")] Interface,
13 | [EnumMember(Value = "LIST")] List,
14 | [EnumMember(Value = "NON_NULL")] NonNull
15 | }
--------------------------------------------------------------------------------
/src/GraphQlClientGenerator/ICodeFileEmitter.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | namespace GraphQlClientGenerator;
4 |
5 | public interface ICodeFileEmitter
6 | {
7 | CodeFile CreateFile(string memberName);
8 |
9 | CodeFileInfo CollectFileInfo(CodeFile codeFile);
10 | }
11 |
12 | public struct CodeFileInfo
13 | {
14 | public string FileName { get; set; }
15 |
16 | public long Length { get; set; }
17 | }
18 |
19 | public class CodeFile : IDisposable
20 | {
21 | private Stream _stream;
22 | private StreamWriter _writer;
23 |
24 | public CodeFile(string fileName, Stream stream)
25 | {
26 | _stream = stream ?? throw new ArgumentNullException();
27 | _writer = new StreamWriter(_stream, Encoding.UTF8);
28 | FileName = fileName;
29 | }
30 |
31 | public string FileName { get; }
32 |
33 | public Stream Stream => _stream ?? throw new ObjectDisposedException(nameof(CodeFile));
34 |
35 | public TextWriter Writer => _writer ?? throw new ObjectDisposedException(nameof(CodeFile));
36 |
37 | public void Dispose()
38 | {
39 | _writer?.Dispose();
40 | _writer = null;
41 | _stream?.Dispose();
42 | _stream = null;
43 | }
44 | }
--------------------------------------------------------------------------------
/src/GraphQlClientGenerator/IScalarFieldTypeMappingProvider.cs:
--------------------------------------------------------------------------------
1 | namespace GraphQlClientGenerator;
2 |
3 | public interface IScalarFieldTypeMappingProvider
4 | {
5 | ScalarFieldTypeDescription GetCustomScalarFieldType(ScalarFieldTypeProviderContext context);
6 | }
7 |
8 | public sealed class DefaultScalarFieldTypeMappingProvider : IScalarFieldTypeMappingProvider
9 | {
10 | public static readonly DefaultScalarFieldTypeMappingProvider Instance = new();
11 |
12 | public ScalarFieldTypeDescription GetCustomScalarFieldType(ScalarFieldTypeProviderContext context)
13 | {
14 | var propertyName = NamingHelper.ToPascalCase(context.FieldName);
15 |
16 | if (propertyName is "From" or "ValidFrom" or "To" or "ValidTo" or "CreatedAt" or "UpdatedAt" or "ModifiedAt" or "DeletedAt" || propertyName.EndsWith("Timestamp"))
17 | return ScalarFieldTypeDescription.FromNetTypeName(GenerationContext.GetNullableNetTypeName(context, nameof(DateTimeOffset), false));
18 |
19 | return GetFallbackFieldType(context);
20 | }
21 |
22 | public static ScalarFieldTypeDescription GetFallbackFieldType(ScalarFieldTypeProviderContext context)
23 | {
24 | var fieldType = context.FieldType.UnwrapIfNonNull();
25 | if (fieldType.Kind is GraphQlTypeKind.Enum)
26 | return GenerationContext.GetDefaultEnumNetType(context);
27 |
28 | var dataType = fieldType.Name is GraphQlTypeBase.GraphQlTypeScalarString ? "string" : "object";
29 | return ScalarFieldTypeDescription.FromNetTypeName(GenerationContext.GetNullableNetTypeName(context, dataType, true));
30 | }
31 | }
--------------------------------------------------------------------------------
/src/GraphQlClientGenerator/KeyValueParameterParser.cs:
--------------------------------------------------------------------------------
1 | namespace GraphQlClientGenerator;
2 |
3 | public static class KeyValueParameterParser
4 | {
5 | public static bool TryGetCustomClassMapping(IEnumerable sourceParameters, out ICollection> customMapping, out string errorMessage)
6 | {
7 | customMapping = new List>();
8 |
9 | foreach (var parameter in sourceParameters ?? Enumerable.Empty())
10 | {
11 | var parts = parameter.Split(':');
12 | if (parts.Length != 2)
13 | {
14 | errorMessage = "\"classMapping\" value must have format {GraphQlTypeName}:{C#ClassName}. ";
15 | return false;
16 | }
17 |
18 | var cSharpClassName = parts[1];
19 | if (!CSharpHelper.IsValidIdentifier(cSharpClassName))
20 | {
21 | errorMessage = $"\"{cSharpClassName}\" is not valid C# class name. ";
22 | return false;
23 | }
24 |
25 | customMapping.Add(new KeyValuePair(parts[0], cSharpClassName));
26 | }
27 |
28 | errorMessage = null;
29 | return true;
30 | }
31 |
32 | public static bool TryGetCustomHeaders(IEnumerable sourceParameters, out ICollection> headers, out string errorMessage)
33 | {
34 | headers = new List>();
35 |
36 | foreach (var parameter in sourceParameters ?? [])
37 | {
38 | var parts = parameter.Split([':'], 2);
39 | if (parts.Length != 2)
40 | {
41 | errorMessage = "\"header\" value must have format {Header}:{Value}. ";
42 | return false;
43 | }
44 |
45 | headers.Add(new KeyValuePair(parts[0], parts[1]));
46 | }
47 |
48 | errorMessage = null;
49 | return true;
50 | }
51 | }
--------------------------------------------------------------------------------
/src/GraphQlClientGenerator/MultipleFileGenerationContext.cs:
--------------------------------------------------------------------------------
1 | namespace GraphQlClientGenerator;
2 |
3 | public class MultipleFileGenerationContext : GenerationContext
4 | {
5 | private const string ProjectTemplate =
6 | $"""
7 |
8 |
9 |
10 | netstandard2.0
11 | latest
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | """;
21 |
22 | private const string RequiredNamespaces =
23 | $"""
24 | using System;
25 | using System.Collections.Generic;
26 | using System.ComponentModel;
27 | using System.Globalization;
28 | using System.Runtime.Serialization;
29 | #if !{GraphQlGenerator.PreprocessorDirectiveDisableNewtonsoftJson}
30 | using Newtonsoft.Json;
31 | #endif
32 |
33 | """;
34 |
35 | private readonly ICodeFileEmitter _codeFileEmitter;
36 | private readonly string _projectFileName;
37 |
38 | private CodeFile _currentFile;
39 |
40 | protected internal override TextWriter Writer =>
41 | (_currentFile ?? throw new InvalidOperationException($"\"{nameof(Writer)}\" not initialized")).Writer;
42 |
43 | public override byte IndentationSize => (byte)(Configuration.FileScopedNamespaces ? 0 : 4);
44 |
45 | public MultipleFileGenerationContext(
46 | GraphQlSchema schema,
47 | ICodeFileEmitter codeFileEmitter,
48 | string projectFileName = null,
49 | GeneratedObjectType objectTypes = GeneratedObjectType.All)
50 | : base(schema, objectTypes)
51 | {
52 | _codeFileEmitter = codeFileEmitter ?? throw new ArgumentNullException(nameof(codeFileEmitter));
53 |
54 | if (projectFileName is not null && !projectFileName.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase))
55 | throw new ArgumentException("Project file name must end with .csproj.", nameof(projectFileName));
56 |
57 | _projectFileName = projectFileName;
58 | }
59 |
60 | public override void BeforeGeneration()
61 | {
62 | }
63 |
64 | public override void BeforeBaseClassGeneration() => InitializeNewSourceCodeFile("BaseClasses", GraphQlGenerator.RequiredNamespaces);
65 |
66 | public override void AfterBaseClassGeneration() => WriteNamespaceEnd();
67 |
68 | public override void BeforeGraphQlTypeNameGeneration() => InitializeNewSourceCodeFile("GraphQlTypes");
69 |
70 | public override void AfterGraphQlTypeNameGeneration() => WriteNamespaceEnd();
71 |
72 | public override void BeforeEnumsGeneration()
73 | {
74 | }
75 |
76 | public override void BeforeEnumGeneration(ObjectGenerationContext context) => InitializeNewSourceCodeFile(context.CSharpTypeName);
77 |
78 | public override void AfterEnumGeneration(ObjectGenerationContext context) => WriteNamespaceEnd();
79 |
80 | public override void AfterEnumsGeneration()
81 | {
82 | }
83 |
84 | public override void BeforeDirectivesGeneration()
85 | {
86 | }
87 |
88 | public override void BeforeDirectiveGeneration(ObjectGenerationContext context) => InitializeNewSourceCodeFile(context.CSharpTypeName);
89 |
90 | public override void AfterDirectiveGeneration(ObjectGenerationContext context) => WriteNamespaceEnd();
91 |
92 | public override void AfterDirectivesGeneration()
93 | {
94 | }
95 |
96 | public override void BeforeQueryBuildersGeneration()
97 | {
98 | }
99 |
100 | public override void BeforeQueryBuilderGeneration(ObjectGenerationContext context) => InitializeNewSourceCodeFile(context.CSharpTypeName);
101 |
102 | public override void AfterQueryBuilderGeneration(ObjectGenerationContext context) => WriteNamespaceEnd();
103 |
104 | public override void AfterQueryBuildersGeneration()
105 | {
106 | }
107 |
108 | public override void BeforeInputClassesGeneration()
109 | {
110 | }
111 |
112 | public override void AfterInputClassesGeneration()
113 | {
114 | }
115 |
116 | public override void BeforeDataClassesGeneration()
117 | {
118 | }
119 |
120 | public override void BeforeDataClassGeneration(ObjectGenerationContext context) => InitializeNewSourceCodeFile(context.CSharpTypeName);
121 |
122 | public override void OnDataClassConstructorGeneration(ObjectGenerationContext context)
123 | {
124 | }
125 |
126 | public override void AfterDataClassGeneration(ObjectGenerationContext context) => WriteNamespaceEnd();
127 |
128 | public override void AfterDataClassesGeneration()
129 | {
130 | }
131 |
132 | public override void BeforeDataPropertyGeneration(PropertyGenerationContext context)
133 | {
134 | }
135 |
136 | public override void AfterDataPropertyGeneration(PropertyGenerationContext context)
137 | {
138 | }
139 |
140 | public override void AfterGeneration()
141 | {
142 | CollectCurrentFile();
143 |
144 | if (String.IsNullOrEmpty(_projectFileName))
145 | return;
146 |
147 | var projectFile = _codeFileEmitter.CreateFile(_projectFileName);
148 | projectFile.Writer.Write(ProjectTemplate);
149 | LogFileCreation(_codeFileEmitter.CollectFileInfo(projectFile));
150 | }
151 |
152 | private void InitializeNewSourceCodeFile(string memberName, string requiredNamespaces = RequiredNamespaces)
153 | {
154 | CollectCurrentFile();
155 |
156 | _currentFile = _codeFileEmitter.CreateFile($"{memberName}.cs");
157 |
158 | var writer = _currentFile.Writer;
159 | writer.WriteLine(GraphQlGenerator.AutoGeneratedLabel);
160 | writer.WriteLine();
161 | writer.WriteLine(requiredNamespaces);
162 | writer.Write("namespace ");
163 | writer.Write(Configuration.TargetNamespace);
164 |
165 | if (Configuration.FileScopedNamespaces)
166 | {
167 | writer.WriteLine(';');
168 | writer.WriteLine();
169 | }
170 | else
171 | {
172 | writer.WriteLine();
173 | writer.WriteLine('{');
174 | }
175 | }
176 |
177 | private void WriteNamespaceEnd() => _currentFile.Writer.WriteLine(Configuration.FileScopedNamespaces ? String.Empty : "}");
178 |
179 | private void CollectCurrentFile()
180 | {
181 | if (_currentFile is not null)
182 | LogFileCreation(_codeFileEmitter.CollectFileInfo(_currentFile));
183 |
184 | _currentFile = null;
185 | }
186 |
187 | private void LogFileCreation(CodeFileInfo fileInfo) =>
188 | Log($"File {fileInfo.FileName} generated successfully ({fileInfo.Length:N0} B). ");
189 | }
190 |
191 | public class FileSystemEmitter : ICodeFileEmitter
192 | {
193 | private readonly string _outputDirectory;
194 |
195 | public FileSystemEmitter(string outputDirectory)
196 | {
197 | if (!Directory.Exists(outputDirectory))
198 | throw new ArgumentException($"Directory \"{outputDirectory}\" does not exist.", nameof(outputDirectory));
199 |
200 | _outputDirectory = outputDirectory;
201 | }
202 |
203 | public CodeFile CreateFile(string fileName)
204 | {
205 | fileName = Path.Combine(_outputDirectory, fileName);
206 | return new CodeFile(fileName, File.Create(fileName));
207 | }
208 |
209 | public CodeFileInfo CollectFileInfo(CodeFile codeFile)
210 | {
211 | codeFile.Writer.Flush();
212 | codeFile.Dispose();
213 |
214 | return
215 | new CodeFileInfo
216 | {
217 | FileName = codeFile.FileName,
218 | Length = (int)new FileInfo(codeFile.FileName).Length
219 | };
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/src/GraphQlClientGenerator/NamingHelper.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using System.Text.RegularExpressions;
3 |
4 | namespace GraphQlClientGenerator;
5 |
6 | internal static class NamingHelper
7 | {
8 | internal const string MetadataFieldTypeName = "__typename";
9 |
10 | private static readonly char[] UnderscoreSeparator = ['_'];
11 |
12 | public static string LowerFirst(string value) => $"{Char.ToLowerInvariant(value[0])}{value.Substring(1)}";
13 |
14 | private static readonly Regex RegexInvalidCharacters = new("[^_a-zA-Z0-9]");
15 | private static readonly Regex RegexNextWhiteSpace = new(@"(?<=\s)");
16 | private static readonly Regex RegexWhiteSpace = new(@"\s");
17 | private static readonly Regex RegexUpperCaseFirstLetter = new("^[a-z]");
18 | private static readonly Regex RegexFirstCharFollowedByUpperCasesOnly = new("(?<=[A-Z])[A-Z0-9]+$");
19 | private static readonly Regex RegexLowerCaseNextToNumber = new("(?<=[0-9])[a-z]");
20 | private static readonly Regex RegexUpperCaseInside = new("(?<=[A-Z])[A-Z]+?((?=[A-Z][a-z])|(?=[0-9]))");
21 |
22 | /// https://stackoverflow.com/questions/18627112/how-can-i-convert-text-to-pascal-case>
23 | public static string ToPascalCase(string text)
24 | {
25 | if (text is MetadataFieldTypeName)
26 | return "TypeName";
27 |
28 | var textWithoutWhiteSpace = RegexInvalidCharacters.Replace(RegexWhiteSpace.Replace(text, String.Empty), String.Empty);
29 | if (textWithoutWhiteSpace.All(c => c is '_'))
30 | return textWithoutWhiteSpace;
31 |
32 | var pascalCase =
33 | RegexInvalidCharacters
34 | // Replaces white spaces with underscore, then replace all invalid chars with an empty string.
35 | .Replace(RegexNextWhiteSpace.Replace(text, "_"), String.Empty)
36 | .Split(UnderscoreSeparator, StringSplitOptions.RemoveEmptyEntries)
37 | .Select(w => RegexUpperCaseFirstLetter.Replace(w, m => m.Value.ToUpper()))
38 | // Replace second and all following upper case letters to lower if there is no next lower (ABC -> Abc).
39 | .Select(w => RegexFirstCharFollowedByUpperCasesOnly.Replace(w, m => m.Value.ToLower()))
40 | // Set upper case the first lower case following a number (Ab9cd -> Ab9Cd).
41 | .Select(w => RegexLowerCaseNextToNumber.Replace(w, m => m.Value.ToUpper()))
42 | // Lower second and next upper case letters except the last if it follows by any lower (ABcDEf -> AbcDef).
43 | .Select(w => RegexUpperCaseInside.Replace(w, m => m.Value.ToLower()));
44 |
45 | return String.Concat(pascalCase);
46 | }
47 |
48 | public static string ToCSharpEnumName(string name)
49 | {
50 | var builder = new StringBuilder();
51 | var startNewWord = true;
52 | var hasLowerLetters = false;
53 | var hasUpperLetters = false;
54 | var length = name?.Length ?? throw new ArgumentNullException(nameof(name));
55 |
56 | for (var i = 0; i < length; i++)
57 | {
58 | var @char = name[i];
59 | if (@char is '_')
60 | {
61 | startNewWord = true;
62 |
63 | if (i == 0 && length > 1 && Char.IsDigit(name[i + 1]))
64 | builder.Append('_');
65 |
66 | continue;
67 | }
68 |
69 | hasLowerLetters |= Char.IsLower(@char);
70 | hasUpperLetters |= Char.IsUpper(@char);
71 |
72 | builder.Append(startNewWord ? Char.ToUpper(@char) : Char.ToLower(@char));
73 |
74 | startNewWord = Char.IsDigit(@char);
75 | }
76 |
77 | return hasLowerLetters && hasUpperLetters ? name : builder.ToString();
78 | }
79 | }
--------------------------------------------------------------------------------
/src/GraphQlClientGenerator/OutputType.cs:
--------------------------------------------------------------------------------
1 | namespace GraphQlClientGenerator;
2 |
3 | public enum OutputType
4 | {
5 | SingleFile,
6 | OneClassPerFile
7 | }
--------------------------------------------------------------------------------
/src/GraphQlClientGenerator/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 | [assembly: InternalsVisibleTo("GraphQlClientGenerator.Test")]
3 |
--------------------------------------------------------------------------------
/src/GraphQlClientGenerator/RegexScalarFieldTypeMappingProvider.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using System.Text.RegularExpressions;
3 |
4 | namespace GraphQlClientGenerator;
5 |
6 | public class RegexScalarFieldTypeMappingProvider(IReadOnlyCollection rules) : IScalarFieldTypeMappingProvider
7 | {
8 | private readonly IReadOnlyCollection _rules = rules ?? throw new ArgumentNullException(nameof(rules));
9 |
10 | public static IReadOnlyCollection ParseRulesFromJson(string json) =>
11 | JsonConvert.DeserializeObject>(json) ?? [];
12 |
13 | public ScalarFieldTypeDescription GetCustomScalarFieldType(ScalarFieldTypeProviderContext context)
14 | {
15 | var valueType = context.FieldType.UnwrapIfNonNull();
16 |
17 | foreach (var rule in _rules)
18 | if (Regex.IsMatch(context.FieldName, rule.PatternValueName) &&
19 | Regex.IsMatch(context.OwnerType.Name, rule.PatternBaseType) &&
20 | Regex.IsMatch(valueType.Name ?? String.Empty, rule.PatternValueType))
21 | return
22 | new ScalarFieldTypeDescription
23 | {
24 | NetTypeName = GenerationContext.GetNullableNetTypeName(context, rule.NetTypeName, rule.IsReferenceType),
25 | FormatMask = rule.FormatMask
26 | };
27 |
28 | return DefaultScalarFieldTypeMappingProvider.GetFallbackFieldType(context);
29 | }
30 | }
31 |
32 | public class RegexScalarFieldTypeMappingRule
33 | {
34 | public string PatternBaseType { get; set; }
35 | public string PatternValueType { get; set; }
36 | public string PatternValueName { get; set; }
37 | public string NetTypeName { get; set; }
38 | public bool IsReferenceType { get; set; }
39 | public string FormatMask { get; set; }
40 | }
--------------------------------------------------------------------------------
/src/GraphQlClientGenerator/SingleFileGenerationContext.cs:
--------------------------------------------------------------------------------
1 | namespace GraphQlClientGenerator;
2 |
3 | public class SingleFileGenerationContext(GraphQlSchema schema, TextWriter writer, GeneratedObjectType objectTypes = GeneratedObjectType.All)
4 | : GenerationContext(schema, objectTypes)
5 | {
6 | private bool _isNullableReferenceScopeEnabled;
7 | private int _enums;
8 | private int _directives;
9 | private int _queryBuilders;
10 | private int _dataClasses;
11 |
12 | public override byte IndentationSize => (byte)(Configuration.FileScopedNamespaces ? 0 : 4);
13 |
14 | protected internal override TextWriter Writer { get; } = writer ?? throw new ArgumentNullException(nameof(writer));
15 |
16 | public override void BeforeGeneration()
17 | {
18 | _enums = _directives = _queryBuilders = _dataClasses = 0;
19 |
20 | Writer.WriteLine(GraphQlGenerator.AutoGeneratedLabel);
21 | Writer.WriteLine();
22 | Writer.WriteLine(GraphQlGenerator.RequiredNamespaces);
23 | Writer.Write("namespace ");
24 | Writer.Write(Configuration.TargetNamespace);
25 |
26 | if (Configuration.FileScopedNamespaces)
27 | {
28 | Writer.WriteLine(";");
29 | Writer.WriteLine();
30 | }
31 | else
32 | {
33 | Writer.WriteLine();
34 | Writer.WriteLine("{");
35 | }
36 | }
37 |
38 | public override void BeforeBaseClassGeneration() => WriteLine("#region base classes");
39 |
40 | public override void AfterBaseClassGeneration()
41 | {
42 | WriteLine("#endregion");
43 | Writer.WriteLine();
44 | }
45 |
46 | public override void BeforeGraphQlTypeNameGeneration() => WriteLine("#region GraphQL type helpers");
47 |
48 | public override void AfterGraphQlTypeNameGeneration()
49 | {
50 | WriteLine("#endregion");
51 | Writer.WriteLine();
52 | }
53 |
54 | public override void BeforeEnumsGeneration() => WriteLine("#region enums");
55 |
56 | public override void BeforeEnumGeneration(ObjectGenerationContext context)
57 | {
58 | if (_enums > 0)
59 | Writer.WriteLine();
60 | }
61 |
62 | public override void AfterEnumGeneration(ObjectGenerationContext context) => _enums++;
63 |
64 | public override void AfterEnumsGeneration()
65 | {
66 | WriteLine("#endregion");
67 | Writer.WriteLine();
68 | }
69 |
70 | public override void BeforeDirectivesGeneration()
71 | {
72 | EnterNullableReferenceScope();
73 | WriteLine("#region directives");
74 | }
75 |
76 | public override void BeforeDirectiveGeneration(ObjectGenerationContext context)
77 | {
78 | if (_directives > 0)
79 | Writer.WriteLine();
80 | }
81 |
82 | public override void AfterDirectiveGeneration(ObjectGenerationContext context) => _directives++;
83 |
84 | public override void AfterDirectivesGeneration()
85 | {
86 | WriteLine("#endregion");
87 | Writer.WriteLine();
88 | }
89 |
90 | public override void BeforeQueryBuildersGeneration()
91 | {
92 | EnterNullableReferenceScope();
93 | WriteLine("#region builder classes");
94 | }
95 |
96 | public override void BeforeQueryBuilderGeneration(ObjectGenerationContext context)
97 | {
98 | if (_queryBuilders > 0)
99 | Writer.WriteLine();
100 | }
101 |
102 | public override void AfterQueryBuilderGeneration(ObjectGenerationContext context) => _queryBuilders++;
103 |
104 | public override void AfterQueryBuildersGeneration()
105 | {
106 | WriteLine("#endregion");
107 | Writer.WriteLine();
108 | }
109 |
110 | public override void BeforeInputClassesGeneration()
111 | {
112 | EnterNullableReferenceScope();
113 | WriteLine("#region input classes");
114 | }
115 |
116 | public override void AfterInputClassesGeneration()
117 | {
118 | WriteLine("#endregion");
119 | Writer.WriteLine();
120 | }
121 |
122 | public override void BeforeDataClassesGeneration()
123 | {
124 | _dataClasses = 0;
125 | EnterNullableReferenceScope();
126 | WriteLine("#region data classes");
127 | }
128 |
129 | public override void BeforeDataClassGeneration(ObjectGenerationContext context)
130 | {
131 | if (_dataClasses > 0)
132 | Writer.WriteLine();
133 | }
134 |
135 | public override void OnDataClassConstructorGeneration(ObjectGenerationContext context)
136 | {
137 | }
138 |
139 | public override void AfterDataClassGeneration(ObjectGenerationContext context) => _dataClasses++;
140 |
141 | public override void AfterDataClassesGeneration() => WriteLine("#endregion");
142 |
143 | public override void BeforeDataPropertyGeneration(PropertyGenerationContext context)
144 | {
145 | }
146 |
147 | public override void AfterDataPropertyGeneration(PropertyGenerationContext context)
148 | {
149 | }
150 |
151 | public override void AfterGeneration()
152 | {
153 | ExitNullableReferenceScope();
154 |
155 | if (!Configuration.FileScopedNamespaces)
156 | Writer.WriteLine("}");
157 | }
158 |
159 | private void EnterNullableReferenceScope()
160 | {
161 | if (_isNullableReferenceScopeEnabled || !Configuration.EnableNullableReferences)
162 | return;
163 |
164 | WriteLine("#nullable enable");
165 | _isNullableReferenceScopeEnabled = true;
166 | }
167 |
168 | private void ExitNullableReferenceScope()
169 | {
170 | if (!_isNullableReferenceScopeEnabled)
171 | return;
172 |
173 | WriteLine("#nullable restore");
174 | _isNullableReferenceScopeEnabled = false;
175 | }
176 |
177 | private void WriteLine(string text)
178 | {
179 | Writer.Write(GraphQlGenerator.GetIndentation(IndentationSize));
180 | Writer.WriteLine(text);
181 | }
182 | }
--------------------------------------------------------------------------------
/test/GraphQlClientGenerator.Test/CompilationHelper.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel;
2 | using System.Reflection;
3 | using System.Runtime.Serialization;
4 | using Microsoft.CodeAnalysis;
5 | using Microsoft.CodeAnalysis.CSharp;
6 |
7 | namespace GraphQlClientGenerator.Test;
8 |
9 | internal static class CompilationHelper
10 | {
11 | public static CSharpCompilation CreateCompilation(string sourceCode, string assemblyName, NullableContextOptions nullableContextOptions = NullableContextOptions.Disable)
12 | {
13 | var syntaxTree =
14 | SyntaxFactory.ParseSyntaxTree(
15 | $$"""
16 | {{GraphQlGenerator.RequiredNamespaces}}
17 |
18 | namespace {{assemblyName}}
19 | {
20 | {{sourceCode}}
21 | }
22 | """,
23 | CSharpParseOptions.Default.WithLanguageVersion(Enum.GetValues(typeof(LanguageVersion)).Cast().Max()));
24 |
25 | var compilationOptions =
26 | new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: nullableContextOptions)
27 | .WithPlatform(Platform.AnyCpu)
28 | .WithOverflowChecks(true)
29 | .WithOptimizationLevel(OptimizationLevel.Release)
30 | .WithSpecificDiagnosticOptions(
31 | new Dictionary
32 | {
33 | { "CS1701", ReportDiagnostic.Suppress }
34 | });
35 |
36 | var systemReference = MetadataReference.CreateFromFile(typeof(DateTimeOffset).Assembly.Location);
37 | var systemObjectModelReference = MetadataReference.CreateFromFile(Assembly.Load("System.ObjectModel").Location);
38 | var systemTextRegularExpressionsReference = MetadataReference.CreateFromFile(Assembly.Load("System.Text.RegularExpressions").Location);
39 | var systemRuntimeReference = MetadataReference.CreateFromFile(Assembly.Load("System.Runtime").Location);
40 | var systemCollectionsReference = MetadataReference.CreateFromFile(Assembly.Load("System.Collections").Location);
41 | var systemGlobalizationReference = MetadataReference.CreateFromFile(Assembly.Load("System.Globalization").Location);
42 | var systemRuntimeExtensionsReference = MetadataReference.CreateFromFile(Assembly.Load("System.Runtime.Extensions").Location);
43 | var netStandardReference = MetadataReference.CreateFromFile(Assembly.Load("netstandard").Location);
44 | var linqReference = MetadataReference.CreateFromFile(Assembly.Load("System.Linq").Location);
45 | var linqExpressionsReference = MetadataReference.CreateFromFile(Assembly.Load("System.Linq.Expressions").Location);
46 | var systemDynamicRuntimeReference = MetadataReference.CreateFromFile(Assembly.Load("System.Dynamic.Runtime").Location);
47 | var systemIoReference = MetadataReference.CreateFromFile(Assembly.Load("System.IO").Location);
48 | var jsonNetReference = MetadataReference.CreateFromFile(Assembly.Load("Newtonsoft.Json").Location);
49 | var runtimeSerializationReference = MetadataReference.CreateFromFile(typeof(EnumMemberAttribute).Assembly.Location);
50 | var componentModelReference = MetadataReference.CreateFromFile(typeof(DescriptionAttribute).Assembly.Location);
51 | var componentModelTypeConverterReference = MetadataReference.CreateFromFile(Assembly.Load("System.ComponentModel.TypeConverter").Location);
52 |
53 | return
54 | CSharpCompilation.Create(
55 | assemblyName,
56 | [syntaxTree],
57 | [
58 | systemReference,
59 | systemIoReference,
60 | systemDynamicRuntimeReference,
61 | runtimeSerializationReference,
62 | systemObjectModelReference,
63 | systemTextRegularExpressionsReference,
64 | componentModelReference,
65 | componentModelTypeConverterReference,
66 | systemRuntimeReference,
67 | systemRuntimeExtensionsReference,
68 | systemCollectionsReference,
69 | systemGlobalizationReference,
70 | jsonNetReference,
71 | linqReference,
72 | linqExpressionsReference,
73 | netStandardReference
74 | ],
75 | compilationOptions);
76 |
77 | }
78 | }
--------------------------------------------------------------------------------
/test/GraphQlClientGenerator.Test/ExpectedMultipleFilesContext/Avatar:
--------------------------------------------------------------------------------
1 | // This file has been auto generated.
2 |
3 | using System;
4 | using System.Collections.Generic;
5 | using System.ComponentModel;
6 | using System.Globalization;
7 | using System.Runtime.Serialization;
8 | #if !GRAPHQL_GENERATOR_DISABLE_NEWTONSOFT_JSON
9 | using Newtonsoft.Json;
10 | #endif
11 |
12 | namespace GraphQlGeneratorTest
13 | {
14 | public enum Avatar
15 | {
16 | [EnumMember(Value = "floorhouse1")] Floorhouse1,
17 | [EnumMember(Value = "floorhouse2")] Floorhouse2,
18 | [EnumMember(Value = "floorhouse3")] Floorhouse3,
19 | [EnumMember(Value = "castle")] Castle,
20 | [EnumMember(Value = "apartment")] Apartment,
21 | [EnumMember(Value = "cottage")] Cottage,
22 | [EnumMember(Value = "rowhouse")] Rowhouse
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/test/GraphQlClientGenerator.Test/ExpectedMultipleFilesContext/Avatar.FileScoped:
--------------------------------------------------------------------------------
1 | // This file has been auto generated.
2 |
3 | using System;
4 | using System.Collections.Generic;
5 | using System.ComponentModel;
6 | using System.Globalization;
7 | using System.Runtime.Serialization;
8 | #if !GRAPHQL_GENERATOR_DISABLE_NEWTONSOFT_JSON
9 | using Newtonsoft.Json;
10 | #endif
11 |
12 | namespace GraphQlGeneratorTest;
13 |
14 | public enum Avatar
15 | {
16 | [EnumMember(Value = "floorhouse1")] Floorhouse1,
17 | [EnumMember(Value = "floorhouse2")] Floorhouse2,
18 | [EnumMember(Value = "floorhouse3")] Floorhouse3,
19 | [EnumMember(Value = "castle")] Castle,
20 | [EnumMember(Value = "apartment")] Apartment,
21 | [EnumMember(Value = "cottage")] Cottage,
22 | [EnumMember(Value = "rowhouse")] Rowhouse
23 | }
24 |
25 |
--------------------------------------------------------------------------------
/test/GraphQlClientGenerator.Test/ExpectedMultipleFilesContext/Home:
--------------------------------------------------------------------------------
1 | // This file has been auto generated.
2 |
3 | using System;
4 | using System.Collections.Generic;
5 | using System.ComponentModel;
6 | using System.Globalization;
7 | using System.Runtime.Serialization;
8 | #if !GRAPHQL_GENERATOR_DISABLE_NEWTONSOFT_JSON
9 | using Newtonsoft.Json;
10 | #endif
11 |
12 | namespace GraphQlGeneratorTest
13 | {
14 | public partial class Home
15 | {
16 | public Guid? Id { get; set; }
17 | public Avatar? Avatar { get; set; }
18 | public string TimeZone { get; set; }
19 | public string Title { get; set; }
20 | public string Type { get; set; }
21 | public bool? HasEnergyDeal { get; set; }
22 | public Address Address { get; set; }
23 | public Subscription Subscription { get; set; }
24 | public ICollection ConsumptionMonths { get; set; }
25 | public Consumption Consumption { get; set; }
26 | public PreLiveComparison PreLiveComparison { get; set; }
27 | public ICollection Comparisons { get; set; }
28 | public Comparison ComparisonCurrentMonth { get; set; }
29 | public ICollection ProfileQuestions { get; set; }
30 | public ICollection