Click here to Skip to main content
15,887,214 members
Articles / Hosted Services / Microservices

Some More Advanced GraphQL Concepts with HotChocolate

Rate me:
Please Sign up or sign in to vote.
3.21/5 (4 votes)
7 Mar 2024CPOL8 min read 1.9K   27   1   3
Understanding more advanced concepts in GraphQL with HotChocolate
Beginning today, we initiate a journey through a series of articles dedicated to exploring advanced concepts in GraphQL. Our objective is to delve further than our previous endeavors, examining queries, mutations, resolvers, and associated notions to foster a profound comprehension of the fundamental frameworks. In illustrating our scenarios, we will leverage HotChocolate.

Introduction

In a preceding series of articles, we extensively explored the essence of GraphQL and the rationale behind its inception by Facebook in the early 2010s. Specifically, GraphQL empowers us to precisely retrieve the desired data, neither more nor less, while circumventing the need for numerous endpoints. For a comprehensive elucidation, please refer here: Building GraphQL API with HotChocolate

While we briefly demonstrated an example utilizing HotChocolate and a JavaScript client, showcasing GraphQL's capability to deliver only the requested payload, we did not extensively explore the advanced intricacies of GraphQL. Therefore, the focus of this series is to address this gap by delving deep into the most intricate aspects of the specification. Our aim is to elucidate the most complicated facets comprehensively.

The subsequent textbooks prove useful for concluding this series:

This article was originally published here:

What is GraphQL?

GraphQL emerged from Facebook's development efforts in the early 2010s with the primary objective of optimizing data transmission over networks, particularly for mobile applications where bandwidth efficiency was crucial. Subsequently, it was formalized as a specification that servers must adhere to in order to fulfill the contractual requirements. This flexible approach allows GraphQL servers to be implemented in virtually any programming language, and it's not uncommon to find multiple implementations in the same language.

Very Important

It's essential to underscore that GraphQL solely serves as a SPECIFICATION. Consequently, APIs leveraging this technology necessitate deployment on servers that conform to this specification and possess the ability to comprehend and fulfill requests crafted in the corresponding language. In practice, a GraphQL server is typically instantiated through the installation of a library or a runtime environment tailored for GraphQL implementation.

HotChocolate serves as an illustration of a C# implementation of the GraphQL specification.

For more details, please refer to:

What is HotChocolate?

HotChocolate is an open-source GraphQL server for the Microsoft .NET platform that is compliant with the newest GraphQL October 2021 spec + Drafts, which makes Hot Chocolate compatible to all GraphQL compliant clients.
https://chillicream.com/docs/hotchocolate/v13

We'll set up a basic GraphQL server using HotChocolate in an Azure Function, and then we'll query it with JavaScript.

Creating the Environment

  • Create a new solution named EOCS.GraphQLAdvanced for example and a new Azure Functions project in it.

    Image 1

  • Create a new class named Customer.cs and add the following code to it:

    C#
    public class Customer
    {
        public string Id { get; set; }
    
        public string Name { get; set; }
    
        public int Age { get; set; }
    }
  • Create a new interface named ICustomerRepository and add the following code in it:
    C#
    public interface ICustomerRepository
    {
        List<Customer> GetAllCustomers();
        Customer GetCustomerById(string id);
    }
  • Create a new class named MockCustomerRepository that implements the ICustomerRepository interface.
    C#
    public class MockCustomerRepository : ICustomerRepository
    {
        public List<Customer> GetAllCustomers()
        {
            return new List<Customer>()
            {
                new Customer(){ Id = "0001", Name = "Bruce Smith", Age = 45 },
                new Customer(){ Id = "0010", Name = "Melissa Price", Age = 52 }
            };
        }
        
        public Customer GetCustomerById(string id)
        {
            return new Customer(){ Id = "0001", Name = "Bruce Smith", Age = 45 };
        }
    }
  • Add the HotChocolate.AzureFunctions NuGet package:

    Image 2

  • Create a class named CustomerService.cs (or a more general name) and add the following code to it:

    C#
    public class CustomerService
    {
        private readonly IGraphQLRequestExecutor _executor;
    
        public CustomerService(IGraphQLRequestExecutor executor)
        {
            _executor = executor;
        }
    
        [FunctionName(nameof(Run))]
        public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Anonymous, 
                     "get", "post", Route = "graphql/{**slug}")] HttpRequest req)
        {
            return await _executor.ExecuteAsync(req);
        }
    }
  • Create a class named StartUp.cs to bootstrap the Azure Function.
    C#
    public class StartUp : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            ConfigureServices(builder.Services);
        }
    
        private static void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<ICustomerRepository, MockCustomerRepository>();
        }
    }

Very Important

This setup is currently non-functional; it merely establishes the framework for future development.

Consuming the Service

Now it's time to consume our service, and for this, we will create a simple HTML file.

  • Create a new ASP.NET Core Web App project, name it EOCS.GraphQLAdvanced.Client for example and add a file named indexQuery.html in wwwroot.

  • Add the following code to it:

    HTML
    <html>
    <head>
        <title>GraphQL</title>
    </head>
    <body>
        <pre><code class="language-json" id="code"></code></pre>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.14.9/beautify.min.js"></script>
        
    </body>
    </html>
  • Edit the Program.cs code:
    C#
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);
            var app = builder.Build();
            
            app.UseDefaultFiles();
            
            app.UseStaticFiles();
            
            app.Run(async (context) =>
            {
                await context.Response.WriteAsync
                      ("Request Handled and Response Generated");
            });
            
            app.Run();
        }
    }

Important

Please ensure you set up multiple projects at startup.

Image 3

Also, make sure to authorize CORS by amending the host.json file:

JSON
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet"
  },
  "Host": {
    "CORS": "*"
  }
}

Information

Our foundational code is rather straightforward, primarily comprising a request to retrieve either a list of customers or a specific customer. While seemingly unremarkable, this basic setup serves as a suitable starting point to demonstrate more advanced concepts in action.

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.

SDL
type Customer {
   id: ID!
   name: String
   age: Int
}

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:

SDL
type Order {
   id: ID!
   reference: String!
   amount: Float
   customerId: ID!
}

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:

SDL
type Customer {
   id: ID!
   name: String
   age: Int
   orders: [Order]
}

type Order {
   id: ID!
   reference: String!
   amount: Float
   customer: Customer
}

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

  1. Query (for data retrieval)
  2. Mutation (for data modification), and
  3. 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.

SDL
type Query {
   customers: Customer
   customer(id:ID!): Customer
}

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.

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.

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

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

JSON
{
  "data": {
    "customer": {
      "id": "123f0",
      "age": "45",
	  "orders": []
    }
  }
}

Information

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

JSON
{
  "errors": [
    {
      "message": "<...>",
      "locations": [
        {
          "line": 2,
          "column": 1
        }
      ]
    }
  ]
}

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.

Image 5

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.

C#
public class MockCustomerRepository : ICustomerRepository
{
    public List<Customer> GetAllCustomers()
    {
        return new List<Customer>()
        {
            new Customer(){ Id = "0001", Name = "Bruce Smith", Age = 45 },
            new Customer(){ Id = "0010", Name = "Melissa Price", Age = 52 }
        };
    }
    
    public Customer GetCustomerById(string id)
    {
        return new Customer(){ Id = "0001", Name = "Bruce Smith", Age = 45 };
    }
}

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:

C#
public class Customer
{
    public string Id { get; set; }

    public string Name { get; set; }

    public int Age { get; set; }
    
    public List<Order> Orders { get; set; }
}

public class Order
{
    public string Id { get; set; }

    public string Reference { get; set; }

    public decimal Amount { get; set; }
    
    public Customer Customer { get; set; }
}

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).

C#
public class Query
{
    public List<Customer> GetCustomers([Service] ICustomerRepository customerRepository)
    {
        return customerRepository.GetAllCustomers();
    }
    
    public Customer GetCustomerById
           ([Service] ICustomerRepository customerRepository, string id)
    {
        return customerRepository.GetCustomerById(id);
    }
}

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.

C#
public class StartUp : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        ConfigureServices(builder.Services);
    }

    private static void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<ICustomerRepository, MockCustomerRepository>();

        services.AddGraphQLFunction().AddQueryType<Query>();
    }
}

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:
    HTML
    <html>
    <head>
        <title>GraphQL</title>
    </head>
    <body>
        <pre><code class="language-json" id="code"></code></pre>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.14.9/beautify.min.js"></script>
        <script>
            (async function () {
                const data = JSON.stringify({
                    query: `query {
                              customerById(id:"0010") {
                                id
                                name
                                age
                              }
                            }`
                });
    
                const response = await fetch(
                    'http://localhost:7132/api/graphql',
                    {
                        method: 'post',
                        body: data,
                        headers: {
                            'Content-Type': 'application/json'
                        },
                    }
                );
    
                const json = await response.json();
                document.getElementById('code').innerHTML = js_beautify(
                    JSON.stringify(json.data)
                );
            })();
        </script>
    </body>
    </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.

    Image 6

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

    Image 7

This concludes our discussion about the Query type, but it's only the beginning of the journey. Next, we will delve into how data can be modified using GraphQL. But to avoid overloading this article, readers interested in this implementation can find the continuation here.

History

  • 7th March, 2024: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Chief Technology Officer
France France
Nicolas is a software engineer who has worked across various types of companies and eventually found success in creating his startup. He is passionate about delving into the intricacies of software development, thoroughly enjoying solving challenging problems.


Comments and Discussions

 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA8-Mar-24 17:08
professionalȘtefan-Mihai MOGA8-Mar-24 17:08 
QuestionMy vote of 4 Pin
Bohdan Stupak7-Mar-24 6:00
professionalBohdan Stupak7-Mar-24 6:00 
AnswerRe: My vote of 4 Pin
Nicolas DESCARTES8-Mar-24 0:24
mvaNicolas DESCARTES8-Mar-24 0:24 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.