ZeroQL V2 - C# GraphQL client
Tags: graphql, csharp, opensource
Published: 2022-10-09 15:07:56


In this article, I want to present the major update for ZeroQL. It brings new features and support for more complex workflows.
Such as file uploads, new request-like way to define queries and mutations, persisted queries, and more.
If you are interested, continue reading.

## What is ZeroQL?

If you read my previous articles[[1]](https://dev.to/byme8/zeroql-c-friendly-graphql-4134)[[2]](https://dev.to/byme8/zeroql-c-graphql-client-adds-fragments-support-1lcf) you know that ZeroQL is a C# GraphQL client that allows you to write GraphQL queries with C# in a Linq-like way. I will give you a quick overview for those who don't know.

Let's suppose that we have a GraphQL schema like that:
``` graphql
schema {
  query: Query
  mutation: Mutation
}

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

type Mutation {
  addUser(firstName: String!, lastName: String!): User!
  updateUser(userId: Int!, firstName: String!, lastName: String!): User!
  addAvatar(userId: Int!, avatar: Upload!): Boolean!
}

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

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

The ZeroQL allows to generate a fully-typed client:
``` bash
# create console app
dotnet new console -o QLClient
# go to project folder 
cd QLClient
# fetch graphql schema from server(depends on your web server)
curl http://localhost:10000/graphql?sdl > schema.graphql 
# create manifest file to track nuget tools
dotnet new tool-manifest 
# add ZeroQL.CLI nuget tool
dotnet tool install ZeroQL.CLI
# add ZeroQL nuget package
dotnet add package ZeroQL 
# to bootstrap schema.graphql file from graphql schema
dotnet zeroql generate --schema .\schema.graphql --namespace TestServer.Client --client-name TestServerGraphQLClient --output Generated/GraphQL.g.cs
```

Now we can write GraphQL queries like that:
``` csharp
var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri("http://localhost:10000/graphql");

var client = new TestServerGraphQLClient(httpClient);
var response = await client
  .Query(static 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
```

The `` q.Me `` method is a query field from the schema, and the `` o => new { o.Id, o.FirstName, o.LastName } `` is a selection set.
When we call the `` Query `` method, the ZeroQL generates a GraphQL query from the C# code at compile time. Then at runtime, it sends the query to the server and gets the response.
As a result, the performance is the same as if we write a GraphQL query manually.

## File uploading

The new version supports the official way to upload files. I mean the `` Upload `` scalar type.
Here is a sample GraphQL query:
``` graphql
mutation AddAvatar($id: Int!, $file: Upload!) {
  addAvatar(id: $id, file: $file)
}
```
Now we can replicate it like that:
``` csharp
var variables = new
{
    Id = id,
    File = new Upload("avatar.png", imageStream)
};
var response = await client.Mutation(variables, static (i, m) => m.AddAvatar(i.Id, i.File));
```
The GraphQL client will create the multipart request and send the request in parts. For more info about the protocol, you can look [here](https://chillicream.com/docs/hotchocolate/server/files#client-usage).

## Request Syntax

ZeroQL brings a new way to define GraphQL queries. The main goal of it is to provide a way to extract complex queries into a separate entity and then reuse them inside the business layer. 
For example, here is a more complex query:
``` graphql
mutation UpdateUser($id: Int!, $avatar: Upload!, $firstName: String!, $lastName: String!) {
  updateUser(firstName: $firstName, lastName: $lastName) {
    id,
    firstName,
    lastName,
  },
  addAvatar(userId: $id, avatar: $avatar)
}
```
The lambda syntax will look like that:

``` csharp
var variables = new 
{
    Id = id,
    FirstName = firstName,
    LastName = lastName,
    Upload = new Upload("avatar.png", imageStream)
};

var response = await client
    .Mutation(variables, static (i, m) => new
    {
        User = m.UpdateUser(i.Id, i.FirstName, i.LastName, o => new UserResponse(o.Id, o.FirstName, o.LastName)),
        AvatarUpdated = m.AddAvatar(i.Id, i.Upload)
    });
```
Such implementation inside a service looks clunky and too verbose. The request syntax makes it much simple:
``` csharp
// somewhere in a proper place
public record UpdateUserResponse(UserResponse User, bool AvatarUpdated);

public record UpdateUser(int Id, string FirstName, string LastName, Upload File) 
  : GraphQL<Mutation, UpdateUserResponse>
{
    public override UpdateUserResponse Execute(Mutation mutation) => new UpdateUserResponse(
        mutation.UpdateUser(Id, FirstName, LastName, o => new UserResponse(o.Id, o.FirstName, o.LastName)),
        mutation.AddAvatar(Id, File));
}

// ...

// inside the service
var response = await client.Execute(new UpdateUser(id, firstName, lastName, new Upload("avatar.png", imageStream)));
```
As I said, the main advantage is splitting the query declaration and query execution. Also, it can be useful if you execute a query via AOT runtime. The "lambda"-like syntax relay on anonymous types to pass the query variables. It is fine when you need just to serialize the instance, but it has issues when you want to get some value from the fields. A good example would be the `` Upload `` type. It requires special handling, and in the case of an anonymous type, it requires reflection and Reflection.Emit to handle it with acceptable performance. As a result, when the app is compiled in AOT mode, it will fail at runtime. The request-like syntax doesn't have such an issue because query variables would have a concrete type, and we can interact with it without the need for reflection.


## Persisted queries

The new version brings support for graphql persisted queries. The ZeroQL supports "static persisted queries" and "automatic persisted queries" pipelines. 

The persisted queries can generally reduce the request size and execution time. For example, if we have the next request:
``` json
{
  "variables" : { "id" : 1, "avatar": null, "firstName": "John", "lastName": "Smith" },
  "query": "mutation UpdateUser($id: Int!, $avatar: Upload!, $firstName: String!, $lastName: String!) {
  updateUser(firstName: $firstName, lastName: $lastName) {
    id,
    firstName,
    lastName,
  },
  addAvatar(userId: $id, avatar: $avatar)
}"
}
```
With persistent queries it will be transformed into this:
``` json
{
  "variables" : { "id" : 1, "avatar": null, "firstName": "John", "lastName": "Smith" },
  "persistedQuery": {
    "version":1,
    "sha256Hash":"420548026ac8cec25f7c4c592c3adc9140c9a9d70a9cfbda8a21b92b01b548f2"
  }
}
```
It brings a bunch of improvements. First, the request is smaller, and the network usage is lower. Secondly, the hash works like an id. The GraphQL server can cache and reuse the execution pipeline without the need to parse the GraphQL syntax on every request. As a result, the request execution time can be much lower. You can think about it like a "sql procedure." You pass the name, and the SQL server knows what to do next.

### Automatic persisted queries
Basically, on the first attempt, the client will send the "hashed" request. Then if the server identifies the hash, it will execute the appropriate query. If such hash is missing on the server, the request is rejected. 
Then the client will send another request like that:
``` json
{
  "variables" : { "id" : 1, "avatar": null, "firstName": "John", "lastName": "Smith" },
  "query": "mutation UpdateUser($id: Int!, $avatar: Upload!, $firstName: String!, $lastName: String!) {
  updateUser(firstName: $firstName, lastName: $lastName) {
    id,
    firstName,
    lastName,
  },
  addAvatar(userId: $id, avatar: $avatar)
}",
  "persistedQuery": {
    "version":1,
    "sha256Hash":"420548026ac8cec25f7c4c592c3adc9140c9a9d70a9cfbda8a21b92b01b548f2"
  }
}
```
The server will remember the hash and the query. So, the next time when the client sends the hash, the request will be executed as expected.

You can enable it for the ZeroQL client by passing `` PersistedQueryPipeline `` pipeline into the client constructor like that:
``` csharp
var client = new TestServerGraphQLClient(httpClient, new PersistedQueryPipeline()); 
var response = await client.Execute(new GetUserQuery(1)); 

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

From the ZeroQL client perspective, it is all you need to do. Additionally, you will need to configure your GraphQL server, but it is a totally different story. You can find more information about how to do it with the HotChocolate server [here](https://chillicream.com/docs/hotchocolate/performance/automatic-persisted-queries).

### Static persisted queries
This workflow requires additional interaction between a server and a client at build time. The idea is the same, but the server expects to know about all possible queries and their hashes before the request happens. To make it possible, you need to export queries from the client.
For ZeroQL it can be done via ZeroQL.CLI like that:
``` bash 
 dotnet zeroql queries extract -a .\bin\Debug\net6.0\TestProject.dll -c TestServer.Client.TestServerGraphQLClient -o ./queries
```

The `` queries `` folder will contain a bunch of "hashed" GraphQL files. Inside they will have a GraphQL query associated hash in file name:

``` bash
8cc1ee42eecdac2a8590486826856c041b04981a2c55d5cc560c338e1f6f0285.graphql # query GetUserQuery($id: Int!) { user(id: $id) { id firstName lastName } }
21cc96eaf0c0db2b5f980c8ec8b5aba2e40eb24f370cfc0cd7e4825509742ae2.graphql # mutation AddAvatar($id: Int!, $file: Upload!) { addUserProfileImage(userId: $id, file: $file)}
```

Then you need to configure your server to access them. How to do it for the HotChocolate server is described [here](https://chillicream.com/docs/hotchocolate/performance/persisted-queries).

## Conclusion

That is all that I wanted to share right now. I would say it is a big release with improvements that extend the capabilities of ZeroQL. The support for file uploading, persisted queries, and request-like syntax opens possibilities for new workflows and optimizations. 

The ZeroQL repository is [Github](https://github.com/byme8/ZeroQL).
Feel free to create issues and ask questions.