GithubHelp home page GithubHelp logo

byme8 / zeroql Goto Github PK

View Code? Open in Web Editor NEW
246.0 6.0 14.0 952 KB

C# GraphQL client with Linq-like syntax

License: MIT License

C# 99.26% PowerShell 0.74%
client csharp dotnet graphql csharp-sourcegenerator

zeroql's Introduction

ZeroQL | GitHub Nuget .NET

๐Ÿš€ Welcome to ZeroQL, a high-performance C#-friendly GraphQL client! ๐ŸŽ‰

ZeroQL makes it easy to perform queries and mutations with Linq-like syntax. Unlike other GraphQL clients, ZeroQL doesn't require Reflection.Emit or expressions, which means the runtime provides performance very close to a raw HTTP call.

Features

Here's a quick rundown of what ZeroQL can do at the moment:

You can find the full wiki here or just by clicking on the feature bullet point you are interested in.

Check out our articles to learn more about ZeroQL:

How to setup

Here, you can find the setup for net6.0+ projects. You can find netstandard or .Net Framework and Unity setup in wiki.

The initial setup:

# create console app
dotnet new console -o QLClient
# go to project folder 
cd QLClient
# create manifest file to track nuget tools
dotnet new tool-manifest 
# add ZeroQL.CLI nuget tool
dotnet tool install ZeroQL.CLI # or 'dotnet tool restore' once you pulled the existing repository
# add ZeroQL nuget package
dotnet add package ZeroQL 
# fetch graphql schema from server(creates schema.graphql file)
dotnet zeroql schema pull --url http://localhost:10000/graphql
# to create ZeroQL config file: ./config.zeroql.json
dotnet zeroql config init
# build the project to initiate the ZeroQL client generation with options specified inside config.zeroql.json
dotnet build

The build should be successful, and now we can use the generated client.

Config

The command dotnet zeroql config init creates the config.zeroql.json. By itself it looks like that:

{
  "$schema": "https://raw.githubusercontent.com/byme8/ZeroQL/main/schema.verified.json",
  "graphql": "./schema.graphql",
  "namespace": "ZeroQL.Client",
  "clientName": "ZeroQLClient"
}

Now if you have ZeroQL package installed to your csproj, it will automatically detect and execute CLI based on this configuration file on every build. To make sure that it works, the config file should follow the *.zeroql.jsonpattern, or you can add a custom definition in your csproj like that:

<ItemGroup>
    <ZeroQLConfig Include="you.custom.config.name.json"/>
</ItemGroup>

The generated client would be stored inside ./obj/ZeroQL folder. So it will never appear in the solution. However, you still have access to generated classes in your source code.

If you want to turn off automatic generation on every build, it is possible to disable it:

<PropertyGroup>
   <ZeroQLOnBuildTriggerEnabled>False</ZeroQLOnBuildTriggerEnabled>
</PropertyGroup>

How to use

Let's suppose that schema.graphql file contains the following:

schema {
  query: Queries
  mutation: Mutation
}

type Queries {
  me: User!
  user(id: Int!): User
}

type Mutation {
  addUser(firstName: String!, lastName: String!): User!
  addUserProfileImage(userId: Int! file: Upload!): Int!
}

type User {
  id: Int!
  firstName: String!
  lastName: String!
  role: Role!
}

type Role {
  id: Int!
  name: String!
}

and we want to execute the query like that:

query { me { id firstName lastName } }

GraphQL lambda syntax

Here is how we can achieve it with ZeroQL "lambda" syntax:

var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri("http://localhost:10000/graphql");

var client = new TestServerGraphQLClient(httpClient);

var response = await client.Query(o => o.Me(o => new { o.Id, o.FirstName, o.LastName }));

Console.WriteLine($"GraphQL: {response.Query}"); // GraphQL: query { me { id firstName lastName } }
Console.WriteLine($"{response.Data.Id}: {response.Data.FirstName} {response.Data.LastName}"); // 1: Jon Smith

You can pass arguments inside lambda if needed:

var userId = 1;
var response = await client.Query(o => o.User(userId, o => new User(o.Id, o.FirstName, o.LastName)));

Console.WriteLine($"GraphQL: {response.Query}"); // GraphQL: query ($id: Int!) { user(id: $id) { id firstName lastName } }
Console.WriteLine($"{response.Data.Id}: {response.Data.FirstName} {response.Data.LastName}"); // 1: Jon Smith

There is a limitation for lambda syntax. The variable should be a local variable or a parameter of the function. Otherwise, it will not be included in the lambda closure. As a result, ZeroQL would not be able to get a value.

Here is an example of the function parameter:

public Task<User> GetUser(int userId)
{
    var response = await client.Query(o => o.User(userId, o => new User(o.Id, o.FirstName, o.LastName)));
    return response.Data;
}

To be clear, you don't need actively account for it. ZeroQL will analyze and report errors if something is wrong.

For example, the next sample will not work:

public int UserId { get; set; }

public Task<User> GetUser()
{
    var response = await client.Query(o => o.User(UserId, o => new User(o.Id, o.FirstName, o.LastName))); // ZeroQL will report a compilation error here
    return response.Data;
}

Also, there is a way to avoid lambda closure:

var variables = new { Id = 1 };
var response = await client.Query(variables, static (i, o) => o.User(i.Id, o => new User(o.Id, o.FirstName, o.LastName)));

You can fetch attached fields:

var variables = new { Id = 1 };
var response = await client.Query(
    variables,
    static (i, o) => o
        .User(i.Id,
            o => new
            {
                o.Id,
                o.FirstName,
                o.LastName,
                Role = o.Role(role => role.Name)
            }));

Console.WriteLine($"GraphQL: {response.Query}"); // GraphQL: query GetUserWithRole($id: Int!) { user(id: $id) { id firstName lastName role { name }  } }
Console.WriteLine($"{response.Data.Id}: {response.Data.FirstName} {response.Data.LastName}, Role: {response.Data.Role}"); // 1: Jon Smith, Role: Admin

GraphQL request syntax

In more complex queries, the "lambda" syntax may look verbose, and extracting requests into a separate entity would be nice. Now it is possible to do it via the "request" syntax. Here is an example:

// define a request
public record GetUserQuery(int Id) : GraphQL<Queries, UserModel?>
{
    public override UserModel? Execute(Queries query) 
        => query.User(Id, o => new UserModel(o.Id, o.FirstName, o.LastName));
}

// execute a request
var response = await client.Execute(new GetUserQuery(variables.FriendId));

Console.WriteLine(response.Query); // query GetUserQuery($id: Int!) { user(id: $id) { id firstName lastName } }
Console.WriteLine(response.Data); // UserModel { Id = 2, FirstName = Ben, LastName = Smith }

You need to create a record from the base record GraphQL<TOperationType, TResult>. Where the TOperationType is a root query type(Query, Mutation) that is associated with the GraphQLClient<TQuery, TMutataion> instance.

Benchmarks

The complete benchmark source code you can find here.

The short version looks like this:

[Benchmark]
public async Task<string> Raw()
{
    var rawQuery = 
        $$"""
        {
            "variables": { "id": {{id}} }, 
            "query": "query GetUser($id: Int!){ user(id: $id) { id firstName lastName } }" 
        }
        """;
    var response = await httpClient.PostAsync("", new StringContent(rawQuery, Encoding.UTF8, "application/json"));
    var responseJson = await response.Content.ReadAsStreamAsync();
    var qlResponse = JsonSerializer.Deserialize<JsonObject>(responseJson, options);

    return qlResponse!["data"]!["user"]!["firstName"]!.GetValue<string>();
}

[Benchmark]
public async Task<string> StrawberryShake()
{
    // query GetUser($id: Int!) {
    //   user(id: $id) {
    //       id
    //       firstName
    //       lastName
    //   }
    // }
    var firstname = await strawberryShake.GetUser.ExecuteAsync(id);
    return firstname.Data!.User!.FirstName;
}

[Benchmark]
public async Task<string> ZeroQLLambdaWithoutClosure()
{
    var variables = new { Id = id };
    var firstname = await zeroQLClient.Query(
        variables, static (i, q)
            => q.User(i.Id, o => new { o.Id, o.FirstName, o.LastName }));

    return firstname.Data!.FirstName;
}

[Benchmark]
public async Task<string> ZeroQLLambdaWithClosure()
{
    var id  = this.id;
    var firstname = await zeroQLClient.Query( q
            => q.User(id, o => new { o.Id, o.FirstName, o.LastName }));

    return firstname.Data!.FirstName;
}

[Benchmark]
public async Task<string> ZeroQLRequest()
{
    var firstname = await zeroQLClient.Execute(new GetUserQuery(id));

    return firstname.Data!.FirstName;
}

// ..
public record GetUserQuery(int id) : GraphQL<Query, User?>
{
    public override User? Execute(Query query)
        => query.User(id, o => new User(o.Id, o.FirstName, o.LastName));
}

Here results:

BenchmarkDotNet=v0.13.2, OS=macOS 14.3.1 (23D60) [Darwin 23.3.0]
Apple M1, 1 CPU, 8 logical and 8 physical cores
.NET SDK=8.0.100
  [Host]     : .NET 8.0.0 (8.0.23.53103), Arm64 RyuJIT AdvSIMD
  DefaultJob : .NET 8.0.0 (8.0.23.53103), Arm64 RyuJIT AdvSIMD

Method Mean Error StdDev Gen0 Allocated
Raw 111.3 us 0.77 us 0.68 us 0.7324 5.29 KB
StrawberryShake 119.3 us 1.61 us 1.51 us 1.7090 11.55 KB
ZeroQLLambdaWithoutClosure 112.4 us 2.04 us 1.91 us 0.9766 6.7 KB
ZeroQLLambdaWithClosure 113.7 us 1.80 us 1.68 us 0.9766 7.18 KB
ZeroQLRequest 112.9 us 1.22 us 1.14 us 0.9766 6.27 KB

As you can see, the Raw method is the fastest. The ZeroQL method is a bit faster than the StrawberryShake method. But in absolute terms, all of them are pretty much the same.

So, with the ZeroQL, you can forget about the graphql and just use the Linq-like interface. It will have little effect on performance.

Credits

The initial inspiration for this project came from the work done at https://github.com/Giorgi/GraphQLinq

zeroql's People

Contributors

byme8 avatar ch1sel avatar cho-trackman avatar davermaltby avatar jarlef avatar jenschude avatar lennykean avatar valerii15298 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

zeroql's Issues

Support for default values for input objects

For a complex input object, I have GraphQL SDL of:

input WeightedValueOfInt32Input {
  value: Int!
  weight: Float! = 1
}

but the ZeroQL generated class looks like:

    [System.CodeDom.Compiler.GeneratedCode ( "ZeroQL" ,  "2.1.1.0" )]
    public class WeightedValueOfInt32Input
    {
        public int Value { get; set; }

        public double Weight { get; set; }
    }

Therefore, the Weight property if not specified, does not default to the value 1 as it would if you did not specify this property in a GraphQL mutation call.

Support for fragments

It would be nice to have a way to extract some parts of the query and reuse them in other queries.
I imagine it should work like that:

// initial query
client.Query(static q => q.User(42, o => new { o.FirstName, o.LastName }))

// improved
client.Query(static q => q.GetUser(42))

public static UserModel GetUser(this Query query, int id)
   => query.User(id, o => new UserModel(o.FirstName, o.LastName));

Is it possbile to generate queries from "query" definitions?

Hello @byme8 !
Besides main server file I have lots of small graphql client files which usually consist of something like:

query QueryName($queryParams) {
  actualQueryToCall($queryParams) {
    ...thingsToReturn
  }
}

Can this lib auto-generate those? Or I can only do this manually? Also, the same question about fragment, mutation etc

Query not bootstrapped when using named arguments

Executing queries using named parameters consistently throws "Query Not Bootstrapped" exceptions, see below snippet.

CancellationTokenSource cts = new();

var result = await gClient.Query(cancellationToken: cts.Token, query: static q => q.Fruits(static f => new { f.Fruit_name }));

I've also attached a reproduction below.

ZeroQLtest.zip

Compatability with numeric IDs

I'm connecting to a server that returns IDs as numbers instead of strings

{
  "data": {
    "info": {
      "id": 55131,
      "name": "Something Descriptive"
    }
  }
}

This appears to be a violation of the GraphQL specification, but it's out of my control.

It sounds like the Newtonsoft Json parser might be ok with it, but System.Text.Json throws an exception that it can't convert a number to a string.
This can be handled with a change to ZeroQL.Runtime/Schema/ID.cs such as

    public override ID? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
	// New: Handle non-strings
        if (reader.TokenType == JsonTokenType.Number)
        {
            var strValue = System.Text.Encoding.UTF8.GetString(reader.ValueSpan);
            return new ID(strValue);
        }
        // Existing: Handle strings
        var value = reader.GetString();
        if (value is null)
        {
            return null;
        }

        return new ID(value);
    }

Covariant return types when using Interfaces

Describe the bug

Our schema makes use of interfaces. There is one occurrence where a field returns a covariant return type of the interface definition. C# disallows covariant return types (at least prior to C# language level 9). Also changing the language level to latest major didn't resolve the issue.

I'm unsure if it would be solvable at all when the language prohibits this. But according to the GraphQL spec covariant return types are allowed. https://spec.graphql.org/October2021/#sec-Objects.Type-Validation

field must return a type which is equal to or a sub-type of (covariant) the return type of implementedField fieldโ€™s return type:

How to Reproduce

schema {
  query: Query
}
  
type Query {
  inStore(key: String!): InStore!
}

interface MeFieldInterface {
  me: MeQueryInterface!
}

type InStore implements MeFieldInterface {
  me: InStoreMe!
}

type InStoreMe implements MeQueryInterface {
  activeCart: Cart
}

interface MeQueryInterface {
  activeCart: Cart
}

type Cart {
  customerId: String
}

When generating from this schema the compiler compiler complains:

GraphQL.cs(34, 28): [CS0738] 'InStore' does not implement interface member 'MeFieldInterface.__Me'. 'InStore.__Me' cannot implement 'MeFieldInterface.__Me' because it does not have the matching return type of 'MeQueryInterface'.

It seems also Covariant return types are allowed for classes in C# language level 9 they aren't allowed for interfaces as like as in Java or C++.

The relevant parts from the generated class:

   public class ZeroQLClient : global::ZeroQL.GraphQLClient<Query, ZeroQL.Unit>
    {
        public ZeroQLClient(global::System.Net.Http.HttpClient client, global::ZeroQL.Pipelines.IGraphQLQueryPipeline? queryPipeline = null) : base(client, queryPipeline)
        {
        }
    }

    [ZeroQL.GraphQLType("Cart")]
    [System.CodeDom.Compiler.GeneratedCode ( "ZeroQL" ,  "5.0.0.0" )]
    public class Cart
    {
        [ZeroQL.GraphQLName("customerId")]
        [JsonPropertyName("customerId")]
        public string? CustomerId { get; set; }
    }

    [ZeroQL.GraphQLType("InStore")]
    [System.CodeDom.Compiler.GeneratedCode ( "ZeroQL" ,  "5.0.0.0" )]
    public class InStore : MeFieldInterface
    {
        [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never), JsonPropertyName("me")]
        public InStoreMe __Me { get; set; }

        [ZeroQL.GraphQLName("me")]
        public T Me<T>(Func<InStoreMe, T> selector = default !)
        {
            return __Me is null ? throw new NullReferenceException("Me is null but it should not be null. Schema can be outdated.") : selector(__Me);
        }
    }

    [ZeroQL.GraphQLType("InStoreMe")]
    [System.CodeDom.Compiler.GeneratedCode ( "ZeroQL" ,  "5.0.0.0" )]
    public class InStoreMe : MeQueryInterface
    {
        [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never), JsonPropertyName("activeCart")]
        public Cart __ActiveCart { get; set; }

        [ZeroQL.GraphQLName("activeCart")]
        public T? ActiveCart<T>(Func<Cart, T> selector = default !)
        {
            return __ActiveCart is null ? default : selector(__ActiveCart);
        }
    }

    [ZeroQL.GraphQLType("Query")]
    [System.CodeDom.Compiler.GeneratedCode ( "ZeroQL" ,  "5.0.0.0" )]
    public class Query : global::ZeroQL.Internal.IQuery
    {
        [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never), JsonPropertyName("inStore")]
        public InStore __InStore { get; set; }

        [ZeroQL.GraphQLName("inStore")]
        public T InStore<T>(string key = default !, Func<InStore, T> selector = default !)
        {
            return __InStore is null ? throw new NullReferenceException("InStore is null but it should not be null. Schema can be outdated.") : selector(__InStore);
        }
    }

    [System.CodeDom.Compiler.GeneratedCode ( "ZeroQL" ,  "5.0.0.0" )]
    public interface MeFieldInterface : global::ZeroQL.IUnionType
    {
        [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never), JsonPropertyName("me")]
        public MeQueryInterface __Me { get; set; }

        [ZeroQL.GraphQLName("me")]
        public T Me<T>(Func<MeQueryInterface, T> selector = default !);
    }

    [System.CodeDom.Compiler.GeneratedCode ( "ZeroQL" ,  "5.0.0.0" )]
    public class MeFieldInterfaceStub : MeFieldInterface
    {
        [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never), JsonPropertyName("me")]
        public MeQueryInterface __Me { get; set; }

        [ZeroQL.GraphQLName("me")]
        public T Me<T>(Func<MeQueryInterface, T> selector = default !)
        {
            return __Me is null ? throw new NullReferenceException("Me is null but it should not be null. Schema can be outdated.") : selector(__Me);
        }
    }

    [System.CodeDom.Compiler.GeneratedCode ( "ZeroQL" ,  "5.0.0.0" )]
    public interface MeQueryInterface : global::ZeroQL.IUnionType
    {
        [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never), JsonPropertyName("activeCart")]
        public Cart __ActiveCart { get; set; }

        [ZeroQL.GraphQLName("activeCart")]
        public T? ActiveCart<T>(Func<Cart, T> selector = default !);
    }

    [System.CodeDom.Compiler.GeneratedCode ( "ZeroQL" ,  "5.0.0.0" )]
    public class MeQueryInterfaceStub : MeQueryInterface
    {
        [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never), JsonPropertyName("activeCart")]
        public Cart __ActiveCart { get; set; }

        [ZeroQL.GraphQLName("activeCart")]
        public T? ActiveCart<T>(Func<Cart, T> selector = default !)
        {
            return __ActiveCart is null ? default : selector(__ActiveCart);
        }
    }

The following resembles the structure in a simple class/interface hierarchy:

   class Cart {
        public string customerId() {
            return "";
        }
    }

    interface MeQueryInterface {
        public Cart activeCart();
    }

    class InStoreMe : MeQueryInterface {
        public Cart activeCart() {
            return new Cart();
        }
    }

    class InStore : MeFieldInterface {
        public InStoreMe me()
        {
            return new InStoreMe();
        }
    }

    interface MeFieldInterface
    {
        public MeQueryInterface me();
    }

Support for custom scalars

GraphQL.g.cs(6110, 36): [CS0246] The type or namespace name 'ObjectId' could not be found (are you missing a using directive or an assembly reference?)

in schema it looks like:

scalar ObjectId

Does it support custom scalars? I've generated client from schema that uses Mongo ObjectId, and got this error

Error while using most of the GraphQL Schema

Describe the bug

When using most of the schema available online the tool is failing to generate the client code without compilation error.

The type or namespace name 'ID' could not be found (are you missing a using directive or an assembly reference?)

How to Reproduce

Use the below schema
curl https://workshop.chillicream.com/graphql?sdl > schema.graphql

Then generate the code using below command
dotnet zeroql generate --schema ./schema.graphql --namespace TestServer.Client --client-name TestServerGraphQLClient --output Generated/GraphQL.g.cs

Expected behavior
The code should be generated without any compilation errors

Environment (please complete the following information):

  • Nuget verion [3.2.0]
  • IDE: [VS]
  • .Net Version [6.0.400, 7.0.100]

QueryRequestAnalyzer throwing a Null exception

This issue is obscuring another issue that I'm having that is preventing me from calling a Mutation of mine. The error that I'm receiving is:
4>CSC : warning AD0001: Analyzer 'ZeroQL.SourceGenerators.Analyzers.QueryRequestAnalyzer' threw an exception of type 'System.NullReferenceException' with message 'Object reference not set to an instance of an object.'.

I started debugging the analyer and see the root cause here. At https://github.com/byme8/ZeroQL/blob/main/src/ZeroQL.SourceGenerators/Analyzers/QueryRequestAnalyzer.cs#L73 the ZeroQLRequestLikeContextResolver.Resolve method can return an Error in its Result<GraphQLSourceGenerationContext> instance, but the if condition following the Resolve call only checks for an ErrorWithData<Diagnostic>. Its else then gets called which assumes that there is no error and that requestContext is not null (but it is null). Therefore, an exception occurs at https://github.com/byme8/ZeroQL/blob/main/src/ZeroQL.SourceGenerators/Analyzers/QueryRequestAnalyzer.cs#L83 when trying to get the OperationQuery property.

As for the other error, I'm still trying to pin it down and will create an issue for that, if it isn't something that I'm doing wrong. Love this library by the way. Thanks.

Support GraphQLโ€™s transport layer agnostics

For integration testing of our product which uses Hot Chocolate, I am not standing up an HTTP endpoint in order to make GraphQL calls. Hot Chocolate exposes an IRequestExecutor.ExecuteAsync() method which allows us to avoid HTTP altogether and just provide json back and forth.
Initially when I began to use your library, I believed that I could use the generated ZeroQL classes based on my schema and just create my own version of ZeroQL.GraphQLClient<> which didn't require a System.Net.Http.HttpClient. Of course, I also had to copy the GraphQLClientLambdaExtensions.cs code and modify it to use my new GraphQLClient derivative. Anyhow, I hit a deadend without making changes to the ZeroQL library in that the ZeroQL code anaylizers are coming back with a DiagnosticDescriptor of Descriptors.FailedToConvert. I believe that there is some conversion that is depending on a client inherited from ZeroQL.GraphQLClient<>. Anyhow, seems like changes in ZeroQL are needed to accomplish what I'm hoping to do.

I have already forked and created a branch that works in the unit test that I created. The PR will follow momentarily.

Custom name for query type breaks client generation

If you call your query type "Queries" instead of "Query" the client is generated incorrectly.

Steps to reproduce:

  1. Follow instructions here
  • Skip curl command,
  1. Manually create schema.graphql with the following content
schema {
  query: Queries
}

type Queries {
  me: User!
  user(id: Int!): User
}

type User {
  id: Int!
  firstName: String!
  lastName: String!
  role: Role!
}

type Role {
  id: Int!
  name: String!
}

Build solution to generate client.
The client will now be missing methods on the query type.

dotnet zeroql generate - ArgumentException

Describe the bug
I'm generating a graphQL client for Shopify using the following command:

dotnet zeroql generate --schema ./schema.graphql --namespace ShopifyGraphQL.Client --client-name ShopifyGraphQLClient --output Generated/GraphQL.g.cs

How to Reproduce

The schema is generated from calling shopify's api using an introspection query https://community.shopify.com/c/shopify-apis-and-sdks/admin-api-graphql-shema-endpoint/td-p/837807 and the graphql cli https://stackoverflow.com/questions/37397886/get-graphql-whole-schema-query

Stack Trace

System.ArgumentException: An item with the same key has already been added. Key: Id
  at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior) 
  at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value) 
  at System.Linq.Enumerable.ToDictionary[TSource,TKey](IEnumerable`1 source, Func`2 keySelector, IEqualityComparer`1 comparer) 
  at ZeroQL.Bootstrap.Generators.TypeGenerator.<>c__DisplayClass0_0.<GenerateTypes>b__0(ClassDefinition o) in /home/runner/work/ZeroQL/ZeroQL/src/ZeroQL.Tools/Bootstrap/Generators/TypeGenerator.cs:34
  at System.Linq.Enumerable.SelectArrayIterator`2.ToList() 
  at ZeroQL.Bootstrap.Generators.TypeGenerator.GenerateTypes(GraphQlGeneratorOptions options, ClassDefinition[] definitions, GraphQLNamedType queryType, GraphQLNamedType mutationType) in /home/runner/work/ZeroQL/ZeroQL/src/ZeroQL.Tools/Bootstrap/Generators/TypeGenerator.cs:23
  at ZeroQL.Bootstrap.GraphQLGenerator.ToCSharp(String graphql, GraphQlGeneratorOptions options) in /home/runner/work/ZeroQL/ZeroQL/src/ZeroQL.Tools/Bootstrap/GraphQLGenerator.cs:94
  at ZeroQL.CLI.Commands.GenerateCommand.ExecuteAsync(IConsole console) in /home/runner/work/ZeroQL/ZeroQL/src/ZeroQL.CLI/Commands/GenerateCommand.cs:107
  at CliFx.CliApplication.RunAsync(ApplicationSchema applicationSchema, CommandInput commandInput) in /D:\a\CliFx\CliFx\CliFx\CliApplication.cs:147
  at CliFx.CliApplication.RunAsync(IReadOnlyList`1 commandLineArguments, IReadOnlyDictionary`2 environmentVariables) in /D:\a\CliFx\CliFx\CliFx\CliApplication.cs:191

Environment (please complete the following information):

  • Nuget version [4.2.0-preview.1]
  • IDE: [Rider]
  • .Net Version 7.0

when the generated stored inside ./obj/ZeroQL IDE occurs problem

Describe the bug
Issue when the generated stored inside ./obj/ZeroQL

notice this bug it not happen in project type console app but it happens like blazor web assembly, web API

Problem 1. IDE occurs problem Duplicate definition 'ZeroQL.Client.ZeroQLClient'. Possibly missing keyword 'partial'
image

Proplem 2. From above problem when using dotnet build is error The namespace 'ZeroQL.Client' already contains a definition for 'ZeroQLClient'

Workaround Solution (not available on version 6.0.0)
To use the config output option but not available on version 6.0.0
but woking on version 4.0.0 follow schema https://raw.githubusercontent.com/byme8/ZeroQL/main/schema.verified.json

How to Reproduce

  1. to create ZeroQL config file: ./config.zeroql.json using command dotnet zeroql config init
  2. add output option like

{
"$schema": "https://raw.githubusercontent.com/byme8/ZeroQL/main/schema.verified.json",
"graphql": "./schema.graphql",
"namespace": "ZeroQL.Client",
"clientName": "ZeroQLClient",
"output": "./Generated/GraphQL.g.cs"
}

Expected behavior
The generated client should be stored inside ./Generated/GraphQL.g.cs
but is still stored inside ./obj/ZeroQL

Environment (please complete the following information):

  • Nuget version [3.0.0]
  • IDE: [Rider]
  • .Net Version [6.0.400]

Optional flag to be able to disable CS8618 warnings in generated client

Since I regenerate the client code with each compile of my automation tests, it would be helpful, if there was a flag that would automatically add:
#prama warning disable CS8618
and
#pragma warning restore CS8618
to the beginning and end of the generated client file.

To avoid this, I currently have my own console application that directly calls GraphQLGenerator.ToCSharp() and I prepend and append the pragma statements myself. Thanks.

Support local functions for selection in addition to lambda

It would be great if the library could support the selection of items in the query via local functions as well as lambdas since the selections are of type Func rather than Expressions.

Lambdas are great, but they incur overhead (heap allocations etc.) (some benchmarks here).

Given the below graphql schema, it would be nice if the following C# code could be used to query it.

schema {
  query: Query
}

type Query {
  foo: Foo
}

type Foo {
  id: Int
  bar: Bar
}

type Bar {
  id: int
  firstName: String
  surname: String
}

c#:

public class MyRepository
{
  private readonly _zeroQlClient;

//this gives the following preview:
//GraphQLQueryPreview: query { foo { } }

  public async Task<Foo> ExecuteQuery()
  {
    var response = await _zeroQlClient.Query(static query => query.Foo(foo => new Foo {Id = foo.Id, Bar = BarSelection }));
    return response;
    
    //local function
    static Bar BarSelection(Bar bar) => new Bar { bar.Id, bar.FirstName, bar.Surname};
  }

Defined Scalars that return JSON Array from the API throw a JsonException

Describe the bug
A scaler "JSON" is defined in the API which throws an exception System.Text.Json.JsonException: The JSON value could not be converted to Test.GraphQL.JSON at

   at System.Text.Json.Utf8JsonReader.GetString()
   at ZeroQL.ZeroQLScalarJsonConverter`1.Read(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options)

How to Reproduce
Define a GraphQL schema with a field as a custom JSON scalar. Return a JSON Array such as [{"foo": "bar"}] in that field.

Expected behavior
Parse the API's JSON Array result and store it in the public sealed record JSON : ZeroQLScalar type.

Additional context
As a workaround I did the following hack.

JSONJsonConverter.cs

public class JSONJsonConverter : JsonConverter<JSON>
{
    public override JSON? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var value = JsonElement.ParseValue(ref reader);

        var scalar = new JSON
        {
            Value = value.ToString(),
        };

        return scalar;
    }

    public override void Write(Utf8JsonWriter writer, JSON value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.Value);
    }
}

GraphQL.g.cs

    internal static class JsonConvertersInitializers
    {
        [global::System.Runtime.CompilerServices.ModuleInitializer]
        public static void Init()
        {
            global::ZeroQL.Json.ZeroQLJsonSerializersStore.Converters[typeof(global::Test.GraphQL.JSON)] = new JSONJsonConverter();
            // global::ZeroQL.Json.ZeroQLJsonSerializersStore.Converters[typeof(global::Test.GraphQL.JSON)] = new ZeroQLScalarJsonConverter<global::Test.GraphQL.JSON>();

How to handle json data types?

Is your feature request related to a problem? Please describe.
My graphQL has a JSON* field that I would like to handle; but I cannot find a way to do this with ZeroQL; haven't had much luck with custom serializers yet.

*(Technically JSONB; we are using Hasura w/ Postgres)

For example I'd like to turn this data:
{ "name": "john", "address": { "street": 1234, "zip": 45567, } }
and put it into a shape like:

class Person 
{
  public string Name {get; set;}
  public jsonb Address {get; set;}
}

So far we've tried the code below, but we haven't had any success. Was wondering if you had any examples you could share or could help us figure out what we are missing.

Out of the box the code generation creates this scalar in the generated code.

public sealed record jsonb : ZeroQLScalar
{
    public jsonb()
    {
    }

    public jsonb(string value)
    {
        Value = value;
    }

    public static implicit operator jsonb(string value) => new jsonb(value);
    public static implicit operator string (jsonb scalar) => scalar.Value;
}

But we are unable to use it as it chokes on the opening curly brace ('StartObject') during deserialization.

So we have tried to create a custom converter, but we are unable to get any of the Console.WriteLine statements to occur; suggesting it's not registering correctly with ZeroQL?

public class JsonBConverter : JsonConverter<jsonb>
{
    public override jsonb? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        Console.WriteLine("Beginning read...");
        throw new NotImplementedException();
    }

    public override void Write(Utf8JsonWriter writer, jsonb value, JsonSerializerOptions options)
    {
        Console.WriteLine("Beginning write...");
        throw new NotImplementedException();
    }
}

public static class Module
{
    [ModuleInitializer]
    public static void Initialize()
    {
        ZeroQLJsonOptions.Configure(o => o.Converters.Add(new JsonBConverter()));
    }
}

Support schema enums with mixed naming (excessive underscores)

Is your feature request related to a problem? Please describe.
I'm working with a schema that has mixed (silly) naming on enums eg.:

enum ImageKinds {
  SPLASH
  THUMBNAIL
  THUMBNAIL_WITH_TEXT
  THUMBNAIL__PORTRAIT
  PROMOTION
}

Notice the double __ (underscores).
This leads to a crash in the ToPascalCase() extension.

Describe the solution you'd like
Ignore excessive underscores.

Code Generation not generating C# code for GraphQL interfaces

I'm trying to use ZeroQL to generate a C# client for a graphql schema that implements interfaces for some types and the interfaces are being ignored during the code generation.
For instance, the graphql snippet below does not generates any code.

interface Client {
  addresses: [ClientAddress!]!
  alternateId: String
  availableCommunicationParticipants: [CommunicationParticipant!]!
  availableCommunicationTemplates: [CommunicationTemplate!]!
  bankAccounts: [BankAccount!]!
  "Provides blacklist information related to this entity."
  blacklisting: BlacklistingContext!
  claims(filters: ClaimListFiltersInput order: ClaimOrderInput = { by: CREATED, direction: DESCENDING, then: null } pagination: OptionalPaginationInput): ClaimList!
  communications(filters: CommunicationListFiltersInput order: CommunicationOrderInput = { by: CREATE_DATE, direction: DESCENDING, then: null } pagination: OptionalPaginationInput): CommunicationList!
  contactDetails: [ContactDetail!]!
  countryOfOrigin: Country
  customFields: CustomFieldContext!
  externalId: String
  id: Int!
  invoiceBatches: [ClaimPurchaseInvoiceBatch!]!
  isActive: Boolean!
  isPinned: Boolean!
  isVip: Boolean!
  language: Culture
  membershipNumber: String
  name: String!
  organisation: Organisation!
  policies(pagination: OptionalPaginationInput): PolicyList!
  purchaseInvoicesAndGuarantees(filters: ClaimPurchaseInvoiceOrPaymentGuaranteeListFiltersInput order: ClaimPurchaseInvoiceOrPaymentGuaranteeOrderInput = { by: CREATE_DATE, direction: DESCENDING, then: null } pagination: OptionalPaginationInput): ClaimPurchaseInvoiceOrPaymentGuaranteeList!
  tasks(filters: TaskListFiltersInput order: TaskOrderInput = { by: COMPLETED, direction: null, then: { by: DATE, direction: null, then: null } } pagination: OptionalPaginationInput): TaskList!
  uid: UUID!
}

Complex query variables fail to convert

Describe the bug
If a query has a complex-type variable as a filter, any initialization of the variable will fail with a compiler-time error: FailedToConvertPartOfTheQuery Failed to convert to graphql query.

How to Reproduce
We have a complex schema like this (cut for brevity):

type Query {
  getItemsPaged(filter: ItemFilter): ItemPage
}

input ItemFilter {
  ids: [UUID]
  name: String
  pageFilter: PageFilter
}

input PageFilter {
  sort: [String]
  page: Int
  size: Int
}

type ItemPage{
  content: [Item]
  size: Int
  number: Int
  totalElements: Int
  totalPages: Int
  numberOfElements: Int
}

type Item{
  id: UUID!
  class: String
  name: String
  comment: JSON
}

Then with the generated client code, the next query won't compile:

var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri("http://localhost:10000/graphql");

var client = new TestServerGraphQLClient(httpClient);

var response = await client.Query(static q =>
                q.GetItemsPaged(
                    filter: new ItemFilter { PageFilter = new PageFilter { Page = 0 } },
                    selector: page => new
                    {
                        page.Number,
                        page.TotalPages,
                        Items = page.Content(b => new { Id = b.Id, Name = b.Name, Comment = b.Comment }),
                    })));

Expected behavior
The query should compile and the filter variable should be correctly added in the GraphQL query.

Screenshots
image

Environment (please complete the following information):

  • Nuget version [3.4.3]
  • IDE: [VS 2022 Community]
  • .Net Version [net6.0]

Class Name clashes with Member in generated code

Describe the bug

The generated schema stumbles across properties which have the same name as the type itself

How to Reproduce

schema {
  query: Query
}
  
type Query {
  perVariant: Limit!
}

type Limit {
  limit: Long
}

As it's not allowed to have Members with the same name as the class in CSharp: [CS0542] 'Limit': member names cannot be the same as their enclosing type

    [System.CodeDom.Compiler.GeneratedCode ( "ZeroQL" ,  "4.2.0.0" )]
    public class Limit
    {
        [ZeroQL.GraphQLFieldSelector("limit")]
        [JsonPropertyName("limit")]
        public long? Limit { get; set; }
    }

Expected behavior

The class or member may be pre-/suffixed to avoid these name clashes and be able to generate a compiling client

Custom scalars for uuid with Hasura

Is your feature request related to a problem? Please describe.
We have added a scalar to the zeroql.json file that looks like:
"scalars": { "uuid": "System.Guid" }
Which works fine when returning a uuid through a query. However, our problem arises when we try to issue a mutation like a delete request and the query it generates looks like:
"mutation ($id: UUID!) { deleteObject(id: $id) { id } }"
Unfortunately, Hasura doesn't understand UUID when it is all caps. All it will understand is uuid. The error is:
Message "variable 'id' is declared as 'UUID!', but used where 'uuid!' is expected"

Describe the solution you'd like
We would like to be able to use the System.Guid for the scalar if possible.

Describe alternatives you've considered
We have tried using our own converter for the uuid, but have not had any luck getting rid of the Value property whenever the uuid is Read. Our output is always "id": { "Value": "dda0d58b-f90e-421a-ba8a-92381272abf8" } instead of our goal of "id": "dda0d58b-f90e-421a-ba8a-92381272abf8"

Additional context

Initial setup instructions improvement

The wiki states for the the initial setup to add a target in the project file so it's rebuild every time before compile:

<Target Name="GenerateQLClient" BeforeTargets="BeforeCompile">
    <Exec Command="dotnet zeroql generate --schema .\schema.graphql --namespace TestServer.Client --client-name TestServerGraphQLClient --output Generated/GraphQL.g.cs" />
</Target>

As the generated client is not declared as output it can lead to build issues when the generated file is not existing already. This can be improved by declaring the output of the exec task and adding an item group:

    <Target Name="GenerateQLClient" BeforeTargets="BeforeCompile">
        <Exec Command="dotnet zeroql generate --schema .\schema.graphql --namespace TestServer.Client --client-name TestServerGraphQLClient --output Generated/GraphQL.g.cs" Outputs="Generated/GraphQL.g.cs">
            <Output ItemName="Generated" TaskParameter="Outputs" />
        </Exec>
        <ItemGroup>
            <Compile Include="@(Generated)" />
            <FileWrites Include="@(Generated)" /> <!-- For clean to work properly -->
        </ItemGroup>
    </Target>

Now the generated file doesn't need to exist when building and also doesn't have to be committed.

Can't wrap the Query calls in another method.

Describe the bug

Can't wrap the Query calls in another method.

How to Reproduce

We're trying to replace an existing implementation and wanted to limit the changes across the code base. So we have the following

public async Task<GraphQLResult<TQueryResult>> QueryAsync<TQueryResult>(string operationName, Func<Query, TQueryResult> query)
        {
            var gqlClient = // .. make generated client with our IHttpHandler
            // do other things we need
            var res = await gqlClient.Query(operationName, query);
            return res;
        }

The await gqlClient.Query(operationName, query); gives the following error

Source generator failed unexpectedly with exception message:
Sequence contains no matching element
   at System.Linq.ThrowHelper.ThrowNoMatchException()
   at ZeroQL.SourceGenerators.Resolver.Context.GraphQLLambdaLikeContextResolver.Resolve(InvocationExpressionSyntax invocation, SemanticModel semanticModel, CancellationToken cancellationToken)
   at ZeroQL.SourceGenerators.Generator.GraphQLLambdaIncrementalSourceGenerator.GenerateFile(SourceProductionContext context, InvocationExpressionSyntax invocation, SemanticModel semanticModel, HashSet`1 processed)
   at ZeroQL.SourceGenerators.Generator.GraphQLLambdaIncrementalSourceGenerator.<>c__DisplayClass1_1.<GenerateSource>b__0()
   at ZeroQL.SourceGenerators.Utils.ErrorWrapper(SourceProductionContext context, CSharpSyntaxNode location, Action action)
Roslyn(ZQL0001)

Expected behavior

Clearly a Source Generator issue, but I'm not sure if it is a limitation of Source Generators or if it could be supported. If it is a limitation of Source Generators then you can close this. Otherwise it would be good to support this.

Environment (please complete the following information):

  • Nuget package version - 6.0.0
  • IDE: VS Code on Mac OS
  • .Net Version - 6

Add .netstandard 2.1 compatibility

Is your feature request related to a problem? Please describe.
I need to be able to use this under Unity. From what I've seen the best way to do it is by supportiong .netstandard 2.1 which has all the APIs

Describe the solution you'd like
Implement .netstandard 2.1 as a target framework

Additional context
I have started with this work. I will be posting the branch or possible PRs to tackle this if accepted.

Nullability fails for interfaces

Hi,

I have a schema which results in nullability errors in the generated code. Anyone know if this is the schemas fault or zeroql? The schema has been generated by the HotChocolate GraphQL server.

Schema is below;

schema
  @link(url: "https://specs.apollo.dev/link/v1.0")
  @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
{
  query: Query
}

"""A coordinate is an array of positions."""
scalar Coordinates

enum GeoJSONGeometryType
{
  Point @join__enumValue(graph: INCIDENT)
  MultiPoint @join__enumValue(graph: INCIDENT)
  LineString @join__enumValue(graph: INCIDENT)
  MultiLineString @join__enumValue(graph: INCIDENT)
  Polygon @join__enumValue(graph: INCIDENT)
  MultiPolygon @join__enumValue(graph: INCIDENT)
  GeometryCollection @join__enumValue(graph: INCIDENT)
}

interface GeoJSONInterface
{
  type: GeoJSONGeometryType!

  bbox: [Float]

  crs: Int
}

type GeoJSONLineStringType implements GeoJSONInterface
{
  coordinates: [Position]

  type: GeoJSONGeometryType!

  bbox: [Float!]!

  crs: Int!
}

type GeoJSONMultiLineStringType implements GeoJSONInterface
{
  coordinates: [Position]

  type: GeoJSONGeometryType!

  bbox: [Float!]!

  crs: Int!
}

type GeoJSONMultiPointType implements GeoJSONInterface
{
  coordinates: [Position]

  type: GeoJSONGeometryType!

  bbox: [Float!]!

  crs: Int!
}

type GeoJSONMultiPolygonType implements GeoJSONInterface
{
  coordinates: Coordinates

  type: GeoJSONGeometryType!

  bbox: [Float!]!

  crs: Int!
}

type GeoJSONPointType implements GeoJSONInterface
{
  coordinates: Position

  type: GeoJSONGeometryType!

  bbox: [Float!]!

  crs: Int!
}

type GeoJSONPolygonType implements GeoJSONInterface
{
  coordinates: [[Position]]

  type: GeoJSONGeometryType!

  bbox: [Float!]!

  crs: Int!
}

scalar Geometry

scalar Position

type Query
{
	fake: UUID!
}

Errors here...

1><PROJDIR>\GraphQL\Generated.g.cs(65,37,65,40): error CS8767: Nullability of reference types in type of parameter 'value' of 'void GeoJSONMultiPointType.Bbox.set' doesn't match implicitly implemented member 'void GeoJSONInterface.Bbox.set' (possibly because of nullability attributes).
1><PROJDIR>\GraphQL\Generated.g.cs(56,42,56,58): error CS0738: 'GeoJSONMultiPointType' does not implement interface member 'GeoJSONInterface.Crs'. 'GeoJSONMultiPointType.Crs' cannot implement 'GeoJSONInterface.Crs' because it does not have the matching return type of 'int?'.
1><PROJDIR>\GraphQL\Generated.g.cs(33,37,33,40): error CS8767: Nullability of reference types in type of parameter 'value' of 'void GeoJSONLineStringType.Bbox.set' doesn't match implicitly implemented member 'void GeoJSONInterface.Bbox.set' (possibly because of nullability attributes).
1><PROJDIR>\GraphQL\Generated.g.cs(24,42,24,58): error CS0738: 'GeoJSONLineStringType' does not implement interface member 'GeoJSONInterface.Crs'. 'GeoJSONLineStringType.Crs' cannot implement 'GeoJSONInterface.Crs' because it does not have the matching return type of 'int?'.
1><PROJDIR>\GraphQL\Generated.g.cs(49,37,49,40): error CS8767: Nullability of reference types in type of parameter 'value' of 'void GeoJSONMultiLineStringType.Bbox.set' doesn't match implicitly implemented member 'void GeoJSONInterface.Bbox.set' (possibly because of nullability attributes).
1><PROJDIR>\GraphQL\Generated.g.cs(40,47,40,63): error CS0738: 'GeoJSONMultiLineStringType' does not implement interface member 'GeoJSONInterface.Crs'. 'GeoJSONMultiLineStringType.Crs' cannot implement 'GeoJSONInterface.Crs' because it does not have the matching return type of 'int?'.
1><PROJDIR>\GraphQL\Generated.g.cs(81,37,81,40): error CS8767: Nullability of reference types in type of parameter 'value' of 'void GeoJSONMultiPolygonType.Bbox.set' doesn't match implicitly implemented member 'void GeoJSONInterface.Bbox.set' (possibly because of nullability attributes).
1><PROJDIR>\GraphQL\Generated.g.cs(72,44,72,60): error CS0738: 'GeoJSONMultiPolygonType' does not implement interface member 'GeoJSONInterface.Crs'. 'GeoJSONMultiPolygonType.Crs' cannot implement 'GeoJSONInterface.Crs' because it does not have the matching return type of 'int?'.
1><PROJDIR>\GraphQL\Generated.g.cs(97,37,97,40): error CS8767: Nullability of reference types in type of parameter 'value' of 'void GeoJSONPointType.Bbox.set' doesn't match implicitly implemented member 'void GeoJSONInterface.Bbox.set' (possibly because of nullability attributes).
1><PROJDIR>\GraphQL\Generated.g.cs(88,37,88,53): error CS0738: 'GeoJSONPointType' does not implement interface member 'GeoJSONInterface.Crs'. 'GeoJSONPointType.Crs' cannot implement 'GeoJSONInterface.Crs' because it does not have the matching return type of 'int?'.
1><PROJDIR>\GraphQL\Generated.g.cs(113,37,113,40): error CS8767: Nullability of reference types in type of parameter 'value' of 'void GeoJSONPolygonType.Bbox.set' doesn't match implicitly implemented member 'void GeoJSONInterface.Bbox.set' (possibly because of nullability attributes).
1><PROJDIR>\GraphQL\Generated.g.cs(104,39,104,55): error CS0738: 'GeoJSONPolygonType' does not implement interface member 'GeoJSONInterface.Crs'. 'GeoJSONPolygonType.Crs' cannot implement 'GeoJSONInterface.Crs' because it does not have the matching return type of 'int?'.

Originally posted by @RobTF in #41

Union Type support

union type support

which like this in schema.graphql file
union IResponse = Success | Fail

"Query is not bootstrapped." When attempting to use Request syntax

I'm getting the above error when executing a query using the classes, methods, and schema specified in your example. Looking at the source it seems like this error happens when the GraphQLQueryStore.Executor.TryGetValue returns null. When is this dictionary value being set?

Prefer Internal over Public for accessibility of generated types

I'm using ZeroQL generated types in a nuget package which I am mapping to other user-defined types via another Models package.

For this reason I would like to have the ZeroQL generated types be declared as Internal so that these types are restricted to the assembly performing the mapping and not publicly exposed.

Currently I have to manually change all of the public types to internal manually and this change is lost each time the tool is run.

Perhaps it could be an --accessibility flag that's passed to the tool which defaults to public for backwards compatibility

Nullable properties of input arguments lead to NullReferenceException in generated code

Running with 4.1.0-preview.2 on dotnet 7.0 i am having an issue with the following input type. Both fields are nullable/optional. The server would expect one of them to be provided:

input MonitorRelateToOneForCreateInput {
  create: MonitorCreateInput
  connect: MonitorWhereUniqueInput
}

The generated code treats both of them as required and tries to get the values of the fields, even if they can be null. This leads to a System.NullReferenceException

image
image

It should be possible to send input objects with nullable properties.

Allow omitting of the schema definition

Is your feature request related to a problem? Please describe.

ZeroQL cli expects a schema definition inside the schema file. According to the GraphQL spec the schema definition should be omitted when using the default type names for the operations:

https://spec.graphql.org/October2021/#sec-Root-Operation-Types.Default-Root-Operation-Type-Names

So you have to add the schema definition manually for the ZeroQL.Cli to accept the schema file.

Describe the solution you'd like

When no schema definition is given in the schema file try to use default operation types.

FailedToConvertPartOfTheQuery: Failed to convert to graphql query: i

Describe the bug
When executing a Query or Mutation with variables, the project won't compile with the error FailedToConvertPartOfTheQuery

How to Reproduce

Create new project with the graphql schema url: https://api-mumbai.lens.dev/graphql/:

Project init:

dotnet new console -o QLClient
cd QLClient
dotnet new tool-manifest 
dotnet tool install ZeroQL.CLI
dotnet add package ZeroQL 
dotnet zeroql schema pull -u https://api-mumbai.lens.dev/graphql/
dotnet zeroql generate --schema ./schema.graphql --namespace TestServer.Client --client-name TestServerGraphQLClient --output Generated/GraphQL.g.cs

Program.cs:

using TestServer.Client;

var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri("https://api-mumbai.lens.dev/graphql/");
var client = new TestServerGraphQLClient(httpClient);

var request = new ChallengeRequest { Address = "0x1c2eAdbB291709D3252610C431A6Ee355191E545" };
client.Query(request, static (i, o) => o.Challenge(i, result => new { result.Text }));

Expected behavior
The project should compile and run the queries.

Screenshots
image

Environment (please complete the following information):

  • IDE: VS for Windows AND VS for Mac (reporoduced on both)
  • .Net Version: 6.0

Wrong name case in query

Is that expected behavior?

Describe the bug
The query that is constructed from my code does not have the right case for the name. They are always lower case.

Expected behavior
The constructed query uses the real type not the lower case one.

var response = await client
.Query(static c => c.QueryClient(null, null, null, null, c => new { c.Name , cars = c.Cars(null, null, null, c => c.Brand) }));

Schema

type Client {
  id: ID!
  name: String! // for testing if making it lowercase in the schema work
  Cars(filter: CarFilter first: Int offset: Int): [Car!]
  CarsAggregate(filter: CarFilter): CarAggregateResult
}

type Car {
  id: ID!
  Brand: CarBrand!
  Owners(filter: ClientFilter order: ClientOrder first: Int offset: Int): [Client!]
  OwnersAggregate(filter: ClientFilter): ClientAggregateResult
}
query { queryClient(filter: null, order: null, first: null, offset: null) { name cars(filter: null, first: null, offset: null) { brand }  } }
Error: Cannot query field "cars" on type "Client". Did you mean "Cars"?

Environment (please complete the following information):

  • Nuget version [3.5.0]
  • IDE: [VS]
  • .Net Version [7.0.103]

Additional context
I'm using Dgraph cloud as my GraphQL provider.

Unable to use Hot Chocolate sorting

Not sure if this is the same issue that I mentioned in #11 as being obscured, but it seems like it. In the first case, it was calling a mutation with a complex parameter and the same is in this case. All both of these issues could very well be syntax errors on my part, but I can't figure it out after much review of your documentation.

So, I'm simplified this problem down as much as possible and created a repo that reproduces the issue. I've made a GraphQL test server that just exposes a simple Author class and has a query to get a list of authors. I added Hot Chocolate's sorting attribute on it. Anyhow, I cannot get the ZeroQL client to compile when trying to provide a sorting criteria.

var sortResponse = await qlClient.Query(static query => query.Authors(new AuthorSortInput[] { new AuthorSortInput { Name = SortEnumType.Asc } }, o => o.Name));

REF: https://github.com/Servant-Software-LLC/ZeroQL_Can_Not_Sort_Hot_Chocolate/blob/master/Integration.Tests/UnitTest1.cs#L29

If I don't provide a sorting argument, then it compiles and runs in the Test Explorer just fine:

var noSortResponse = await qlClient.Query(static query => query.Authors(null, o => o.Name));

If you clone the repo at https://github.com/Servant-Software-LLC/ZeroQL_Can_Not_Sort_Hot_Chocolate, it has the GraphQL server, generator and XUnit test which doesn't compile in the latest commit.

Code Analyzer fails on unrelated code

Describe the bug
A code analyser is causing "FailedToConverPartOfTheQuery: Failed to convert to graphql query: {0}" to be raised for an interface that defines a Query method but has no relation to the GraphQL implementation. the method is defined blow as
Task<PartialResult<Item>> Query(string id, bool inc, int limit)

The method is part of a database database interface.

Expected behavior
Unless the method is part of a ZeroQL generated class/interface then the analyzer should not raise an error.

Environment (please complete the following information):
ZeroQL - 3.6.2.0

CancellationToken support on Query and Mutation operations

Currently there is no way to way to provide a CancellationToken instance to the async operation of invoking a query/mutation operation.

Since the generated client is backed by a regular .Net HttpClient then an optional CancellationToken should be accepted and forwarded to the request issued by the underlying client.

The general pattern is to declare a cancellationToken parameter on the method and set it to default so invoking a query could look something like this:

var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri("http://localhost:10000/graphql");

var client = new TestServerGraphQLClient(httpClient);

using CancellationTokenSource cts = new(); 

var response = await client.Query(static o => o.Me(o => new { o.Id, o.FirstName, o.LastName }), cts.Token);

Source generator don't work in latest sdk version 7.0.200 (.NET 7.0.3)

Describe the bug
Microsoft just launched a security fix yesterday (.NET 7.0.3)

This breaks the ZeroQL source generator

ZeroQL.SourceGenerators/ZeroQL.SourceGenerators.Generator.GraphQLLambdaIncrementalSourceGenerator/ZeroQLModuleInitializer.24cfda3e5e964b5c9dc6bda0646f82b4.g.cs(32,34): Error CS0305: Using the generic type 'GraphQLResult<TData>' requires 1 type arguments

How to Reproduce
Install latest .NET SDK (7.0.200)

Environment (please complete the following information):

  • .NET Version [7.0.3] (SDK 7.0.200)

support extensions

A GraphQL server reponds to:
{"query": "query { ping }"}
with:

{ "data": { "ping": "pong" },
  "extensions": { "requestId": "488baa5c-be04-49ac-957f-ba739096b229" }
}

When sending the same query request to that server using ZeroQL, the response is:

{
    "query": "query { ping}",
    "data": { "ping": "pong"},
    "errors": null
}

The extensions key is missing from the response, making ZeroQL unsuitable for some integrations.

Is Newtonsoft Support Within Consideration/Scope?

Is your feature request related to a problem? Please describe.
I am working with an ASP.NET project that is using newtonsoft for serialization for controllers. However the code generated by ZeroQL is built for System.Text.Json (e.g. the JsonPropertyName attributes). Apologies if this is already possible; my check of the documentation and thumbing through of GenerateCommand.cs did not yield results.

Like all projects, this one has to decide what is within scope and out of scope; and so I wanted to see if Newtonsoft support was within scope of ZeroQL

Describe the solution you'd like
For usage, the simplest approach I could think of is some option on generate command to configure using Newtonsoft vs System.Text.Json. Maybe some sort of flag like --serializer="Newtonsoft" vs --seralizer="System.Text.Json", with it defaulting to System.Text.Json if the flag is not specified. (and a similar approach for the zeroql.json file)

Describe alternatives you've considered
I am trying to configure the ASP.NET project to use one serializer or another depending on the controller; this appears to be possible but also appears to be very difficult - I've yet to get it to work. Other alternatives are being considered but they all feel quite clunky and undesirable.

Additional context
This is very much asked in the spirit of "if you never ask, the answer is always no". If this support is not within consideration at this time, no worries!

Do ZeroQL support file sending?

I have not found an example or words about sending a file to GraphQL in the documentation. I had problems using GraphqlClient to send files. Can I use ZqroQL for it?

Compatibility

Hi.
When I try to add ZeroQL Nuget on Visual Studio I have this error :

Could not install package 'ZeroQL 1.2.0'. You are trying to install this package into a project that targets '.NETFramework,Version=v4.8', but the package does not contain any assembly references or content files that are compatible with that framework. For more information, contact the package author.

I'm new to C# et .NET dev so any guidance will be much appreciated ;)

Thanks.

Generated File produces Compiler Error CS0542

Describe the bug
The Code-Generation can produce a class that contains a field with the name of the class.
That leads to Compiler Error CS0542 (member names cannot be the same as their enclosing type)

How to Reproduce
I testet ZeroQL with the public GraphQL API at "https://spacex-production.up.railway.app/".
After generating, the GraphQL.g.cs contained the class Address, that lead to the error:
[System.CodeDom.Compiler.GeneratedCode ( "ZeroQL" , "4.1.0.0" )]
public class Address
{
[ZeroQL.GraphQLFieldSelector("address")]
[JsonPropertyName("address")]
public string? Address { get; set; }

[ZeroQL.GraphQLFieldSelector("city")]
[JsonPropertyName("city")]
public string? City { get; set; }

[ZeroQL.GraphQLFieldSelector("state")]
[JsonPropertyName("state")]
public string? State { get; set; }

}

Screenshots
image

Environment (please complete the following information):

  • Nuget version 4.1.0
  • IDE: VS 2022
  • .Net Version 7

Nullability issue with struct types in the generated client

Describe the bug
I have a scalar Date defined in my GraphQL schema. I have a query defined in the schema that returns a (nullable) list of Dates. I noticed that Date is internally mapped to the DateOnly C# struct and so, the generated method for the said query returns DateOnly?[]?. However in the generated code, the method returns a property of the type DateOnly[], which causes a compilation error since it is of a struct type and cannot be converted to DateOnly?[].

How to Reproduce

  • Have a scalar Date defined in the schema.
  • Have a query in the schema that returns [Date].
  • The generated client code fails to compile.

Expected behavior
The query method should return the struct array properly.

Environment (please complete the following information):

  • ZeroQL library and tools versions [5.0.0]
  • .NET version [7.0.202]

Invalid Casing on Enum Types

I'm working with an API that uses Pascal Case for its enum types. For example,

enum GraphDataType {
  DetailView
  SummaryView
}

ZeroQL is generating GraphDataType.Detailview and GraphDataType.Summaryview.

Then, when I run a query using an enum as an argument query.Report(dataType: GraphDataType.Detailview... I get an error such as

Field "report" argument "dataType" requires type GraphDataType, found DETAILVIEW; Did you mean the enum value DetailView?

How do I get ZeroQL to generate code that respects the enum casing found in the GraphQL schema?

Optional parameters are being set to required

Describe the bug
The generated query is marking a variable as required, but the schema is optional.

How to Reproduce

Expected behavior
The optional parameter shouldn't be marked as required. Ultimately I'd like to be able to not include limit and offset, but c# is yelling at me.

Screenshots
Generated Query:
Screenshot 2023-05-18 at 5 35 43 PM

Source Code:
Screenshot 2023-05-18 at 5 38 13 PM

Implementation:
Screenshot 2023-05-18 at 5 39 14 PM

Environment (please complete the following information):

  • Nuget version [3.6.2]
  • IDE: [VS Code]
  • .Net Version [6.0.400]

Custom scalars

Is there a way to define custom scalars? A simple mapping would be fine

I.e. map a gql defined scalar Date to dotnet DateTime.

Also, we have a scalar defined Point and it just returns the json of the point {x: 1, y: 1}. It would be good to be able to map that to a C# class

public class Point {
  public int X { get; set; }
  public int Y { get; set; }
}

I couldn't find anything in the documentation about this. And the generated scalars just store the returned string value.

Thanks

Custom scalar types using built-in types

Is your feature request related to a problem? Please describe.

The Int type for GraphQL may not be sufficient and one would like to support a wider range. Also sometimes you may want to map a scalar just to some builtin type.

schema {
  query: Query
}
  
type Query {
  bigNumber: Long!,
  bigDecimal: BigDecimal!,
  locale: Locale!
}

"Represents a 64 bit number"
scalar Long

"Represents an arbitrary big decimal number e.g. decimal type"
scalar BigDecimal

"Represents an IETF language tag https://en.wikipedia.org/wiki/IETF_language_tag"
scalar Locale

For now we would have to create a Wrapper class to support this custom scalar.

Describe the solution you'd like

It would be a nice feature to just map this scalar to a built-in type

  "scalars": {
    "Long": "long",
    "BigDecimal": "decimal",
    "Locale": "string"
  }

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.