C# Linq for GraphQL queries

A little about GraphQL

Disclaimer: The article deals only with Query (analogous to GET requests). Mutations and subscriptions are not considered.

GraphQL is a tool that allows you to replace the usual API. Instead of writing controllers and methods, you write methods in Query:

public class GraphQLQuery 
{
  public IQueryable<UserModel> GetUsers([Service] IUsersRepository repository) 
  {
    return repository.Users;
  }
}

Just a couple of lines and you have added a new GraphQL endpoint to your application. Now it can be accessed with a POST request (usually) by passing the following string:

users {
   id
   userName 
   roles {
      code
      description
   }
}

At the output, we will get a list of users with the selected fields – id, userName and a list of roles – roles (with the fields code and description).

This article covers interacting with a GraphQL server from Chilli Cream – HotChocolate. You can check out its documentation. here.

HotChocolate supports endpoint attributes, incl. and self-written, which allow you to add new functionality to your request. For example, you can modify the example above to use predefined attributes:

public class GraphQLQuery 
{
  [UseOffsetPaging]     // Добавили пагинацию
  [UseProjection]       // Добавили проекцию
  [UseFiltering]        // Добавили фильтрацию
  [UseSorting]          // Добавили сортировку
  public IQueryable<UserModel> GetUsers([Service] IUsersRepository repository) 
  {
    return repository.Users;
  }
}

Now we can apply additional tools to the request (filtering, sorting and pagination):

users (
  where: { userName: { startsWith: "a" } }
  order: [{ id: DESC }],
  skip: 100,
  take: 20
) {
  items {
    id
    userName 
    roles {
      code
      description
    }
  }
  pageInfo {
    hasNextPage
    hasPreviousPage
  }
  totalCount   
}

So, we were able to add filtering, sorting and pagination to our GraphQL query. Thanks to the attribute [UseOffsetPaging] our list of users is now wrapped in a special structure and lies in itemsas well as the response contains information about the current page pageInfo and the total number of elements IQueryable<>totalCount.

Conclusion: Thanks to the use of GraphQL, the end user (this could be your front end, for example) does not have to wait for a new filter parameter to be added or a new field to be added to the output model of some GET REST api in your backend. The consumer decides which fields he needs and how he can filter / sort according to your data.

pros

  • There is no need to spend a lot of time creating such a flexible GET REST-api (with filtering, sorting, etc.)

  • The consumer decides how to use your GraphQL methods

  • Minimum time to finalize the backend

  • Optimal database queries (due to query translation to SQL)

Minuses

Description of the problem

From the considered disadvantages, it follows that the most difficult process when using GraphQL is the formation of a Query string. Yes, it can be really time consuming when used on real projects, especially when integrating back-to-back with a GraphQL server.

The process of generating a Query string can look something like this:

var query = @$"
  users (
    where: {{
      {(model.UserId.HasValue ? $"\{ id: \{ eq: {model.UserId} \} \}" : null)}
      {(model.UserName != null ? $"\{ userName: \{ contains: {model.UserName} \} \}" : null)}
    }}
    {model.Order != null
      ? $"order: [\{ {model.Order.Field}: {model.Order.Direction} \}]"
      : null}
  ) {{
    items {{
      id
      name
    }}
  }}
";

What you will definitely encounter:

  • Infinite shielding of everything possible (symbols { And })

  • Heaps of thorns, and most often with a large nesting

  • Exceptions in runtime when the field of the model has changed, and you did not look through all the lines in the project in time and did not find it

Solution

I asked myself a question:

Why is there no adequate and developer-friendly tool to write typed GraphQL queries without building an infinite number of rows?

Strawberry shake

Almost immediately, I found GraphQL clients that are provided by large projects. Take the same ChillliCream – they also have their own GraphQLClient (Strawberry shake). Using it looks like this:

  1. You write in the query file

  2. Run a special code generation tool

  3. Get a typed client with this query

Yes, it’s convenient, but when your query changes frequently, i.e., for example, if you have to pull a different data set (different projection) from the same Endpoint, you will have to constantly invent something, re-generate classes and duplicate query.

GraphQL.Client

Is there some more GraphQL Client from GraphQL-dotnet. The scenario for using it is something like this:

var personAndFilmsRequest = new GraphQLRequest {
    Query =@"
    query PersonAndFilms($id: ID) {
        person(id: $id) {
            name
            filmConnection {
                films {
                    title
                }
            }
        }
    }",
    OperationName = "PersonAndFilms",
    Variables = new {
        id = "cGVvcGxlOjE="
    }
};

Again we came to strings, but now we have variables, it has become a little more convenient. Also, do not forget that for each such request, in a good way, you need to create a DTO class. In general, also not at all what I wanted to see.

Using Expressions

The following thought popped into my head:

Why not try using Expressions to build the required query? After all, we have mechanisms for translating Expressions into SQL for a database in the Entity Framework. Why not do the same?

With this thought, I continued to search for existing solutions.

GraphQL.Query.Builder

The first thing I found was – GraphQL.Query.Builder. Link to GitHub.
The author of the library suggests building a query like this:

IQuery<Human> query = new Query<Human>("humans") // set the name of the query
    .AddArguments(new { id = "uE78f5hq" }) // add query arguments
    .AddField(h => h.FirstName) // add firstName field
    .AddField(h => h.LastName) // add lastName field
    .AddField( // add a sub-object field
        h => h.HomePlanet, // set the name of the field
        sq => sq /// build the sub-query
            .AddField(p => p.Name)
    )
    .AddField<human>( // add a sub-list field
        h => h.Friends,
        sq => sq
            .AddField(f => f.FirstName)
            .AddField(f => f.LastName)
    );

Already not bad, but rather simple, and endless challenges AddField() don’t look very good. In addition, there is no filtering, no sorting, no pagination, and the library api is not similar to the usual Linq.

GraphQLinq.Client

Another library – GraphQLinq.Client.
The author of the library has implemented an api similar to Linq. And the requests look like this:

var launches = await spaceXContext.Launches(null, 10, 0, null, null)
        .Include(launch => launch.Links)
        .Include(launch => launch.Rocket)
        .Include(launch => launch.Rocket.Second_stage.Payloads
                             .Select(payload => payload.Manufacturer));

There is support Include‘ov, Select‘ov. but I never saw filtering and sorting. Of the pluses, it can also be noted that the author offers a tool for generating DTO classes from the GraphQL server schema, which, in general, can be useful and will reduce part of the development time.

Conclusions: GraphQL.Query.Builder and GraphQLinq.Client seem to be more convenient for building GraphQL queries, especially the latter option, which offers similarities to Linq extension methods. But still, we have neither filtering nor sorting.

Implementing your own solution

After reviewing existing solutions, I thought it would be nice to implement my own Linq-like api for building queries to the GraphQL server on Expressions and implement everything that is not in other libraries.

Required functionality:

  • Building projections – Select() And Include()

  • Building conditional expressions – Where()

  • Building sort expressions – OrderBy(), OrderByDescending(), ThenBy(), ThenByDescending()

  • Pagination – Take(), Skip()

  • Custom Arguments – Argument()

  • Various options for materializing the result – ToArrayAsync(), ToListAsync(), ToPageAsync(), FirstOrDefaultAsync(), FirstAsync()

Having decided on the main functionality, I began to develop. It was very convenient to bypass expressions using Visitor‘ov. If you are interested, I will talk about this in more detail and with examples in another article.

Here’s an expression as an example:

client.Query<UserModel>()
  .Where(x => x.Id > 1 && x.Roles.Any(r => r.Code == RoleCodes.ADMINISTRATOR));

Such a Where-expression is translated into the following line (we do not consider the projection yet):

and: [
  { id: { gt: 1 } }
  { roles: { some: { code: { eq: ADMINISTRATOR } } } }
]

We also managed to implement translation for Select expressions.

After bypassing the expression of such a method call Select():

client.Query<UserModel>()
  .Select(x => new
  {
    x.Id,
    x.UserName,
    Roles = x.Roles
      .Select(r => new 
      {
        r.Id,
        r.Code
      })
      .ToArray()
  });

We get the following generated projection string:

id
userName
roles {
  id
  code
}

Putting it all together, I got a mechanism that allows using Expressions to form the correct GraphQL string for a subsequent request to the GraphQL server.

Result:

var users = await client.Query<UserModel>("users")
    .Include(x => x.Roles)
      .ThenInclude(x => x.Users)
    .Where(x => x.UserName.StartsWith("A") || x.Roles.Any(r => r.Code == RoleCode.ADMINISTRATOR))
    .OrderBy(x => x.Id)
      .ThenByDescending(x => x.UserName)
    .Select(x => new 
    {
        x.Id,
        Name = x.UserName,
        x.Roles,
        IsAdministrator = x.Roles.Any(r => r.Code == RoleCode.ADMINISTRATOR)
    })
    .Skip(5)
    .Take(10)
    .Argument("secretKey", "1234")
    .ToListAsync();

Such a request will turn into the following line and materialize the results of the response from the GraphQL server in the form of a list:

{ 
    users (
      where: {
        or: [ 
          { userName: { startsWith: "A" } }
          { roles: { some: { code: { eq: ADMINISTRATOR } } } }
        ]
      }
      order: [
        { id: ASC }
        { userName: DESC }
      ]
      skip: 5
      take: 10
      secretKey: "1234"
  ) {
        id
        userName
        roles {
            code
            name
            description
            id
            users {
                userName
                age
                id
            }
        }
    }
}

Conclusion

It turned out to implement a workable GraphQL client that corresponds to all the declared functionality.

Yes, there is still work to be done:

  • It is necessary to refine the mechanism for translating expressions into a GraphQL string, because not all options can be broadcast correctly

  • Add cursor pagination

  • Add support for scalars

  • Check performance on other GraphQL servers and modify if necessary

It was very interesting to work with expressions and methods for bypassing them. Write in the comments what specific part of the functionality would be interesting to consider in more detail. Ask your questions.

I will be glad to participate in the life of the project – form Issues, make Pull Requests, add new functionality, refactor the code, do not forget to add new Unit tests.

Thank you for your time.

Links

  1. Link to the client’s GitHub repository

  2. Link to Nuget package

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *