HotChocolate: Schema stitching for distributed services in .Net core 6
This article will be the first part of a series where we will be exploring how to create a distributed system using HotChocolate platform. In this part, we will see how to connect different microservices together using schema stitching.
we will build a distributed system using graphQL API and primarily focus on querying data from different services. Graphql provides an efficient way to write queries as it solves over-fetching and under-fetching problems (i.e) it will help us to fetch the exact data required by the client, nothing more nothing less.
Let us look at the architecture of the distributed system that we are about to build.
So we need to create three microservices that will be connected through the gateway service.
- Customer service: This will have details related to the customer
- Order service: This will have details of the orders placed and who placed them.
- Product service: This contains product-related information.
Connecting the microservice can be achieved through schema stitching (i.e) Schemas of the individual microservices will be published to the gateway service where they will be combined together to form a single schema.
Gateway service will serve as the entry point for all the requests. Once a request is received, then respective downstream services will be called as per the query. For instance, Let us construct a scenario where the client wants to fetch all the orders placed by a particular customer along with the customer details.
The query for the Above scenario will look like this.
{
customerByName(name: “Ava”) {
name
isSuperMember
orders {
orderNumber
productNames
}
}
}
Once the gateway receives a request, It will pass the request to customer service and fetch the necessary details. Then gateway will pass the request to the order service to fetch order details. Once all the details are fetched the gateway will return the fetched data as a response to the client.
Creating Customer service:
- Create a .Net core 6 web API project named “Customer_API” using visual studio.
- Create a class CustomerType
namespace Customer_API
{
public class CustomerType
{
public CustomerType(string name, int age, bool isSuperMember)
{
Name = name;
Age = age;
IsSuperMember = isSuperMember;
}
public string Name { get; set; }
public int Age { get; set; }
public bool IsSuperMember { get; set; }
}
}
3. Create a class CustomerRepository and implement a method to fetch customers by name.
namespace Customer_API
{
public class CustomerRepository
{
private readonly Dictionary<string, CustomerType> _customer;
public CustomerRepository()
{
_customer = new CustomerType[]
{
new CustomerType("John", 18, true),
new CustomerType("Jeeva", 22, false),
new CustomerType("Ava", 20, false)
}.ToDictionary(t => t.Name);
}
public CustomerType GetCustomerBy(string name) => _customer[name];
}
}
4. Install HotChocolate.AspNetCore package.
5. Create a class Query to implement the GraphQL API which will fetch the customer by its name.
namespace Customer_API
{
public class Query
{
public CustomerType GetCustomerByName([Service] CustomerRepository customerRepository, string name)
{
return customerRepository.GetCustomerBy(name);
}
}
}
6. Add the following configuration to Program.cs file.
builder.Services.AddSingleton<CustomerRepository>()
.AddGraphQLServer()
.AddQueryType<Query>();app.MapGraphQL("/customer-api/graphql");
Now let’s run the API and test it. You should get the result as shown below.
Creating Order Service:
- Create a .Net core 6 web API project named “Order_API” using visual studio.
- Create a class OrderType
namespace Order_API
{
public class OrderType
{
public OrderType(long orderNumber, DateTime orderDate, double totalAmount, string? customerName, List<ProductName> productNames)
{
OrderNumber = orderNumber;
OrderDate = orderDate;
TotalAmount = totalAmount;
CustomerName = customerName;
ProductNames = productNames;
}
public long OrderNumber { get; set; }
public DateTime OrderDate { get; set; }
public double TotalAmount { get; set; }
public String? CustomerName { get; set; }
public List<ProductName>? ProductNames { get; set; }
}
public class ProductName {
public ProductName(string name)
{
Name = name;
}
public string? Name { get; set; }
}
}
3. Create a class OrderRepository and implement a method to fetch orders.
namespace Order_API
{
public class OrderRepository
{
private readonly List<OrderType> _order;
public OrderRepository()
{
_order = new OrderType[]
{
new OrderType(1,Convert.ToDateTime("2022-01-01"), 1000,"Ava",new List<ProductName>(){ new ProductName("Watch"),new ProductName("Laptop"),new ProductName("Mobile") }),
new OrderType(2,Convert.ToDateTime("2022-01-01"), 150,"Jeeva",new List<ProductName>(){ new ProductName("Mobile") }),
new OrderType(3,Convert.ToDateTime("2022-02-01"), 50,"Ava",new List<ProductName>(){ new ProductName("Perfume") }),
new OrderType(4,Convert.ToDateTime("2022-03-01"), 1100,"Ava",new List<ProductName>(){ new ProductName("Sheo"),new ProductName("Shirt") }),
new OrderType(5,Convert.ToDateTime("2022-03-01"), 200,"Ava",new List<ProductName>(){ new ProductName("Jean") })
}.ToList();
}
public List<OrderType> GetOrder() => _order;
}
}
4. Install HotChocolate.AspNetCore package.
5. Create a class Query to implement the GraphQL API to fetch orders.
namespace Order_API
{
public class Query
{
public List<OrderType> GetOrder([Service] OrderRepository orderRepository)
{
return orderRepository.GetOrder();
}
}
}
6. Add the following configuration to Program.cs file.
builder.Services
.AddSingleton<OrderRepository>()
.AddGraphQLServer()
.AddQueryType<Query>();
app.MapGraphQL("/order-api/graphql");
Let's query and test the API.
Creating Product service:
- Create a .Net core 6 web API project named “Product_API” using visual studio.
- Create a class ProductType
namespace Product_API
{
public class ProductType
{
public ProductType(long serialNumber, string? name, Category category)
{
SerialNumber = serialNumber;
Name = name;
Category = category;
}
public long SerialNumber { get; set; }
public String? Name { get; set; }
public Category Category { get; set; }
}
public enum Category{
FASHION,
TECH
}
}
3. Create a class ProductRepository and implement a method to fetch Products.
namespace Product_API
{
public class ProductRepository
{
private readonly List<ProductType> _product;
public ProductRepository()
{
_product = new ProductType[]
{
new ProductType(001,"Jean",Category.FASHION),
new ProductType(002,"Mobile",Category.TECH),
new ProductType(003,"Watch",Category.FASHION),
new ProductType(004,"Sheo",Category.FASHION),
new ProductType(005,"Shirt",Category.FASHION),
new ProductType(006,"Laptop",Category.TECH),
new ProductType(007,"Perfume",Category.FASHION)
}.ToList();
}
public List<ProductType> GetProduct() => _product;
}
}
4. Install HotChocolate.AspNetCore package.
5. Create a class Query to implement the GraphQL API to fetch Products.
namespace Product_API
{
public class Query
{
public List<ProductType> GetProduct([Service] ProductRepository productRepository)
{
return productRepository.GetProduct();
}
}
}
6. Add the following configuration to Program.cs file.
builder.Services
.AddSingleton<ProductRepository>()
.AddGraphQLServer()
.AddQueryType<Query>();
app.MapGraphQL("/product-api/graphql");
Let's query and test the API.
Hot chocolate provides three approaches for implementing schema stitching.
Stitching centralized approach: In this approach, the extension file that is used for stitching will be present in the gateway service. So when the number of services increases then there will be too much code written in one single file. Thus maintenance would become an overhead.
Stitching Federated with schema polling: In this approach, the extension file that is used for stitching will be present in the respective downstream services. Schema of the individual services will be published directly to the gateway so when there is a change in the schema for any one of the downstream services, then the gateway also has to be restarted to reflect the change in the schema.
Stitching Federated with Redis: Similar to schema polling, the extension files are present in respective downstream services. But in this approach, the schema of individual services will be published to a Redis instance and the gateway will be subscribed to the Redis instance. So any changes in the schema will be reflected in the gateway without restarting it.
Each approach has its own pros and cons. And there is no clear winner here. In this article, we will use Federated with Redis to implement schema stitching.
Create and configure gateway service
- Create a .Net core 6 web API project named “Gateway_API” using visual studio.
- Install Redis and start the Redis service. By default, Redis runs on port 6379.
- Install HotChocolate.Stitching.Redis and HotChocolate.AspNetCore package.
Configure downstream services:
- Install HotChocolate.Stitching.Redis in customer_API,order_API, and product_API.
- Configure and publish schema to Redis instance.
For customer API
builder.Services .AddSingleton(ConnectionMultiplexer.Connect("localhost,abortConnect=false"))
.AddSingleton<CustomerRepository>()
.AddGraphQLServer()
.AddQueryType<Query>()
.PublishSchemaDefinition(c => c
.SetName("customer")
.PublishToRedis( "gateway", sp => sp.GetRequiredService<ConnectionMultiplexer>()));
For order API
builder.Services
.AddSingleton(ConnectionMultiplexer.Connect("localhost,abortConnect=false"))
.AddSingleton<OrderRepository>()
.AddGraphQLServer()
.AddQueryType<Query>()
.PublishSchemaDefinition(c => c
.SetName("order")
.PublishToRedis("gateway", sp => sp.GetRequiredService<ConnectionMultiplexer>()));
For product API
builder.Services
.AddSingleton(ConnectionMultiplexer.Connect("localhost,abortConnect=false"))
.AddSingleton<ProductRepository>()
.AddGraphQLServer()
.AddQueryType<Query>()
.PublishSchemaDefinition(c => c
.SetName("product")
.PublishToRedis("gateway", sp => sp.GetRequiredService<ConnectionMultiplexer>()));
Configure gateway service:
- Install HotChocolate.Stitching.Redis in Gateway_API.
- Configure and subscribe to Redis instance.
For gateway:
Routing to the individual downstream services is taken care by hotchocolate itself. We just need to establish a connection between the schema and downstream service URI.
var Customer = "customer";
var Order = "order";
var Product = "product";
builder.Services.AddHttpClient(Customer, c => c.BaseAddress = new Uri("https://localhost:7190/customer-api/graphql/"));
builder.Services.AddHttpClient(Order, c => c.BaseAddress = new Uri("https://localhost:7236/order-api/graphql/"));
builder.Services.AddHttpClient(Product, c => c.BaseAddress = new Uri("https://localhost:7046/product-api/graphql/"));
builder.Services.AddSingleton(ConnectionMultiplexer.Connect("localhost,abortConnect=false"))
.AddGraphQLServer()
.ConfigureSchema(sb => sb.ModifyOptions(opts => opts.StrictValidation = false))
.AddRemoteSchemasFromRedis("gateway", sp => sp.GetRequiredService<ConnectionMultiplexer>());
Now all the downstream schemas are available in the gateway and you will be able to query them individually. But this does not serve our purpose. We aim to establish communication between the microservices so that we can get all information from all the microservices using a single query.
Let us establish a use case to understand the problem that we are trying to solve.
Use case: Get the list of product categories ordered by a Customer.
Let us see how can this be achieved in form of a query:
{
customerByName(name: "Ava") {
orders {
productNames{
productInfo {
category
}
}
}
}
}
From the query, we can understand that CustomerType should be extended to have a field “orders” to get the orders placed by the customer. This can be done with the help of customerName property. And productName should be extended to have “productInfo” to get the product information. This can be done using the productName property.
Before establishing the connections we need to add two resolvers
- Resolver to fetch order by customerName in order_API
- Resolver to fetch product by productname in product_API
For product_API
Resolver method in Query.cs:
public List<ProductType> GetProductBy([Service] ProductRepository productRepository,String name)
{
return productRepository.GetProductBy(name);
}
Repository method in productRepositry.cs:
public List<ProductType> GetProductBy(String name) => _product.Where(x => x.Name == name).ToList();
For order_API
Resolver method in Query.cs:
public List<OrderType> GetOrderBy([Service] OrderRepository orderRepository,string name)
{
return orderRepository.GetOrderBy(name);
}
Repository method in orderRepository.cs:
public List<OrderType> GetOrderBy(string name) => _order.Where(x => x.CustomerName == name).ToList();
we have added the required resolvers. Let us add the extension file to establish a connection between different services.
- Create Stitching.graphql file in Order_API to extend order to CustomerType.
extend type CustomerType{
orders: [OrderType]
@delegate(path: "orderBy(name: $fields:name)")
}
Add configuration .AddTypeExtensionsFromFile(“./Stitching.graphql”) to program.cs file.
.PublishSchemaDefinition(c => c
.SetName("order")
.AddTypeExtensionsFromFile("./Stitching.graphql")
.PublishToRedis("gateway", sp => sp.GetRequiredService<ConnectionMultiplexer>()));
2. Create Stitching.graphql file in Product_API to extend productInfoto ProductList.
extend type ProductName{
productInfo: ProductType
@delegate(path: "productBy(name: $fields:name)")
}
Add configuration .AddTypeExtensionsFromFile(“./Stitching.graphql”) to program.cs file.
.PublishSchemaDefinition(c => c
.SetName("product")
.AddTypeExtensionsFromFile("./Stitching.graphql")
.PublishToRedis("gateway", sp => sp.GetRequiredService<ConnectionMultiplexer>()));
Let's test out the stitching now.
Using HotChocolate platform we have built a distributed system and implemented schema stitching to communicate with different services. In the next part, we will see how to implement paging, sorting, and filtering for the distributed system using HotChocolate platform.
Please visit the link for the source code.