ZeroQL - LINQ Graphql Client Updates
Tags: csharp, graphql, tooling, dotnet
Published: 2023-07-18 07:27:05

Today I want to showcase the new version of the ZeroQL C# friendly GraphQL client with a Linq-like interface that allows to call GraphQL endpoints without the need to write raw GraphQL queries. 

The new version brings a bunch of new features, such as improved initial configuration, compatibility with .Net Framework and Unity, a new way to pass variables into query, optional variables, improved scalar types handling, and the ability to get raw GraphQL query from C# lambda.

[Github repository](https://github.com/byme8/ZeroQL)
[Full Github wiki](https://github.com/byme8/ZeroQL/wiki)

## Configuration improvements

ZeroQL's latest update introduces a major simplification to its configuration process, eliminating the need for manual setup and csproj file modifications. This change is centered around the removal of the previous necessity for ZeroQL Command Line Interface (CLI) usage for client generation.

In the past, the initial configuration process required executing the ZeroQL CLI to generate the client using the following command:
``` bash
dotnet zeroql generate 
  --schema .\schema.graphql 
  --namespace TestServer.Client
  --client-name TestServerGraphQLClient
  --output Generated/GraphQL.g.cs
```
There was a way to simplify it by using the configuration file:
``` bash
dotnet zeroql generate -c ./config.zeroql.json
```
With the new version, there is no need for it anymore. It is enough to generate the config file and add the ZeroQL package to the project. The ZeroQL will detect it and automatically execute generation on the build — no changes to csproj and custom scripts are required. 

The generated GraphQL client would be placed inside the `` obj/ZeroQL `` folder. In such a way, you would have access to the client and it would not be visible in the solution viewer or git.

## .Net Standard Support

The new version brings support for the .Net Standard. It opens the possibility of using ZeroQL inside Xamarin, MAUI, Unity, and even legacy .Net Framework projects. 

To make it work, you need to create a `` netstandard `` class library and set the language version to the latest in your  `` csproj ``.
``` xml
	<LangVersion>latestmajor</LangVersion>
```
Subsequently, compatibility must be enabled within the ZeroQL configuration, which can be achieved with the following setting:
``` json
{
	"netstandardCompatibility": true
}
```

That's all for classic `` netstandard `` projects. The full documentation can be found [here](https://github.com/byme8/ZeroQL/wiki/netstandard-setup).

For Unity, setup is more complicated. More info on it can be found in [wiki article](https://github.com/byme8/ZeroQL/wiki/Unity-setup)

## New variables

In previous versions, if you want to pass a variable, you need to create an object, pass it as a parameter, and reuse it inside the lambda:
``` csharp
var input = new { Id = 42 };
var response = client.Query(input, static (i, q) => q.User(i.Id, o => new { ... });
```
With the new version, there is no need for it. You can just pass a variable into lambda closure like that:
``` csharp 
var id = 42;
var response = client.Query(q => q.User(id, o => new { ... });
``` 
This approach still has limitations. The variable should be the local variable or the method parameter. 

Here is an example with the parameter:
``` csharp
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;
}
```
Here is an example that would fail:
``` csharp
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;
}
```
To be clear, you don't need actively account for it. ZeroQL will analyze and report errors if something is wrong.

## Optional Variables

Now the optional variables work as expected. For example, let's have a look at the next GraphQL schema:
``` graphql
type Query {
    users(page: Int!, size: Int!, filter: String): [User!]!
}

type User {
    Id: ID!
    Name: String!
}
```
In previous versions, if you needed to get users without filter, you would need to set it to null:
``` csharp
var page = 1;
var size = 10;
var response = await client.Query(q => q
    .Users(page, size, null, o => new { o.Id, o.Name }));
```
Now you can skip it:
``` csharp
var page = 1;
var size = 10;
var response = await client.Query(q => q
    .Users(page, size, selector: o => new { o.Id, o.Name }));
```

##  New Scalar Types

With ZeroQL, you can map GraphQL scalars into C# classes. There are two ways to do it.
First, just generate a ZeroQL GraphQL client. By default, all custom scalars will be mapped into stubs like that:
``` csharp
public sealed record Instant : ZeroQLScalar
{
    public Instant()
    {
    }

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

    public static implicit operator Instant(string value) => new Instant(value);
    public static implicit operator string (Instant scalar) => scalar.Value;
}
```
If you don't have any special requirements for a scalar, then this way is for you.

The second way allows you to define complex mappings for your scalars. 
For example, we have a schema with scalar `` Instant ``:
``` graphql
schema {
  query: Query
}
  
type Query {
  instant: Instant!
}

"Represents an instant on the global timeline, with nanosecond resolution."
scalar Instant
```

We can create a C# class and JSON serializer and add mapping for it:
``` csharp
public class Instant
{
    public DateTimeOffset DateTimeOffset { get; set; }
}

public class InstantConverter : JsonConverter<Instant?>
{
    public override Instant? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var instant = reader.GetString();
        return new Instant { DateTimeOffset = DateTimeOffset.Parse(instant!) };
    }

    public override void Write(Utf8JsonWriter writer, Instant? value, JsonSerializerOptions options)
    {
        var instant = value.DateTimeOffset.ToString("yyyy-MM-ddTHH:mm:ssZ");
        writer.WriteStringValue(instant);
    }
}

public static class Module
{
    [ModuleInitializer]
    public static void Initialize()
    {
        // don't forget to add serializer to ZeroQL json options
        ZeroQLJsonOptions.Configure(o => o.Converters.Add(new InstantConverter()));
    }
}
```

Then during generation, pass parameter `` --scalars Instant=MyApp.Instant `` or set property scalars in `` zeroql.json `` config file
``` json
"scalars": {
    "Instant": "MyApp.Instant"
}
```

It will replace the GraphQL scalar with the correct type in generated GraphQL client.

## Get Raw GraphQL Query

Some GraphQL endpoints may accept raw GraphQL queries as arguments. Here example:
``` graphql
 type Query {
  user(id: ID!) User!
  readInBatch(graphql: String!, variables: JSON): JSON!
}

type User {
    id: ID!
    name: String!
}
```
Now it is possible to generate GraphQL query without execution:
``` csharp
var queryInfo = QueryInfoProvider.Materialize<Query>(q => q
    .User(id, u => new { u.Id, u.Name }));
// {
//  "Query": "query ($id: ID!) { user(id: $id) { id name } }",
//  "QueryBody": "($id: ID!) { user(id: $id) { id name } }",
//  "OperationType": "query",
}

// then pass values into the query
var variables = // ...
var graphqlQuery = queryInfo.QueryBody;
var response = await client.Query(q => q.ReadInBatch(graphqlQuery, variables));
```

## Final thoughts

In conclusion, the newest version of ZeroQL offers many features and improvements that enhance the usability of the GraphQL client. The simplified initial configuration process saves time and effort, while the extended .Net Standard support significantly broadens the client's potential usage. The new variables help to align expectations with a 'classic' LINQ-like interface.

Overall it is a huge update that makes building GrapgQL queries even easier. 

The full source code can be found in the [Github repository](https://github.com/byme8/ZeroQL). 

Have a look and press the start button. 😊