JuanuMusic / ZeroQL

C# GraphQL client

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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 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
# 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 bootstrap schema.graphql file from graphql schema
dotnet zeroql generate --schema ./schema.graphql --namespace TestServer.Client --client-name TestServerGraphQLClient --output Generated/GraphQL.g.cs

It is possible to add next target to csproj to keep generated client in sync with schema.graphql:

<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 a result, the graphql client will be generated on every build.

Config

There is a way to simplify the CLI command. The command dotnet zeroql config init creates the zeroql.json. It may look like that:

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

Then we can use it like that:

dotnet zeroql generate -c ./zeroql.json

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 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 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 = @"{ ""query"": ""query { me { firstName }}"" }";
    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"]["me"]["firstName"].GetValue<string>();
}

[Benchmark]
public async Task<string> StrawberryShake()
{
    var firstname = await strawberryShake.Me.ExecuteAsync();
    return firstname.Data.Me.FirstName;
}

[Benchmark]
public async Task<string> ZeroQL()
{
    var firstname = await zeroQLClient.Query(static q => q.Me(o => o.FirstName));

    return firstname.Data;
}

Here results:

BenchmarkDotNet=v0.13.2, OS=macOS 13.2.1 (22D68) [Darwin 22.3.0]
Apple M1, 1 CPU, 8 logical and 8 physical cores
.NET SDK=7.0.200
  [Host]     : .NET 7.0.3 (7.0.323.6910), Arm64 RyuJIT AdvSIMD
  DefaultJob : .NET 7.0.3 (7.0.323.6910), Arm64 RyuJIT AdvSIMD
Method Mean Error StdDev Gen0 Allocated
Raw 172.2 μs 1.49 μs 1.40 μs 0.7324 4.96 KB
StrawberryShake 175.0 μs 1.18 μs 1.05 μs 1.4648 9.32 KB
ZeroQLLambda 174.2 μs 1.26 μs 1.17 μs 0.7324 5.5 KB
ZeroQLRequest 174.8 μs 1.68 μs 1.49 μs 0.7324 5.88 KB
ZeroQLLambdaUpload 208.5 μs 2.06 μs 1.83 μs 1.4648 10.34 KB
ZeroQLRequestUpload 208.9 μs 3.02 μs 2.83 μs 1.7090 10.43 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 performace.

About

C# GraphQL client

License:MIT License


Languages

Language:C# 99.1%Language:PowerShell 0.9%