This simple console app generates C# GraphQL query builder and data classes for simple, compiler checked, usage of a GraphQL API.
GraphQlClientGenerator.Console --serviceUrl <GraphQlServiceUrl> --outputPath <TargetPath> --namespace <TargetNamespace> [--header <header value>]
Installation:
Install-Package GraphQlClientGenerator
dotnet tool install GraphQlClientGenerator.Tool --global
graphql-client-generator --serviceUrl <GraphQlServiceUrl> --outputPath <TargetPath> --namespace <TargetNamespace> [--header <header value>]
Code example for class generation:
var schema = await GraphQlGenerator.RetrieveSchema(url);
var generator = new GraphQlGenerator();
var generatedClasses = generator.Generate(schema);
or
var schema = await GraphQlGenerator.RetrieveSchema(url);
var csharpCode = new GraphQlGenerator().GenerateFullClientCSharpFile(schema, "MyGqlApiClient");
await File.WriteAllTextAsync("MyGqlApiClient.cs", csharpCode);
C# 9 introduced source generators that can be attached to compilation process. Generated classes will be automatically included in project.
Project file example:
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<!-- GraphQL generator properties -->
<GraphQlClientGenerator_ServiceUrl>https://api.tibber.com/v1-beta/gql</GraphQlClientGenerator_ServiceUrl>
<!-- GraphQlClientGenerator_Namespace is optional; if omitted the first compilation unit namespace will be used -->
<GraphQlClientGenerator_Namespace>$(RootNamespace)</GraphQlClientGenerator_Namespace>
<GraphQlClientGenerator_CustomClassMapping>Consumption:ConsumptionEntry|Production:ProductionEntry|RootMutation:TibberMutation|Query:Tibber</GraphQlClientGenerator_CustomClassMapping>
<!-- other GraphQL generator property values -->
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GraphQlClientGenerator" Version="0.9.*" IncludeAssets="analyzers" />
<!-- AdditionalFiles and GraphQlClientGenerator_ServiceUrl are mutually exclusive -->
<!-- <AdditionalFiles Include="GqlSchemaTibberApi.gql.schema.json" CacheObjects="true" /> --> <!-- schema file name must end with ".gql.schema.json" -->
<CompilerVisibleProperty Include="GraphQlClientGenerator_ServiceUrl" />
<CompilerVisibleProperty Include="GraphQlClientGenerator_Namespace" />
<!-- other configuration properties -->
<!--<CompilerVisibleProperty Include="GraphQlClientGenerator_{ConfigurationProperty}" />-->
</ItemGroup>
var builder =
new QueryQueryBuilder()
.WithMe(
new MeQueryBuilder()
.WithAllScalarFields()
.WithHome(
new HomeQueryBuilder()
.WithAllScalarFields()
.WithSubscription(
new SubscriptionQueryBuilder()
.WithStatus()
.WithValidFrom())
.WithSignupStatus(
new SignupStatusQueryBuilder().WithAllFields())
.WithDisaggregation(
new DisaggregationQueryBuilder().WithAllFields()),
"b420001d-189b-44c0-a3d5-d62452bfdd42")
.WithEnergyStatements ("2016-06", "2016-10"));
var query = builder.Build(Formatting.Indented);
results into
query {
me {
id
firstName
lastName
fullName
ssn
email
language
tone
home (id: "b420001d-189b-44c0-a3d5-d62452bfdd42") {
id
avatar
timeZone
subscription {
status
validFrom
}
signupStatus {
registrationStartedTimestamp
registrationCompleted
registrationCompletedTimestamp
checkCurrentSupplierPassed
supplierSwitchConfirmationPassed
startDatePassed
firstReadingReceived
firstBillingDone
firstBillingTimestamp
}
disaggregation {
year
month
fixedConsumptionKwh
fixedConsumptionKwhPercent
heatingConsumptionKwh
heatingConsumptionKwhPercent
behaviorConsumptionKwh
behaviorConsumptionKwhPercent
}
}
energyStatements(from: "2016-06", to: "2016-10")
}
}
var mutation =
new MutationQueryBuilder()
.WithUpdateHome(
new HomeQueryBuilder().WithAllScalarFields(),
new UpdateHomeInput { HomeId = Guid.Empty, AppNickname = "My nickname", Type = HomeType.House, NumberOfResidents = 4, Size = 160, AppAvatar = HomeAvatar.Floorhouse1, PrimaryHeatingSource = HeatingSource.Electricity }
)
.Build(Formatting.Indented, 2);
result:
mutation {
updateHome (input: {
homeId: "00000000-0000-0000-0000-000000000000"
appNickname: "My nickname"
appAvatar: FLOORHOUSE1
size: 160
type: HOUSE
numberOfResidents: 4
primaryHeatingSource: ELECTRICITY
}) {
id
timeZone
appNickname
appAvatar
size
type
numberOfResidents
primaryHeatingSource
hasVentilationSystem
}
}
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
.
new ViewerQueryBuilder()
.WithHomes(
new HomeQueryBuilder()
.WithAllScalarFields()
.ExceptPrimaryHeatingSource()
.ExceptMainFuseSize()
)
.Build(Formatting.Indented);
result:
query {
homes {
id
timeZone
appNickname
appAvatar
size
type
numberOfResidents
hasVentilationSystem
}
}
Queried fields can be freely renamed to match target data classes using GraphQL aliases.
new ViewerQueryBuilder("MyQuery")
.WithHome(
new HomeQueryBuilder()
.WithType()
.WithSize()
.WithAddress(new AddressQueryBuilder().WithAddress1("primaryAddressText").WithCountry(), "primaryAddress"),
Guid.NewGuid(),
"primaryHome")
.WithHome(
new HomeQueryBuilder()
.WithType()
.WithSize()
.WithAddress(new AddressQueryBuilder().WithAddress1("secondaryAddressText").WithCountry(), "secondaryAddress"),
Guid.NewGuid(),
"secondaryHome")
.Build(Formatting.Indented);
result:
query MyQuery {
primaryHome: home (id: "120efe4a-6839-45fc-beed-27455d29212f") {
type
size
primaryAddress: address {
primaryAddressText: address1
country
}
}
secondaryHome: home (id: "0c735830-be56-4a3d-a8cb-d0189037f221") {
type
size
secondaryAddress: address {
secondaryAddressText: address1
country
}
}
}
var homeIdParameter = new GraphQlQueryParameter<Guid>("homeId", "ID", homeId);
var builder =
new TibberQueryBuilder()
.WithViewer(
new ViewerQueryBuilder()
.WithHome(new HomeQueryBuilder().WithAllScalarFields(), homeIdParameter)
)
.WithParameter(homeIdParameter);
result:
query ($homeId: ID = "c70dcbe5-4485-4821-933d-a8a86452737b") {
viewer{
home(id: $homeId) {
id
timeZone
appNickname
appAvatar
size
type
numberOfResidents
primaryHeatingSource
hasVentilationSystem
mainFuseSize
}
}
}
var includeDirectParameter = new GraphQlQueryParameter<bool>("direct", "Boolean", true);
var includeDirective = new IncludeDirective(includeDirectParameter);
var skipDirective = new SkipDirective(true);
var builder =
new TibberQueryBuilder()
.WithViewer(
new ViewerQueryBuilder()
.WithName(include: includeDirective)
.WithAccountType(skip: skipDirective)
.WithHomes(new HomeQueryBuilder().WithId(), skip: skipDirective)
)
.WithParameter(includeDirectParameter);
result:
query (
$direct: Boolean = true) {
viewer {
name @include(if: $direct)
accountType @skip(if: true)
homes @skip(if: true) {
id
}
}
}
var builder =
new RootQueryBuilder("InlineFragments")
.WithUnion(
new UnionTypeQueryBuilder()
.WithTypeName()
.WithConcreteType1Fragment(new ConcreteType1QueryBuilder().WithAllFields())
.WithConcreteType2Fragment(new ConcreteType2QueryBuilder().WithAllFields())
.WithConcreteType3Fragment(
new ConcreteType3QueryBuilder()
.WithTypeName()
.WithName()
.WithConcreteType3Field("alias")
.WithFunction("my value", "myResult1")
)
)
.WithInterface(
new NamedTypeQueryBuilder()
.WithName()
.WithConcreteType3Fragment(
new ConcreteType3QueryBuilder()
.WithTypeName()
.WithName()
.WithConcreteType3Field()
.WithFunction("my value")
),
Guid.Empty
);
result:
query InlineFragments {
union {
__typename
... on ConcreteType1 {
name
concreteType1Field
}
... on ConcreteType2 {
name
concreteType2Field
}
... on ConcreteType3 {
__typename
name
alias: concreteType3Field
myResult1: function(value: "my value")
}
}
interface(parameter: "00000000-0000-0000-0000-000000000000") {
name
... on ConcreteType3 {
__typename
name
concreteType3Field
function(value: "my value")
}
}
}
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:
var configuration = new GraphQlGeneratorConfiguration();
configuration.ScalarFieldTypeMappingProvider = new MyCustomScalarFieldTypeMappingProvider();
public class MyCustomScalarFieldTypeMappingProvider : IScalarFieldTypeMappingProvider
{
public ScalarFieldTypeDescription GetCustomScalarFieldType(GraphQlGeneratorConfiguration configuration, GraphQlType baseType, GraphQlTypeBase valueType, string valueName)
{
valueType = valueType is GraphQlFieldType fieldType ? fieldType.UnwrapIfNonNull() : valueType;
// DateTime and Byte
switch (valueType.Name)
{
case "Byte": return new ScalarFieldTypeDescription { NetTypeName = "byte?", FormatMask = null };
case "DateTime": return new ScalarFieldTypeDescription { NetTypeName = "DateTime?", FormatMask = null };
}
// fallback - not needed if all fields and arguments are resolved or the expected type is of "object" type
return DefaultScalarFieldTypeMappingProvider.GetFallbackFieldType(configuration, valueType);
}
}
Generated class example:
public class OrderType
{
public DateTime? CreatedDateTimeUtc { get; set; }
public byte? SomeSmallNumber { get; set; }
}
vs.
public class OrderType
{
public object CreatedDateTimeUtc { get; set; }
public object SomeSmallNumber { get; set; }
}
Source generator supports RegexScalarFieldTypeMappingProvider
rules using JSON configuration file. Example:
[
{
"patternBaseType": ".+",
"patternValueType": ".+",
"patternValueName": "^((timestamp)|(.*(f|F)rom)|(.*(t|T)o))$",
"netTypeName": "DateTimeOffset?",
"formatMask": "O"
}
]
All pattern values must be specified. Null
values are not accepted.
The file must be named RegexScalarFieldTypeMappingProvider.gql.config.json
and included as additional file.
<ItemGroup>
<AdditionalFiles Include="RegexScalarFieldTypeMappingProvider.gql.config.json" CacheObjects="true" />
</ItemGroup>