19 May 2020

DadenU Day: GraphQL

From Steven:

GraphQL is a data query language to make it easier and more intuitive to use APIs. Existing APIs use fixed endpoints to give the user what they want, however, the fixed nature means that that endpoint can give too much or too little information. These are called over-fetching and under-fetching. Over-fetching uses more intensive database calls on the server and more bandwidth, whereas under-fetching requires more API calls and more complexity on the client. GraphQL allows the user to define what data they want.

I used a prewritten example to experiment with this in the C# library GraphQL for .NET. The example includes a nice GUI playground to test queries. An example GraphQL query is show in Figure 1.



Figure 1:An example query from the playground.

The GraphQL query consists of a type “query”, a name “TestQuery”, a reference to the query to query “reservations”, a list of arguments to the query, and a list of fields to include in the result. This allows the ability to include only the fields you want and no more. It is passed in a standard get or post request to /graphql by default, so very easy to use.

The result of this GraphQL query is shown in Figure 2. It is in JSON format so is easily read by humans and easily parsed with standard libraries. It looks similar in structure to the query, which allows quick checking of the results.




Figure 2:The result from the query in Figure 1.

The example uses a simple hotel reservation system with rooms, reservations and guests. Each of these has its own C# class as normal. To make the GraphQL classes, we need to define a new ObjectGraphType that takes the existing type as a generic parameter as in Figure 3.



Figure 3:The reservation ObjectGraphType. It takes the existing Reservation type as a generic parameter, but fields still need to be defined in the constructor.


Each property in that type must then be given in the constructor by applying the Field method to it. Complex properties are defined manually by giving the type as a generic parameter and the string for querying, though this seems like a library limitation.

A GraphQL query is defined as another ObjectGraphType where the Field method defines the name of the query, a list of arguments to pick up on, and a resolve method that is called to resolve the GraphQL query using the defined arguments. This is shown in Figure 4. The return type is a List of Reservations as expected, but each type is its counterpart in GraphQL.



Figure 4:The reservation query. The argument and resolve parameters are defined unlike in Figure 3.

The resolve function gets passed a context, which contains all the arguments and subfields from the GraphQL query. In the function you can then build up a query to access data depending on what is in the GraphQL query. The arguments can end up triggering complex filters, not just equality checks, and could include sorting.

In GraphQL, there are not just “queries”, there are also “mutations” that allow data change, and “subscriptions” that allow real-time updates. These are wrapped up in a schema, although I am only using queries here. The schema is shown in Figure 5.



Figure 5:The schema used in the example.

All these classes are made available through dependency injection in Startup.cs as shown in Figure 6. Every GraphQL type needs added, and the Query is retrieved in the Schema through the service provider.



Figure 6:The Startup.cs showing the level of dependancy injection required.

The resolve function was originally a list of if-else statements with similar structure. I changed it to a series of chained calls to the same function to remove code duplication and improve readability. I also added conditional inclusion so that the extra fields are only included if they exist in the GraphQL query. This cuts down on the cost of database calls. These methods are shown in Figure 7.

Overall GraphQL is something I will consider in a future project. Once the initial hurdle has been overcome it is powerful, and can be backed up by a standard API if necessary.

Expressions



Figure 7:IQueryable extensions to enable conditional including and generic equal functions.

I wanted to be able to pass a lambda to select a field in the class, a string for the argument and a list of validation functions with messages. I am using Entity Framework Core (EF) to do the database calls, so to avoid execution of the query until the end, I had to write the lambda as an expression. Expressions are the form the C# compiler keeps the function logic until they are compiled and are also used by EF to translate queries into SQL, instead of C#.

In C# you can define a lambda as an Expression<Func<>> and C# will automatically convert the lambda into an expression. However, building on an expression requires you to use the expression tree API and define every little part of the lambda including parameters to build up the tree. In the AddEqual method, you can see the method I wanted to write commented out at the end of the return line. The expression tree building took four statements to build up, by defining the next small bit of the function its own statement. In the end though, it allows the same method to be used for a generic type where you want to check equality in an EF query.

As an aside, checking equality generically for all types in a function (value vs reference, nullable vs non-nullable) required a call to EqualityComparer<T>.Default.Equals(x, default) instead of just using == or .Equals().

In the Include function I had to pass an expression and examine the PropertyInfo to check if that property exists in the subfields.

No comments:

Post a Comment