ZeroQL - C# GraphQL client adds fragments support

byme8

Stanislav Silin

Posted on July 23, 2022

ZeroQL - C# GraphQL client adds fragments support

In my previous article I introduce you to ZeroQL.
It is a C# "native" graphql client with a Linq-like interface with no compromises on the performance.
In this article, I want to present support for fragments.

What do fragments mean?

In the graphql, you have to specify every field that you want to fetch from the server like that:

query GetMe {
    me {
        id
        firstName
        lastName
    }
}
Enter fullscreen mode Exit fullscreen mode

It is fine if you need to do it only once, but, in some cases, you may want to get the same set of fields again and again. It may become too verbose:

query GetMeAndFriend($friendId: Int!) {
    me {
        id
        firstName
        lastName
    }
    user(id: $friendId) {
        id
        firstName
        lastName
    }
}
Enter fullscreen mode Exit fullscreen mode

To make life easier, GraphQL has support fragments. They allow to define a set of fields and then reuse them when needed.

fragment UserFields on User {
    id
    firstName
    lastName
}

query GetMeAndFriend($friendId: Int!) {
    me {
        ...UserFields
    }
    user(id: $friendId) {
        ...UserFields
    }
}
Enter fullscreen mode Exit fullscreen mode

I guess the idea is clear. Now let's see how we can create fragments in side the C#.

We will work with next GraphQL schema:

schema {
  query: Query
  mutation: Mutation
}

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

type Mutation {
  addUser(firstName: String!, lastName: String!): User!
}

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

type Role {
  id: Int!
  name: String!
}
Enter fullscreen mode Exit fullscreen mode

How to setup the ZeroQL you can find in this article.

The GetMeAndFriend GraphQL query from the sample above looks like that:

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

Console.WriteLine(response.Query); // query ($friendId: Int!) { me { id firstName lastName }  user(id: $friendId) { id firstName lastName } }
Console.WriteLine(response.Data); //  { Me = { Id = 1, FirstName = Jon, LastName = Smith }, User = { Id = 2, FirstName = Ben, LastName = Smith } }
Enter fullscreen mode Exit fullscreen mode

Let's move user fields into a fragment. For this, we need a user model and an extension method::

public record UserModel
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public static class UserFragments
{
    [GraphQLFragment]
    public static UserModel AsUserModel(this User user)
    {
        return new UserModel
        {
            Id = user.Id,
            FirstName = user.FirstName,
            LastName = user.LastName
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we can rewrite the query like that:

var variables = new { FriendId = 2 };
var response = await client.Query(
    variables, 
    static (i, q) => new
    {
        Me = q.Me(o => o.AsUserModel()),
        User = q.User(i.FriendId, o => o.AsUserModel()),
    });

Console.WriteLine(response.Query); // query ($friendId: Int!) { me { id firstName lastName }  user(id: $friendId) { id firstName lastName } }
Console.WriteLine(response.Data); // { Me = UserModel { Id = 1, FirstName = Jon, LastName = Smith }, User = UserModel { Id = 2, FirstName = Ben, LastName = Smith } }
Enter fullscreen mode Exit fullscreen mode

And everything works as expected. If you look at the generated GraphQL query, you will see that, strictly speaking, it is not a "graphql fragment." It is more of a sub-query that is inserted into a final query. So, we can go further and combine multiple queries like that:

var variables = new { FriendId = 2 };
var response = await client.Query(
    variables, 
    static (i, q) => q.GetMeAndFriend(i.FriendId));

Console.WriteLine(response.Query); // query ($friendId: Int!) { me { id firstName lastName }  user(id: $friendId) { id firstName lastName } }
Console.WriteLine(response.Data); // MeAndFriendResponse { Me = UserModel { Id = 1, FirstName = Jon, LastName = Smith }, Friend = UserModel { Id = 2, FirstName = Ben, LastName = Smith } }

// ...

public record MeAndFriendResponse
{
    public UserModel Me { get; set; }
    public UserModel Friend { get; set; }
}

public static class QueryFragments
{
    [GraphQLFragment]
    public static MeAndFriendResponse GetMeAndFriend(this Query query, int friendId)
    {
        return new MeAndFriendResponse
        {
            Me = query.Me(o => o.AsUserModel()),
            Friend = query.User(friendId, o => o.AsUserModel())
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

And again, everything works as expected.

Limitations

There is one thing that can make life a bit complicated. The source generator looks through the source code to generate the query. It also needs to look into every sub-query, which is fine until it is defined in another assembly/project. In this case, the source generator can't dig into it because the source code for such assemblies is not available. As a result, we can't generate a query for them. If we want to make the fragment work, it should be defined in the same assembly that we call the client.Query or client.Mutation methods. This requirement definitely can break some workflows and expectations. At the same time, how often do you need to split your SQL queries into different assemblies? This happens, but not so often. In the worst-case scenario, you must copy-paste fragments from one project to another.

Again, it doesn't mean this issue can not be fixed in the future. I have some ideas on how to solve it, but at the moment, it is just ideas with tricky implementation. Meanwhile fight is not over.

Performance

I have said that the ZeroQL has excellent performance. But how excellent is it?
The repository contains a benchmark. It compares raw graphql request, StrawberryShake, and ZeroQL.

The short version looks like that:

[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(); // query { me { firstName }}
    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;
}
Enter fullscreen mode Exit fullscreen mode

Here results:


BenchmarkDotNet=v0.13.1, OS=macOS Monterey 12.4 (21F79) [Darwin 21.5.0]
Apple M1, 1 CPU, 8 logical and 8 physical cores
.NET SDK=6.0.302
  [Host]     : .NET 6.0.7 (6.0.722.32202), Arm64 RyuJIT
  DefaultJob : .NET 6.0.7 (6.0.722.32202), Arm64 RyuJIT


Enter fullscreen mode Exit fullscreen mode
Method Mean Error StdDev Gen 0 Allocated
Raw 182.5 μs 1.07 μs 1.00 μs 2.4414 5 KB
StrawberryShake 190.9 μs 0.74 μs 0.69 μs 3.1738 6 KB
ZeroQL 185.9 μs 1.39 μs 1.30 μs 2.9297 6 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. The difference is negligible.

Summary

So, with the ZeroQL you can forget about the graphql and just use a fully typed Linq-like interface. It will have little effect on performance. In the future, I plan to figure out how to generate queries for fragments defined in another assembly and think about how to make the initial setup more straightforward.

Thank you for your time!
If you like what I am working on. Please, give a start to the Github repository so more people can see it.

Links

Github
NuGet

💖 💪 🙅 🚩
byme8
Stanislav Silin

Posted on July 23, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related