Some more advanced GraphQL concepts with HotChocolate - Part 3

In this article, we'll thoroughly examine the process of executing queries within a GraphQL server.

It's imperative to emphasize once more: GraphQL operates as a specification, necessitating the installation or implementation of a runtime on the server side. As a result, the client cannot merely wait for data passively; instead, it assumes an active role and must explicitly articulate its data requirements. This section will elucidate how this process is delineated within the GraphQL specification, detailing how clients can specify their requests.

What does SDL stand for ?

GraphQL specification introduces its unique type language, designated as the Schema Definition Language (SDL), which serves as the medium for crafting GraphQL schemas. For instance, a customer would be defined as follows in SDL.

1type Customer {
2   id: ID!
3   name: String
4   age: Int
5}

In this snippet, we define a Customer type comprising three fields: one of type ID, one of type String representing alphanumeric data and the third of type Int indicating an integer value. Notably, we specify that the Id field is mandatory by appending an exclamation mark (!) to it.

Information

The ID type resolves to a string, but expects a unique value.

Similarly, we could define an Order type to represent an order within an ecommerce application. It might be defined as follows.

1type Order {
2   id: ID!
3   reference: String!
4   amount: Float
5   customerId: ID!
6}

However, orders and customers are not mutually exclusive entities; customers can have multiple orders, while an order is frequently linked to a specific customer. Therefore, we can enhance the model as follows.

 1type Customer {
 2   id: ID!
 3   name: String
 4   age: Int
 5   orders: [Order]
 6}
 7
 8type Order {
 9   id: ID!
10   reference: String!
11   amount: Float
12   customer: Customer
13}

Here, we specify that a customer can possess an array of orders, denoted by the square brackets [ and ].

How to perform a query on these types ?

The Customer and Order types themselves do not provide any functionality to client applications; they solely outline the structure of the entities available. To execute a query, the GraphQL specification mandates the inclusion of the Query root type.

Information

What do root types entail ? These are intrinsic types outlined in the specification and can be regarded as entry points for any GraphQL API. In essence, there are three distinct root types available: Query (for data retrieval), Mutation (for data modification), and Subscription.

To provide clients with the capability to retrieve either all customers or a specific customer by ID, we must define a Query type as follows.

1type Query {
2   customers: Customer
3   customer(id:ID!): Customer
4}

At this juncture, the server has completed its task, and the client assumes responsibility. The client can no longer simply access an endpoint and passively await the response; it must actively specify the data it requires. This can be accomplished through either a GET or a POST request.

The client assumes responsibility.

Information

As elucidated in our prior series, there's no necessity for a plethora of endpoints. In practice, a singular endpoint suffices to fulfill requirements, commonly denoted as /graphql. We will adhere to this guideline accordingly.

Querying with a POST request

A standard GraphQL POST request should use the application/json content type, and include a JSON-encoded body of the following form.

1POST http://<path_api>/graphql
2{
3    "query": "query customer (id: "123f0") { id, age, orders { reference } }",
4}

According to the GraphQL specification, the response will consist of a formatted JSON payload.

1{
2  "data": {
3    "customer": {
4      "id": "123f0",
5      "age": "45",
6	  "orders": []
7    }
8  }
9}
Information

In case the response includes an error, GraphQL will provide a corresponding formatted message.

 1{
 2  "errors": [
 3    {
 4      "message": "<...>",
 5      "locations": [
 6        {
 7          "line": 2,
 8          "column": 1
 9        }
10      ]
11    }
12  ]
13}

Querying with a GET request

Performing a GET request is also feasible, but it tends to be more cumbersome; consequently, it is less commonly utilized compared to its POST counterpart. For interested readers, we recommend referring to the official documentation for further details.

What are resolvers ?

Up to this point, we've solely outlined in SDL the types and queries available with these types. However, we haven't addressed how the fields are retrieved from any data store. Resolvers are just that: they serve as the bridge between the specifications, defining the structure we desire, and the concrete implementations detailing how to obtain it.

Information

The GraphQL specification does not offer precise guidelines on how resolvers should be implemented. It primarily emphasizes that this concept must be present and extensible within the library.

We won't delve deeply into this concept because it seems intuitive for most developers: ultimately, for an API to be functional, it must gather data from a datastore at some point. A resolver is merely a function that understands how to extract certain data.

Now that we comprehend the concept of a query and a resolver within the specification, let's explore how it is tangibly implemented in practice.

How is SDL integrated with HotChocolate ?

Information

This implementation is entirely specific to HotChocolate, and other libraries may implement queries and resolvers differently.

Implementing resolvers

Recall from the previous post that we utilized a MockCustomerRepository class to interact with customers.

 1public class MockCustomerRepository : ICustomerRepository
 2{
 3    public List<Customer> GetAllCustomers()
 4    {
 5        return new List<Customer>()
 6        {
 7            new Customer(){ Id = "0001", Name = "Bruce Smith", Age = 45 },
 8            new Customer(){ Id = "0010", Name = "Melissa Price", Age = 52 }
 9        };
10    }
11	
12    public Customer GetCustomerById(string id)
13    {
14        return new Customer(){ Id = "0001", Name = "Bruce Smith", Age = 45 };
15    }
16}

If everything is understood thus far, these methods will serve as resolvers and will need to be invoked at some point to retrieve data.

Implementing the Query type

Before exploring how to retrieve data with a query, we need to understand how SDL types are represented in C#. HotChocolate simply requires the creation of simple POCO (plain old CLR object) classes as follows.

 1public class Customer
 2{
 3    public string Id { get; set; }
 4
 5    public string Name { get; set; }
 6
 7    public int Age { get; set; }
 8	
 9    public List<Order> Orders { get; set; }
10}
11
12public class Order
13{
14    public string Id { get; set; }
15
16    public string Reference { get; set; }
17
18    public decimal Amount { get; set; }
19	
20    public Customer Customer { get; set; }
21}

With these classes in place, we are now ready to execute a query. To do so, HotChocolate necessitates the definition of a Query class and the integration of all components (POCO classes and resolvers).

 1public class Query
 2{
 3    public List<Customer> GetCustomers([Service] ICustomerRepository customerRepository)
 4    {
 5        return customerRepository.GetAllCustomers();
 6    }
 7	
 8    public Customer GetCustomerById([Service] ICustomerRepository customerRepository, string id)
 9    {
10        return customerRepository.GetCustomerById(id);
11    }
12}

Here, we observe that resolvers are obtained via dependency injection using the [Service] annotation. However, it's worth noting that this isn't the sole method for managing resolvers, nor is it the most prevalent. For further details, please consult the documentation.

Once all components are configured, we need to modify the StartUp class to instruct the server to adhere to the GraphQL specification.

 1public class StartUp : FunctionsStartup
 2{
 3    public override void Configure(IFunctionsHostBuilder builder)
 4    {
 5        ConfigureServices(builder.Services);
 6    }
 7
 8    private static void ConfigureServices(IServiceCollection services)
 9    {
10        services.AddSingleton<ICustomerRepository, MockCustomerRepository>();
11
12        services.AddGraphQLFunction().AddQueryType<Query>();
13    }
14}
Information

This example illustrates how HotChocolate streamlines our tasks by allowing us to effortlessly define GraphQL types using native classes. Behind the scenes, HotChocolate handles the heavy lifting by adhering to the GraphQL specification.

Consuming the service

We will now transition to the client project.

  • Edit the indexQuery.html file
 1<html>
 2<head>
 3    <title>GraphQL</title>
 4</head>
 5<body>
 6    <pre><code class="language-json" id="code"></code></pre>
 7    <script src="https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.14.9/beautify.min.js"></script>
 8    <script>
 9        (async function () {
10            const data = JSON.stringify({
11                query: `query {
12                          customerById(id:"0010") {
13                            id
14                            name
15                            age
16                          }
17                        }`
18            });
19
20            const response = await fetch(
21                'http://localhost:7132/api/graphql',
22                {
23                    method: 'post',
24                    body: data,
25                    headers: {
26                        'Content-Type': 'application/json'
27                    },
28                }
29            );
30
31            const json = await response.json();
32            document.getElementById('code').innerHTML = js_beautify(
33                JSON.stringify(json.data)
34            );
35        })();
36    </script>
37</body>
38</html>
Important

In the previous example, we noticed that the method name invoked in JavaScript is "customerById". This naming convention is employed by HotChocolate: the server expects to find a resolver that ends with "customerById" (case insensitive).

  • Run the program

Navigate to the indexQuery.html file and observe the result.

It's evident that the mechanisms are functioning correctly, and the appropriate resolver is being invoked.

This concludes our lengthy post, but it's only the beginning of the journey. Next, we will delve into how data can be modified using GraphQL.

Advanced GraphQL concepts with HotChocolate - Part 4