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.
-
Create a new class named Customer.cs and add the following code to it:
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:
public interface ICustomerRepository
{
List<Customer> GetAllCustomers();
Customer GetCustomerById(string id);
}
- Create a new class named
MockCustomerRepository
that implements the ICustomerRepository
interface.
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:
-
Create a class named CustomerService.cs (or a more general name) and add the following code to it:
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.
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>
<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:
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.
Also, make sure to authorize CORS by amending the host.json file:
{
"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.
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:
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:
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 ]
.
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.
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.
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.
POST http:
{
"query": "query customer (id: "123f0") { id, age, orders { reference } }",
}
According to the GraphQL specification, the response will consist of a formatted JSON payload.
{
"data": {
"customer": {
"id": "123f0",
"age": "45",
"orders": []
}
}
}
Information
In case the response includes an error, GraphQL will provide a corresponding formatted message.
{
"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.
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
.
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:
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).
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.
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>
<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.
It's evident that the mechanisms are functioning correctly, and the appropriate resolver is being invoked.
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