Back to: Microservices using ASP.NET Core Web API Tutorials
Implementing GraphQL in ASP.NET Core using Hot Chocolate
In a microservices-based system, data is usually spread across multiple services, such as Order, Product, User, and Payment. Frontend screens often need combined data from these services in a single response. GraphQL helps solve this problem by serving as a flexible layer that fetches and combines data from multiple microservices.
We will implement GraphQL in the API Gateway as a BFF (Backend for Frontend), so the UI can retrieve all required data through a single/graphql endpoint in a clean, flexible way. We will use the HotChocolate library to implement GraphQL in an ASP.NET Core Web API.
Scenario: Order Details Screen + Place Order
In real-world applications, not all screens need the same data. Even when they deal with the same business concept (such as an order), different screens require different data shapes. This is one of the main reasons for introducing GraphQL.
Read Operation (Query): Order Details Screen
An Order Details screen typically needs to show a complete view of an order. However, the exact data required can vary from screen to screen.
For example:
- A Customer Order Details Page may need basic order information, products, and payment status.
- An Admin Order Screen may need additional fields, such as internal status, audit information, or warnings.
- A Mobile Screen may need only a minimal summary.
All these screens deal with the same order, but each screen requires a different data format. With GraphQL, the frontend can request only the fields required for that specific screen in a single query. Internally, GraphQL can fetch data from the Order, User, Payment, and Product services and return a response tailored exactly to that UI.
If all clients needed the same fixed-order summary, our existing API Gateway aggregation endpoint would already be sufficient. GraphQL becomes valuable only when different screens need different data structures from the same backend.
Write Operation (Mutation): Place Order
Placing an order is a Write Operation, and it follows a similar principle.
The client sends:
- Product Items
- User ID
- Selected payment method
This data is sent as a GraphQL mutation. The GraphQL layer simply forwards the request to the existing Order service that creates the order. No new business logic is added at the GraphQL level.
This keeps responsibilities clear:
- Microservices handle business rules
- GraphQL handles request shaping and orchestration
Where Does GraphQL Fit in Our Microservices Architecture?
GraphQL must be implemented in the API Gateway Layer Project because this layer serves as the Single Entry Point between frontend applications and backend microservices. Different UI screens often require different data shapes, and GraphQL helps meet these requirements by aggregating data from multiple microservices and shaping the response based on frontend needs. By placing GraphQL in the API Gateway, we keep microservices focused on business logic while enabling flexible, efficient data fetching for the UI.
Packages to Install
We will use stable Hot Chocolate packages (NuGet shows 15.1.12 as stable for both) to implement GraphQL in ASP.NET Core, which Microsoft also recommends. We need to install the following Packages in the API Gateway project. Open the Package Manager Console and run the following command.
- Install-Package HotChocolate.AspNetCore -Version 15.1.12
- Install-Package HotChocolate.AspNetCore.Authorization -Version 15.1.12
- Install-Package HotChocolate.AspNetCore -Version 15.1.12
Notes:
- HotChocolate.AspNetCore includes the GraphQL middleware and IDE support.
- For authorization, Hot Chocolate uses its own authorization integration.
Step-by-step Implementation:
First, create a folder named GraphQL at the project root directory. Then, inside the GraphQL folder, please add the following subfolders.
- GraphQL
-
- Schema
- Input
-
- Models
- Types
- Validators
-
- Output
-
- Models
- Types
-
- Queries
- Mutations
- Services (Already Exists)
-
Creating Input Models (DTOs or Models)
Input models represent the data the client sends to GraphQL when performing operations, especially mutations. They describe the structure of the request data required to create or update information.
GraphQL/Input/Models/OrderItemInputModel.cs
Create a class file named OrderItemInputModel.cs within the GraphQL/Input/Models folder, and then copy-paste the following code. This class represents a single product item that the user wants to order. It is sent by the frontend when placing an order and contains only essential item data, such as the product ID and quantity.
using System.ComponentModel.DataAnnotations;
namespace APIGateway.GraphQL.Input.Models
{
// Represents a single cart/order item coming from the client.
// This is a plain C# input model (DTO) used by our GraphQL mutations.
// It is similar to a DTO used in REST APIs.
public class OrderItemInputModel
{
// ProductId is mandatory.
// If the client does not send this value,
// GraphQL validation will fail before execution.
[Required(ErrorMessage = "ProductId is required.")]
public Guid ProductId { get; set; }
// Quantity must be at least 1.
// This prevents invalid orders such as 0 or negative quantities.
// Validation happens before calling backend services.
[Range(1, int.MaxValue, ErrorMessage = "Quantity must be at least 1.")]
public int Quantity { get; set; }
}
}
GraphQL/Input/Models/AddressInputModel.cs
Create a class file named AddressInputModel.cs within the GraphQL/Input/Models folder, and then copy-paste the following code. This class represents an address sent by the client, such as a shipping or billing address. It supports both new address entry and existing saved addresses, making the order flow flexible.
using System.ComponentModel.DataAnnotations;
namespace APIGateway.GraphQL.Input.Models
{
// Represents an address object coming from the client in a GraphQL request.
// This is a pure C# input model (DTO) used by GraphQL mutations/inputs.
public class AddressInputModel
{
// Optional Id.
// - If Id is provided, it usually means "update existing address".
// - If Id is null, it usually means "create a new address".
public Guid? Id { get; set; }
// Address line 1 is mandatory (example: house no, street, area).
// [Required] ensures client must send a value.
// [MaxLength] prevents very large strings that could cause DB/storage issues.
[Required(ErrorMessage = "AddressLine1 is required.")]
[MaxLength(200, ErrorMessage = "AddressLine1 must be <= 200 characters.")]
public string AddressLine1 { get; set; } = null!;
// Address line 2 is optional (landmark, apartment, etc.).
// MaxLength still applies to prevent overly long input.
[MaxLength(200, ErrorMessage = "AddressLine2 must be <= 200 characters.")]
public string? AddressLine2 { get; set; }
// City is mandatory.
// [MaxLength] keeps the input clean and safe (also helps DB consistency).
[Required(ErrorMessage = "City is required.")]
[MaxLength(100, ErrorMessage = "City must be <= 100 characters.")]
public string City { get; set; } = null!;
// State is mandatory.
[Required(ErrorMessage = "State is required.")]
[MaxLength(100, ErrorMessage = "State must be <= 100 characters.")]
public string State { get; set; } = null!;
// PostalCode is mandatory.
// MaxLength is limited because postal codes are typically short (PIN/ZIP).
[Required(ErrorMessage = "PostalCode is required.")]
[MaxLength(20, ErrorMessage = "PostalCode must be <= 20 characters.")]
public string PostalCode { get; set; } = null!;
// Country is mandatory.
[Required(ErrorMessage = "Country is required.")]
[MaxLength(100, ErrorMessage = "Country must be <= 100 characters.")]
public string Country { get; set; } = null!;
// If true, this address should be treated as the customer's default billing address.
public bool IsDefaultBilling { get; set; }
// If true, this address should be treated as the customer's default shipping address.
public bool IsDefaultShipping { get; set; }
}
}
GraphQL/Input/Models/PlaceOrderInputModel.cs
Create a class file named PlaceOrderInputModel.cs within the GraphQL/Input/Models folder, and then copy-paste the following code. This is the main input model for placing an order. It combines user details, order items, addresses, and payment methods into one structured request, allowing the client to place an order in a single GraphQL mutation.
using OrderService.Contracts.Enums;
using System.ComponentModel.DataAnnotations;
namespace APIGateway.GraphQL.Input.Models
{
// Represents the input payload sent by the client when placing an order via GraphQL.
// - The client sends cart items + user info + address info + payment method in ONE request.
// - The gateway validates the input and then forwards/orchestrates the actual order creation.
public class PlaceOrderInputModel
{
// The user placing the order.
// Required because an order must always belong to a user/customer.
[Required(ErrorMessage = "UserId is required.")]
public Guid UserId { get; set; }
// The list of items the user is ordering (cart items).
// [Required] + [MinLength(1)] ensures the client cannot place an order with an empty cart.
// This prevents unnecessary calls to backend services.
[Required(ErrorMessage = "At least one order item is required.")]
[MinLength(1, ErrorMessage = "At least one order item is required.")]
public List<OrderItemInputModel> Items { get; set; } = new();
// Optional reference to an already saved shipping address.
// If this is provided, the client is saying: "Use my existing saved address".
public Guid? ShippingAddressId { get; set; }
// Optional full shipping address object.
// If this is provided, the client is saying: "Use this new shipping address right now".
// In many real projects, UI sends either ShippingAddressId OR ShippingAddress.
// This model allows both, and business logic can enforce the rule if needed.
public AddressInputModel? ShippingAddress { get; set; }
// Optional reference to an already saved billing address.
// Similar usage as ShippingAddressId.
public Guid? BillingAddressId { get; set; }
// Optional full billing address object.
// Similar usage as ShippingAddress.
public AddressInputModel? BillingAddress { get; set; }
// Payment method chosen by the client (e.g., COD, Card, UPI, NetBanking, etc.).
// Required because we must know how the customer is paying before placing the order.
[Required(ErrorMessage = "PaymentMethod is required.")]
public PaymentMethodEnum PaymentMethod { get; set; }
}
}
Creating Input Types for GraphQL Input Type Schema
Input Types are the GraphQL Schema Representation of input data. While Input Models are our C# classes, Input Types are the input objects exposed in GraphQL’s schema. GraphQL treats inputs differently from outputs: inputs must be Input Types, which ensures the schema stays strict and predictable.
GraphQL/Input/Types/OrderItemInputType.cs
Create a class file named OrderItemInputType.cs within the GraphQL/Input/Types folder, and then copy-paste the following code. This class defines how OrderItemInputModel appears in the GraphQL schema. It controls which fields are required and how the frontend can send item data.
using APIGateway.GraphQL.Input.Models;
namespace APIGateway.GraphQL.Input.Types
{
// GraphQL Input Type definition for OrderItemInputModel.
// GraphQL does not directly use C# models,
// so we map the model to a GraphQL Input Type.
// Important:
// - InputModel = C# representation (used in .NET code)
// - InputType = GraphQL schema representation (what the client sees/uses)
//
// This class tells Hot Chocolate how to expose the input model in the GraphQL schema.
public class OrderItemInputType : InputObjectType<OrderItemInputModel>
{
// Configure the GraphQL schema for this input object.
// The descriptor allows us to:
// - Set the input type name shown to clients
// - Control field types (UUID, Int, NonNull)
// - Add constraints and schema-level rules
protected override void Configure(IInputObjectTypeDescriptor<OrderItemInputModel> descriptor)
{
// The name that will appear in the GraphQL schema.
// Clients will use this name inside mutations like:
// items: [OrderItemInput!]!
descriptor.Name("OrderItemInput");
// Map ProductId property to a GraphQL field named "productId".
// NonNullType means the client MUST provide this field.
// UuidType ensures GraphQL treats it as a UUID/GUID value.
descriptor.Field(x => x.ProductId)
.Type<NonNullType<UuidType>>();
// Map Quantity property to a GraphQL field named "quantity".
// NonNullType forces client to send the quantity.
// IntType ensures it is treated as an integer at schema level.
// Note: Range validation (>=1) is still handled by DataAnnotations validation.
descriptor.Field(x => x.Quantity)
.Type<NonNullType<IntType>>();
}
}
}
GraphQL/Input/Types/AddressInputType.cs
Create a class file named AddressInputType.cs within the GraphQL/Input/Types folder, and then copy-paste the following code. This class exposes the address structure to GraphQL clients. It clearly defines which address fields are mandatory and ensures invalid address data is rejected before execution.
using APIGateway.GraphQL.Input.Models;
namespace APIGateway.GraphQL.Input.Types
{
// GraphQL Input Type definition for AddressInputModel.
// Important distinction:
// - AddressInputModel = C# class used inside .NET code (DTO)
// - AddressInputType = GraphQL schema definition exposed to clients
// This class controls:
// - The GraphQL input object name
// - Which fields are exposed
// - Which fields are required (NonNull)
// - The exact GraphQL scalar types (String, UUID, Boolean, etc.)
public class AddressInputType : InputObjectType<AddressInputModel>
{
// Configure how this input appears in the GraphQL schema.
// The descriptor lets us customize input name and field types.
protected override void Configure(IInputObjectTypeDescriptor<AddressInputModel> descriptor)
{
// The name of the input object in GraphQL schema.
// Client will use it like:
// address: AddressInput!
descriptor.Name("AddressInput");
// Id is optional, so we don't wrap it with NonNullType<>.
// This allows the client to omit it for "create new address".
descriptor.Field(x => x.Id)
.Type<UuidType>();
// AddressLine1 is required for the address to be meaningful,
// so we mark it NonNull in the GraphQL schema.
// If missing, GraphQL rejects the request immediately.
descriptor.Field(x => x.AddressLine1)
.Type<NonNullType<StringType>>();
// AddressLine2 is optional, so we keep it nullable in schema.
descriptor.Field(x => x.AddressLine2)
.Type<StringType>();
// City/State/PostalCode/Country are required, so use NonNullType<StringType>.
descriptor.Field(x => x.City)
.Type<NonNullType<StringType>>();
descriptor.Field(x => x.State)
.Type<NonNullType<StringType>>();
descriptor.Field(x => x.PostalCode)
.Type<NonNullType<StringType>>();
descriptor.Field(x => x.Country)
.Type<NonNullType<StringType>>();
// These flags are booleans. We mark them NonNull so client always sends true/false.
// This avoids ambiguity and makes the contract explicit.
descriptor.Field(x => x.IsDefaultBilling)
.Type<NonNullType<BooleanType>>();
descriptor.Field(x => x.IsDefaultShipping)
.Type<NonNullType<BooleanType>>();
}
}
}
GraphQL/Input/Types/PlaceOrderInputType.cs
Create a class file named PlaceOrderInputType.cs within the GraphQL/Input/Types folder, and then copy-paste the following code. This class represents the GraphQL input object for placing an order. It enforces schema-level rules, such as required fields, nested inputs, and the use of enums for payment methods.
using APIGateway.GraphQL.Input.Models;
using OrderService.Contracts.Enums;
namespace APIGateway.GraphQL.Input.Types
{
// GraphQL Input Type definition for PlaceOrderInputModel.
// - PlaceOrderInputModel is a C# DTO used internally.
// - PlaceOrderInputType defines how that DTO appears in the GraphQL schema.
// This helps us control:
// - Input object name in schema
// - Which fields are required (NonNull)
// - How nested inputs (items, addresses) are represented
// - How enums are exposed to clients
public class PlaceOrderInputType : InputObjectType<PlaceOrderInputModel>
{
// Configures the GraphQL schema details for the "PlaceOrderInput" object.
// The descriptor allows us to map each C# property to a GraphQL field and type.
protected override void Configure(IInputObjectTypeDescriptor<PlaceOrderInputModel> descriptor)
{
// Name of the input object shown in GraphQL schema.
descriptor.Name("PlaceOrderInput");
// UserId is mandatory, so we mark it as NonNull UUID in GraphQL schema.
// If the client does not send it, GraphQL rejects the request immediately.
descriptor.Field(x => x.UserId)
.Type<NonNullType<UuidType>>();
// Items is mandatory and must contain at least one item.
// GraphQL-level: NonNull list + NonNull items inside it.
// That means:
// - Items list itself cannot be null
// - Each item inside list cannot be null
descriptor.Field(x => x.Items)
.Type<NonNullType<ListType<NonNullType<OrderItemInputType>>>>();
// ShippingAddressId is optional (nullable),
// so we expose it as nullable UUID type in schema.
descriptor.Field(x => x.ShippingAddressId)
.Type<UuidType>();
// ShippingAddress is optional (nullable) because user may choose an existing saved address
// via ShippingAddressId. If UI provides a new address, it sends this object.
descriptor.Field(x => x.ShippingAddress)
.Type<AddressInputType>();
// BillingAddressId is optional for the same reason as ShippingAddressId.
descriptor.Field(x => x.BillingAddressId)
.Type<UuidType>();
// BillingAddress is optional. UI can send it only when a new billing address is needed.
descriptor.Field(x => x.BillingAddress)
.Type<AddressInputType>();
// PaymentMethod is required, so we mark it NonNull.
// EnumType<PaymentMethodEnum> tells Hot Chocolate to expose this C# enum as a GraphQL enum.
// Clients will see a fixed set of allowed values in the schema.
descriptor.Field(x => x.PaymentMethod)
.Type<NonNullType<EnumType<PaymentMethodEnum>>>();
// Alternative (less explicit) approach:
// descriptor.Field(x => x.PaymentMethod);
// Hot Chocolate can infer enum type automatically, but we keep it explicit for clarity.
}
}
}
Creating Output Models (DTOs or Models)
Output Models are the C# classes we return to the client after a Query or Mutation completes. They represent the response structure our application wants to produce—like OrderSummaryDto, PlaceOrderResultDto, or PaymentStatusDto. They’re designed for UI use, so they usually contain only the fields the client needs, not all the fields from database entities.
GraphQL/Output/Models/UserError.cs
Create a class file named UserError.cs within the GraphQL/Output/Models folder, and then copy-paste the following code. This class represents a user-friendly error returned by GraphQL mutations. Instead of throwing exceptions, GraphQL returns clear error messages that the UI can display directly.
namespace APIGateway.GraphQL.Output.Models
{
// Represents a user-friendly error that can be returned to the client
// as part of a GraphQL mutation response.
// Example usage:
// - Code: "VALIDATION_ERROR", "NOT_FOUND", "PAYMENT_FAILED", "OUT_OF_STOCK"
// - Message: A clean message to show on UI.
public class UserError
{
// A short machine-readable error code.
// The frontend can use this code to decide what to show or what action to take
// (e.g., highlight field, show toast, redirect).
// Default value is VALIDATION_ERROR for generic invalid inputs.
public string Code { get; set; } = "VALIDATION_ERROR";
// A human-readable message that can be shown directly on UI.
// Keep it simple, safe, and user-friendly (avoid technical details).
public string Message { get; set; } = "Invalid request.";
}
}
GraphQL/Output/Models/PlaceOrderPayload.cs
Create a class file named PlaceOrderPayload.cs within the GraphQL/Output/Models folder, and then copy-paste the following code. This class represents the result of the place order mutation. It always returns a predictable response containing either the created order or a list of user-friendly errors.
namespace APIGateway.GraphQL.Output.Models
{
// Represents a user-friendly error that can be returned to the client
// as part of a GraphQL mutation response.
// Example usage:
// - Code: "VALIDATION_ERROR", "NOT_FOUND", "PAYMENT_FAILED", "OUT_OF_STOCK"
// - Message: A clean message to show on UI.
public class UserError
{
// A short machine-readable error code.
// The frontend can use this code to decide what to show or what action to take
// (e.g., highlight field, show toast, redirect).
// Default value is VALIDATION_ERROR for generic invalid inputs.
public string Code { get; set; } = "VALIDATION_ERROR";
// A human-readable message that can be shown directly on UI.
// Keep it simple, safe, and user-friendly (avoid technical details).
public string Message { get; set; } = "Invalid request.";
}
}
Output Types for GraphQL Output Schema
Output Types are the GraphQL Schema Types that clients can query and select fields from. While Output Models are C# classes in our project, Output Types are the type definitions GraphQL exposes in the schema. These types define which fields the client is allowed to request, and GraphQL uses them to enforce a strongly typed contract for responses.
GraphQL/Output/Types/UserErrorType.cs
Create a class file named UserErrorType.cs within the GraphQL/Output/Types folder, and then copy-paste the following code. This class defines how UserError is exposed in the GraphQL schema. It ensures every error returned to the client contains a clear code and message.
using APIGateway.GraphQL.Output.Models;
namespace APIGateway.GraphQL.Output.Types
{
// GraphQL output type mapping for the UserError model.
// - UserError is a C# model used in our code.
// - UserErrorType defines how that model appears in the GraphQL schema.
// This allows us to control:
// - The GraphQL type name ("UserError")
// - Field nullability (NonNull fields)
// - Field types (String, etc.)
public class UserErrorType : ObjectType<UserError>
{
// Configure the schema representation of UserError.
// GraphQL schema will expose:
protected override void Configure(IObjectTypeDescriptor<UserError> descriptor)
{
// The name shown in GraphQL schema.
// The client will see it as "UserError".
descriptor.Name("UserError");
// Code is required in the response because the UI can use it for logic:
// e.g., show specific message, highlight a field, or display a toast.
descriptor.Field(x => x.Code)
.Type<NonNullType<StringType>>();
// Message is required because it is typically displayed directly to the user.
// Keeping it NonNull ensures the client always receives a readable message.
descriptor.Field(x => x.Message)
.Type<NonNullType<StringType>>();
}
}
}
GraphQL/Output/Types/OrderItemOutputType.cs
Create a class file named OrderItemOutputType.cs within the GraphQL/Output/Types folder, and then copy-paste the following code. This class defines how individual order items are returned in GraphQL responses. It includes product details, pricing, quantity, and status for display on UI screens.
using OrderService.Application.DTOs.Order;
using OrderService.Domain.Enums;
namespace APIGateway.GraphQL.Output.Types
{
// GraphQL Output Type mapping for OrderItemResponseDTO.
// - OrderItemResponseDTO is a C# DTO coming from OrderService.
// - OrderItemOutputType defines how that DTO appears in the GraphQL schema.
public class OrderItemOutputType : ObjectType<OrderItemResponseDTO>
{
// The descriptor helps us control:
// - GraphQL type name ("OrderItem")
// - Field types (UUID, String, Int, Decimal, Enum)
// - Which fields are required (NonNull) vs optional (nullable)
protected override void Configure(IObjectTypeDescriptor<OrderItemResponseDTO> descriptor)
{
// This is the name clients will see in the GraphQL schema
descriptor.Name("OrderItem");
// Unique identifier for this order item (line item).
// Marked NonNull because the UI should always receive a valid identifier.
descriptor.Field(x => x.OrderItemId)
.Type<NonNullType<UuidType>>();
// Product identifier for the item.
// NonNull because each order item must be linked to a product.
descriptor.Field(x => x.ProductId)
.Type<NonNullType<UuidType>>();
// ProductName is typically displayed on the UI (order details page).
// NonNull ensures clients don't have to handle missing product names.
descriptor.Field(x => x.ProductName)
.Type<NonNullType<StringType>>();
// ItemStatusId represents the current status of the individual item (e.g., Placed/Shipped/Delivered).
// Exposed as a GraphQL enum so client gets a fixed list of allowed values.
// NonNull ensures UI always receives the status.
descriptor.Field(x => x.ItemStatusId)
.Type<NonNullType<EnumType<OrderStatusEnum>>>();
// Alternative (Hot Chocolate can infer enums automatically),
// but explicit mapping makes schema intention clearer:
// descriptor.Field(x => x.ItemStatusId);
// Price information at the time of purchase.
// NonNull because pricing is critical for invoice/order display.
descriptor.Field(x => x.PriceAtPurchase)
.Type<NonNullType<DecimalType>>();
// DiscountedPrice is included so UI can show MRP vs discounted pricing.
descriptor.Field(x => x.DiscountedPrice)
.Type<NonNullType<DecimalType>>();
// Quantity ordered.
descriptor.Field(x => x.Quantity)
.Type<NonNullType<IntType>>();
// TotalPrice = DiscountedPrice * Quantity (or final calculated price for this item).
// NonNull because totals are needed for summary calculations in UI.
descriptor.Field(x => x.TotalPrice)
.Type<NonNullType<DecimalType>>();
}
}
}
GraphQL/Output/Types/OrderOutputType.cs
Create a class file named OrderOutputType.cs within the GraphQL/Output/Types folder, and then copy-paste the following code. This class represents the complete order view exposed to clients. It is used after placing an order or when fetching order details, and clients can select only the fields they need.
using OrderService.Application.DTOs.Order;
using OrderService.Contracts.Enums;
using OrderService.Domain.Enums;
namespace APIGateway.GraphQL.Output.Types
{
// GraphQL Output Type mapping for OrderResponseDTO.
// - This type represents the complete "Order" view that the UI can query.
// - It is typically returned from:
// 1) Order summary queries
// 2) Place order mutation payload (PlaceOrderPayload.Order)
//
// GraphQL advantage:
// - Clients can select only required fields (e.g., orderId + status)
// - Or request deeper fields (e.g., items + pricing) based on screen needs.
public class OrderOutputType : ObjectType<OrderResponseDTO>
{
protected override void Configure(IObjectTypeDescriptor<OrderResponseDTO> descriptor)
{
// Name of the GraphQL type visible to clients
descriptor.Name("Order");
// Core identifiers
descriptor.Field(x => x.OrderId)
.Type<NonNullType<UuidType>>();
// OrderNumber is usually shown to customer/admin as a readable identifier.
descriptor.Field(x => x.OrderNumber)
.Type<NonNullType<StringType>>();
// The user who placed the order.
descriptor.Field(x => x.UserId)
.Type<NonNullType<UuidType>>();
// Order status + payment method
// These are enums, so GraphQL exposes them as enum values.
// Clients get a strict set of allowed values which improves contract clarity.
// Enums can be inferred automatically by Hot Chocolate,
// but explicit mapping is clearer in training docs and real projects.
descriptor.Field(x => x.OrderStatus)
.Type<NonNullType<EnumType<OrderStatusEnum>>>();
descriptor.Field(x => x.PaymentMethod)
.Type<NonNullType<EnumType<PaymentMethodEnum>>>();
// Date/time when the order was placed.
// NonNull ensures UI can always display order placed date.
descriptor.Field(x => x.OrderDate)
.Type<NonNullType<DateTimeType>>();
// Order items (line items).
// NonNull list + NonNull items means:
// - The list itself is never null (client can safely iterate)
// - Each item inside list is never null
descriptor.Field(x => x.Items)
.Type<NonNullType<ListType<NonNullType<OrderItemOutputType>>>>();
// Address references for shipping and billing.
// These are NonNull because order processing requires both address references.
descriptor.Field(x => x.ShippingAddressId)
.Type<NonNullType<UuidType>>();
descriptor.Field(x => x.BillingAddressId)
.Type<NonNullType<UuidType>>();
// Pricing summary fields
// These are NonNull because totals are essential for order summary/invoice display.
descriptor.Field(x => x.SubTotalAmount)
.Type<NonNullType<DecimalType>>();
descriptor.Field(x => x.DiscountAmount)
.Type<NonNullType<DecimalType>>();
descriptor.Field(x => x.ShippingCharges)
.Type<NonNullType<DecimalType>>();
descriptor.Field(x => x.TaxAmount)
.Type<NonNullType<DecimalType>>();
descriptor.Field(x => x.TotalAmount)
.Type<NonNullType<DecimalType>>();
// Payment URL is optional.
// Example: it may exist only for online payment methods (UPI/Card)
// and might be null for COD or when payment is already completed.
descriptor.Field(x => x.PaymentUrl)
.Type<StringType>();
}
}
}
GraphQL/Output/Types/PlaceOrderPayloadType.cs
Create a class file named PlaceOrderPayloadType.cs within the GraphQL/Output/Types folder, and then copy-paste the following code. This class defines the GraphQL response structure for the place order mutation. It ensures the client always receives either order data or errors in a consistent format.
using APIGateway.GraphQL.Output.Models;
namespace APIGateway.GraphQL.Output.Types
{
// GraphQL Output Type mapping for PlaceOrderPayload.
// Why do we return a "payload" object in GraphQL mutations?
// - It gives the client a predictable response shape for both success and failure.
// - Instead of throwing exceptions for validation/business failures,
// we can return:
// 1) Order -> when operation succeeds
// 2) Errors -> when operation fails (or empty when success)
public class PlaceOrderPayloadType : ObjectType<PlaceOrderPayload>
{
protected override void Configure(IObjectTypeDescriptor<PlaceOrderPayload> descriptor)
{
// Name shown in GraphQL schema as: type PlaceOrderPayload { ... }
descriptor.Name("PlaceOrderPayload");
// 'Order' is nullable because placeOrder can fail.
// On failure:
// - Order will be null
// - Errors will contain one or more UserError entries
//
// On success:
// - Order will contain created order details
// - Errors will typically be empty
descriptor.Field(x => x.Order)
.Type<OrderOutputType>();
// 'Errors' is a NonNull list of NonNull UserError objects:
// - The list itself is never null (client can safely iterate without null checks).
// - Each error item inside the list is never null.
//
// This design makes UI handling simple:
// if (errors.length > 0) show errors;
descriptor.Field(x => x.Errors)
.Type<NonNullType<ListType<NonNullType<UserErrorType>>>>();
}
}
}
GraphQL/Output/Types/OrderInfoOutputType.cs
Create a class file named OrderInfoOutputType.cs within the GraphQL/Output/Types folder, and then copy-paste the following code. This class represents order-level summary information, such as totals, status, and dates. It is commonly used in order details and summary screens.
using APIGateway.DTOs.OrderSummary;
namespace APIGateway.GraphQL.Output.Types
{
// GraphQL Output Type for the order-level summary information.
// This type is typically used in an "Order Details / Order Summary" screen where the UI needs:
// - Order number + date + status
// - Pricing breakdown (subtotal, discount, shipping, tax, total)
// - Currency + payment method
public class OrderInfoOutputType : ObjectType<OrderInfoDTO>
{
protected override void Configure(IObjectTypeDescriptor<OrderInfoDTO> descriptor)
{
// Name shown in GraphQL schema as:
descriptor.Name("OrderInfo");
// Basic order details are required for any order summary.
descriptor.Field(x => x.OrderNumber).Type<NonNullType<StringType>>();
descriptor.Field(x => x.OrderDate).Type<NonNullType<DateTimeType>>();
descriptor.Field(x => x.Status).Type<NonNullType<StringType>>();
// Pricing values are required because UI needs them to display totals and invoice-like summary.
descriptor.Field(x => x.SubTotalAmount).Type<NonNullType<DecimalType>>();
descriptor.Field(x => x.DiscountAmount).Type<NonNullType<DecimalType>>();
descriptor.Field(x => x.ShippingCharges).Type<NonNullType<DecimalType>>();
descriptor.Field(x => x.TaxAmount).Type<NonNullType<DecimalType>>();
descriptor.Field(x => x.TotalAmount).Type<NonNullType<DecimalType>>();
// Currency and payment method are usually required to display on the order summary screen.
// (Example: INR / UPI / COD)
descriptor.Field(x => x.Currency).Type<NonNullType<StringType>>();
descriptor.Field(x => x.PaymentMethod).Type<NonNullType<StringType>>();
}
}
}
GraphQL/Output/Types/CustomerInfoOutputType.cs
Create a class file named CustomerInfoOutputType.cs within the GraphQL/Output/Types folder, and then copy-paste the following code. This class exposes customer information related to an order. Different screens can request only the customer fields they need, such as name or email.
using APIGateway.DTOs.OrderSummary;
namespace APIGateway.GraphQL.Output.Types
{
// GraphQL Output Type for customer/user information shown on the Order Details screen.
// Customer info can vary by screen:
// - Some screens need full profile details (name, email, mobile)
// - Some screens may need only UserId + FullName
// GraphQL makes this easy because clients can request only the fields they need.
public class CustomerInfoOutputType : ObjectType<CustomerInfoDTO>
{
protected override void Configure(IObjectTypeDescriptor<CustomerInfoDTO> descriptor)
{
// Name shown in schema as:
descriptor.Name("CustomerInfo");
// UserId is required because it's the key identity of the customer.
descriptor.Field(x => x.UserId).Type<NonNullType<UuidType>>();
// These fields are optional because user profile data may not always exist
// (example: email/mobile not provided, profile photo not set).
descriptor.Field(x => x.FullName).Type<StringType>();
descriptor.Field(x => x.Email).Type<StringType>();
descriptor.Field(x => x.Mobile).Type<StringType>();
descriptor.Field(x => x.ProfilePhotoUrl).Type<StringType>();
}
}
}
GraphQL/Output/Types/OrderProductInfoOutputType.cs
Create a class file named OrderProductInfoOutputType.cs within the GraphQL/Output/Types folder, and then copy-paste the following code. This class represents product details inside an order, including name, quantity, pricing, and images. It is mainly used to build order item lists on UI screens.
using APIGateway.DTOs.OrderSummary;
namespace APIGateway.GraphQL.Output.Types
{
// GraphQL Output Type for product/item information inside an order.
// This type represents the "Items" section of an Order Details screen.
// It includes product identity + display info (name, image) + pricing and quantity.
public class OrderProductInfoOutputType : ObjectType<OrderProductInfoDTO>
{
protected override void Configure(IObjectTypeDescriptor<OrderProductInfoDTO> descriptor)
{
// Name shown in schema as:
descriptor.Name("OrderProductInfo");
// ProductId and Name are required because every line item must refer to a product,
// and UI must show at least the product name.
descriptor.Field(x => x.ProductId).Type<NonNullType<UuidType>>();
descriptor.Field(x => x.Name).Type<NonNullType<StringType>>();
// SKU and ImageUrl are optional because:
// - SKU might not be configured for all products
// - ImageUrl might be missing for some products
descriptor.Field(x => x.SKU).Type<StringType>();
descriptor.Field(x => x.ImageUrl).Type<StringType>();
// Quantity and UnitPrice are required for displaying order item breakdown.
descriptor.Field(x => x.Quantity).Type<NonNullType<IntType>>();
descriptor.Field(x => x.UnitPrice).Type<NonNullType<DecimalType>>();
// LineTotal is typically a computed value:
// LineTotal = Quantity * UnitPrice (or after discount rules if applied)
// Mark NonNull because UI expects it for totals.
descriptor.Field(x => x.LineTotal).Type<NonNullType<DecimalType>>();
}
}
}
GraphQL/Output/Types/PaymentInfoOutputType.cs
Create a class file named PaymentInfoOutputType.cs within the GraphQL/Output/Types folder, and then copy-paste the following code. This class exposes payment-related details of an order. It supports different payment methods, such as COD and online payments, and some fields may be optional.
using APIGateway.DTOs.OrderSummary;
namespace APIGateway.GraphQL.Output.Types
{
// GraphQL Output Type for payment information related to an order.
// Payment details may vary depending on payment method:
// - COD: PaymentId/transaction may not exist, PaidOn may be null
// - Online payment: PaymentId, PaidOn, and transaction reference often exist
public class PaymentInfoOutputType : ObjectType<PaymentInfoDTO>
{
protected override void Configure(IObjectTypeDescriptor<PaymentInfoDTO> descriptor)
{
// Name shown in schema as:
// type PaymentInfo { ... }
descriptor.Name("PaymentInfo");
// PaymentId can be null in some flows (example: COD or payment not created yet),
// so it is kept nullable in schema.
descriptor.Field(x => x.PaymentId).Type<UuidType>();
// Status and Method are mandatory for UI (Paid/Pending/Failed, COD/UPI/Card).
descriptor.Field(x => x.Status).Type<NonNullType<StringType>>();
descriptor.Field(x => x.Method).Type<NonNullType<StringType>>();
// PaidOn is optional because payment may not be completed yet.
descriptor.Field(x => x.PaidOn).Type<DateTimeType>();
// TransactionReference is optional because:
// - COD may not have it
// - Some gateways may provide it only after success
descriptor.Field(x => x.TransactionReference).Type<StringType>();
}
}
}
GraphQL/Output/Types/OrderSummaryOutputType.cs
Create a class file named OrderSummaryOutputType.cs within the GraphQL/Output/Types folder, and then copy-paste the following code. This class represents a screen-ready order summary. It combines order, customer, product, and payment details into a single flexible response for different UIs.
using APIGateway.DTOs.OrderSummary;
namespace APIGateway.GraphQL.Output.Types
{
// GraphQL Output Type for OrderSummaryResponseDTO.
// This is a "screen-ready" response model typically used for the Order Details screen.
// Instead of the UI calling multiple microservices and merging responses,
// the Gateway returns a single OrderSummary object that contains:
// - Order info (totals, status, payment method, etc.)
// - Customer info (optional, depends on availability/permissions)
// - Products/items (line items)
// - Payment info (optional, depends on payment flow)
//
// GraphQL advantage:
// Different screens can request different shapes from this type.
// Example:
// - Mobile screen may request only OrderId + Status + Total
// - Web screen may request deep product list + customer + payment details
public class OrderSummaryOutputType : ObjectType<OrderSummaryResponseDTO>
{
protected override void Configure(IObjectTypeDescriptor<OrderSummaryResponseDTO> descriptor)
{
// Name shown in GraphQL schema as:
descriptor.Name("OrderSummary");
// Unique identifier of the order summary.
// NonNull because it is the key used by UI and links/actions.
descriptor.Field(x => x.OrderId)
.Type<NonNullType<UuidType>>();
// Order info is mandatory because every order summary must contain base order details.
// NonNull ensures the UI can safely read totals/status without null checks.
descriptor.Field(x => x.Order)
.Type<NonNullType<OrderInfoOutputType>>();
// Customer info is optional because:
// - Some screens may not require it
// - Some flows may not fetch it (or privacy rules may hide it)
// Keeping it nullable makes the response flexible across screens/clients.
descriptor.Field(x => x.Customer)
.Type<CustomerInfoOutputType>();
// Products (items) list is mandatory because an order summary without items is meaningless.
// NonNull list + NonNull items means:
// - The list itself is never null (client can iterate safely)
// - Each product entry is never null
descriptor.Field(x => x.Products)
.Type<NonNullType<ListType<NonNullType<OrderProductInfoOutputType>>>>();
// Payment info is optional because:
// - COD may not have full payment details
// - Online payment may not be completed yet
// - Payment service might be temporarily unavailable in partial scenarios
descriptor.Field(x => x.Payment)
.Type<PaymentInfoOutputType>();
// Indicates whether this summary is partial.
// Example: some microservice call failed, so only partial data is returned.
// NonNull so the UI can always rely on this flag.
descriptor.Field(x => x.IsPartial)
.Type<NonNullType<BooleanType>>();
// Warnings are non-fatal messages meant for the UI.
// Example:
// - "Payment service unavailable, showing last known status."
// - "Some product details could not be loaded."
//
// NonNull list ensures UI always receives a list (empty or filled).
descriptor.Field(x => x.Warnings)
.Type<NonNullType<ListType<NonNullType<StringType>>>>();
}
}
}
Creating Services:
In a GraphQL-based application, services contain the actual business logic and data access code. GraphQL itself does not handle business rules; instead, it calls existing services to fetch or modify data. This keeps GraphQL lightweight and ensures that all core logic remains reusable and independent of the API style.
- Contain business rules and validations
- Communicate with databases or other microservices
- Remain the same whether you use REST or GraphQL
- Called by GraphQL resolvers to perform real work
Services/IOrderApiClient.cs
Create an interface named IOrderApiClient.cs within the Services folder, and then copy-paste the following code. This interface defines how the API Gateway communicates with the Order microservice. It keeps GraphQL independent of HTTP and service discovery details.
using APIGateway.DTOs.Common;
using OrderService.Application.DTOs.Order;
namespace APIGateway.Services
{
public interface IOrderApiClient
{
// Calls OrderService to create a new order.
// request : The order request data (userId, items, addresses, payment method etc.)
// authorizationHeader : The incoming Authorization header from client request (forwarded to OrderService)
// cancellationToken : Supports request cancellation (timeout, client disconnect, etc.)
Task<ApiResponse<OrderResponseDTO>> CreateOrderAsync(
CreateOrderRequestDTO request,
string? authorizationHeader,
CancellationToken cancellationToken = default);
}
}
Services/OrderApiClient.cs
Create a class file named OrderApiClient.cs within the Services folder, and then copy-paste the following code. This class implements the actual HTTP communication with the Order service. It forwards requests, handles authorization headers, and converts responses into a common format.
using APIGateway.DTOs.Common;
using ECommerce.Common.ServiceDiscovery.Resolution;
using OrderService.Application.DTOs.Order;
using System.Net;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace APIGateway.Services
{
// Concrete implementation of IOrderApiClient.
// Responsibility:
// - Resolve OrderService URL (service discovery)
// - Forward the request to OrderService
// - Forward Authorization token if present
// - Read and return the response in ApiResponse<T> format
// Important:
// This class should NOT contain business logic.
// It only handles HTTP communication and basic response normalization.
public class OrderApiClient : IOrderApiClient
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServiceResolver _resolver;
private readonly ILogger<OrderApiClient> _logger;
// JSON options used across all HTTP calls to OrderService.
// - PropertyNameCaseInsensitive: supports different JSON casing
// - JsonStringEnumConverter: allows enum values in string form ("COD", "UPI") instead of numeric
// Keeping a static shared instance avoids re-creating options for every request.
private static readonly JsonSerializerOptions JsonOptions = new()
{
// PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = null,
Converters = { new JsonStringEnumConverter() }
};
// IHttpClientFactory: recommended way in ASP.NET Core to create HttpClient safely.
// IServiceResolver : resolves microservice base URL dynamically (Consul/Eureka/etc.)
// ILogger : logs failures for debugging and monitoring.
public OrderApiClient(
IHttpClientFactory httpClientFactory,
IServiceResolver resolver,
ILogger<OrderApiClient> logger)
{
_httpClientFactory = httpClientFactory;
_resolver = resolver;
_logger = logger;
}
// Calls OrderService /api/Order to create an order.
// This method is typically invoked by a GraphQL mutation.
public async Task<ApiResponse<OrderResponseDTO>> CreateOrderAsync(
CreateOrderRequestDTO request,
string? authorizationHeader,
CancellationToken cancellationToken = default)
{
try
{
// Create HttpClient instance from factory.
// This avoids socket exhaustion and supports proper handler reuse.
var client = _httpClientFactory.CreateClient();
// Resolve the current base URI of OrderService using service discovery.
// This allows load balancing and dynamic instance discovery.
var orderServiceUri = await _resolver.ResolveServiceUriAsync("OrderService", cancellationToken);
// Build the final endpoint URL for placing the order.
// Example: http://orderservice/api/Order
var requestUri = new Uri(orderServiceUri, "/api/Order");
// Forward Authorization header to OrderService
if (!string.IsNullOrWhiteSpace(authorizationHeader))
{
// If already in the format: "Bearer <token>", parse directly.
if (authorizationHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
client.DefaultRequestHeaders.Authorization = AuthenticationHeaderValue.Parse(authorizationHeader);
else
// If only raw token is passed, wrap it as Bearer token.
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", authorizationHeader);
}
// Send request to OrderService
// PostAsJsonAsync serializes request body into JSON using JsonOptions.
var response = await client.PostAsJsonAsync(requestUri, request, JsonOptions, cancellationToken);
// Read response into common API wrapper
// We expect OrderService to return ApiResponse<OrderResponseDTO> JSON.
// If response body is empty, we create a friendly failure response.
var apiResponse =
await response.Content.ReadFromJsonAsync<ApiResponse<OrderResponseDTO>>(JsonOptions, cancellationToken)
?? ApiResponse<OrderResponseDTO>.FailResponse("OrderService returned an empty response.");
// Handle Unauthorized explicitly
// Even if downstream returned a body, unauthorized should be handled clearly.
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
return ApiResponse<OrderResponseDTO>.FailResponse(
"Unauthorized.",
new List<string> { "Invalid or missing token." });
}
// Handle mismatch: HTTP failure but body says Success = true
// This is a safety check to avoid returning "success" when HTTP status is not success.
if (!response.IsSuccessStatusCode && apiResponse.Success)
{
return ApiResponse<OrderResponseDTO>.FailResponse("OrderService returned failure status.");
}
// Return the response as-is (either success or failure with messages).
return apiResponse;
}
catch (Exception ex)
{
// Any unexpected issue (service discovery failure, network error, serialization issue, etc.)
// should be logged and converted into a safe response for the UI.
_logger.LogError(ex, "CreateOrderAsync failed in OrderApiClient.");
// Avoid leaking internal stack trace details to client in production.
// Here we return a friendly message + minimal detail.
return ApiResponse<OrderResponseDTO>.FailResponse(
"Failed to place order.",
new List<string> { ex.Message });
}
}
}
}
Queries (For Read Operation)
Queries in GraphQL are used to fetch data. They represent read-only operations and do not modify any data. Queries allow the client to specify exactly which fields it needs, making them ideal for building UI screens that require different data shapes.
- Used only for data retrieval
- Similar to REST GET requests
- Client controls which fields are returned
- Can fetch related and nested data in one request
GraphQL/Queries/GatewayQuery.cs
Create a class file named GatewayQuery.cs within the GraphQL/Queries folder, and then copy-paste the following code. This class defines read-only GraphQL operations. It allows the frontend to fetch order summaries in a single query rather than calling multiple microservices.
using APIGateway.DTOs.OrderSummary;
using APIGateway.Services;
using HotChocolate.Authorization;
namespace APIGateway.GraphQL.Queries
{
// GraphQL Query resolver class for the API Gateway.
// Purpose:
// - Exposes read operations (Queries) to clients through the /graphql endpoint.
// - Acts as a BFF layer: the UI asks once, and the gateway collects data from multiple services.
// Note:
// Each public method becomes a GraphQL query field (based on Hot Chocolate conventions).
public class GatewayQuery
{
// Real-time scenario:
// The Order Details / Dashboard screen needs a complete OrderSummary in ONE call:
// - Order info
// - Customer info
// - Product list
// - Payment info
// Different screens need different data shapes. With GraphQL, the UI can request only the
// fields it needs from OrderSummary.
[Authorize] // Ensures only authenticated users can access this query.
public async Task<OrderSummaryResponseDTO?> GetOrderSummaryAsync(
Guid orderId,
[Service] IOrderSummaryAggregator aggregator, // Injected service that collects data from microservices.
[Service] ILogger<GatewayQuery> logger) // Injected logger for observability & debugging.
{
// Log the query execution start with the orderId for traceability.
logger.LogInformation(
"GraphQL Query GetOrderSummary called. OrderId={OrderId}",
orderId);
try
{
// Call the gateway aggregation service that fetches and combines data
// from multiple microservices into a single OrderSummaryResponseDTO.
//
// This keeps the resolver thin:
// - Resolver = orchestration entry point
// - Aggregator/service layer = actual orchestration logic
var result = await aggregator.GetOrderSummaryAsync(orderId);
// If result is null, it usually means the order does not exist
// (or could not be fetched due to downstream service issues).
if (result == null)
{
logger.LogWarning(
"OrderSummary not found. OrderId={OrderId}",
orderId);
}
else
{
// Success log helps in monitoring and debugging performance issues.
logger.LogInformation(
"OrderSummary successfully fetched. OrderId={OrderId}",
orderId);
}
// Returning the DTO allows GraphQL to shape the final response
// based on what fields the client requested.
return result;
}
catch (Exception ex)
{
// Log the full exception for server-side debugging.
logger.LogError(
ex,
"Error occurred while fetching OrderSummary. OrderId={OrderId}",
orderId);
// Re-throw so Hot Chocolate can return a GraphQL error response.
throw;
}
}
}
}
Mutations (For Write Operations)
Mutations in GraphQL are used to create, update, or delete data. They represent write operations and usually return a structured response that includes both the result and any user-friendly errors. Mutations help keep data changes explicit and predictable.
- Used for write operations
- Similar to REST POST, PUT, and DELETE
- Accept structured input models
- Return a payload containing result data and errors
GraphQL/Mutations/GatewayMutation.cs
Create a class file named GatewayMutation.cs within the GraphQL/Mutations folder, and then copy-paste the following code. This class defines write operations, such as placing an order. It receives GraphQL input, calls backend services, and returns structured results without duplicating business logic.
using APIGateway.GraphQL.Input.Models;
using APIGateway.GraphQL.Output.Models;
using APIGateway.Services;
using HotChocolate.Authorization;
using OrderService.Application.DTOs.Order;
using OrderService.Contracts.DTOs;
namespace APIGateway.GraphQL.Mutations
{
// GraphQL Mutation resolver class for the API Gateway.
// Purpose:
// - Exposes write operations (Mutations) through /graphql.
// - Acts as a BFF layer: UI sends ONE request, gateway forwards it to the right microservice.
public class GatewayMutation
{
// Mutation: placeOrder
// Real-time UI scenario:
// - Client sends cart items + userId + addresses + payment method in one request.
// - Gateway forwards the request to OrderService (CreateOrder API).
// - Gateway returns PlaceOrderPayload: { order, errors }
[Authorize] // Only authenticated users can place orders.
public async Task<PlaceOrderPayload> PlaceOrderAsync(
PlaceOrderInputModel input, // GraphQL input sent by client
[Service] IOrderApiClient orderApiClient, // Gateway client that calls OrderService
[Service] IHttpContextAccessor httpContextAccessor, // Needed to forward Authorization header
CancellationToken cancellationToken)
{
// 1) Map GraphQL Input Model -> OrderService CreateOrderRequestDTO
// GraphQL input models are designed for client-friendly inputs.
// Microservice DTOs are designed for service contracts.
// So we map between them in the gateway.
var createOrderRequest = new CreateOrderRequestDTO
{
// Who is placing the order
UserId = input.UserId,
// How user wants to pay (Enum)
PaymentMethod = input.PaymentMethod,
// Address references (if UI selected saved addresses)
ShippingAddressId = input.ShippingAddressId,
BillingAddressId = input.BillingAddressId,
// Convert item input list to OrderService expected request list
Items = input.Items.Select(i => new OrderItemRequestDTO
{
ProductId = i.ProductId,
Quantity = i.Quantity
}).ToList(),
// If UI provided a new address object (instead of addressId),
// map it to AddressDTO for OrderService.
ShippingAddress = MapAddress(input.UserId, input.ShippingAddress),
BillingAddress = MapAddress(input.UserId, input.BillingAddress)
};
// 2) Forward Authorization header to downstream OrderService
var authHeader = httpContextAccessor.HttpContext?.Request.Headers["Authorization"].ToString();
// 3) Call downstream OrderService using gateway client
// OrderService performs the actual business logic:
// - validation rules
// - pricing calculation
// - inventory checks (if any)
// - order creation
DTOs.Common.ApiResponse<OrderResponseDTO> response =
await orderApiClient.CreateOrderAsync(createOrderRequest, authHeader, cancellationToken);
// 4) Convert downstream response to GraphQL payload
// Success case:
// - return Order data
// - return empty error list
if (response.Success && response.Data is not null)
{
return new PlaceOrderPayload
{
Order = response.Data,
Errors = new List<UserError>() // always return list; keep it empty for success
};
}
// Failure case:
// We do NOT throw here because this is a business failure (not a system crash).
// Instead, return errors in payload so UI can show a proper message.
var errors = new List<UserError>();
// If OrderService returned a top-level message, convert it to UserError.
if (!string.IsNullOrWhiteSpace(response.Message))
{
errors.Add(new UserError
{
Code = "ORDER_FAILED",
Message = response.Message!
});
}
// If OrderService returned multiple error messages, convert each to UserError.
if (response.Errors is not null && response.Errors.Count > 0)
{
errors.AddRange(response.Errors.Select(e => new UserError
{
Code = "ORDER_FAILED",
Message = e
}));
}
// Safety fallback: Ensure at least one error exists.
if (errors.Count == 0)
{
errors.Add(new UserError
{
Code = "ORDER_FAILED",
Message = "Failed to place order."
});
}
// Return predictable mutation result: Order = null, Errors = list.
return new PlaceOrderPayload { Order = null, Errors = errors };
}
// Maps a GraphQL AddressInputModel to the AddressDTO expected by OrderService.
// - GraphQL input is designed for UI
// - OrderService contract expects AddressDTO
// - Mapping keeps the gateway clean and avoids repeating code in the mutation
private static AddressDTO? MapAddress(Guid userId, AddressInputModel? address)
{
// If user did not provide a new address object, skip mapping.
if (address is null) return null;
return new AddressDTO
{
// If Id is not provided, Guid.Empty indicates "new address".
// OrderService can decide whether to create or ignore based on its rules.
Id = address.Id ?? Guid.Empty,
// Attach userId so service can validate ownership and store correctly.
UserId = userId,
// Copy address fields
AddressLine1 = address.AddressLine1,
AddressLine2 = address.AddressLine2,
City = address.City,
State = address.State,
PostalCode = address.PostalCode,
Country = address.Country,
// Flags used to mark default addresses
IsDefaultBilling = address.IsDefaultBilling,
IsDefaultShipping = address.IsDefaultShipping
};
}
}
}
GraphQL/Schema/GatewayGraphQLSchemaExtensions.cs
Create a class file named GatewayGraphQLSchemaExtensions.cs within the GraphQL/Schema folder, and then copy-paste the following code. This class registers all GraphQL components—queries, mutations, input types, output types, and enums—into the ASP.NET Core DI container to build the GraphQL schema.
using APIGateway.GraphQL.Input.Types;
using APIGateway.GraphQL.Mutations;
using APIGateway.GraphQL.Output.Types;
using APIGateway.GraphQL.Queries;
using OrderService.Contracts.Enums;
using OrderService.Domain.Enums;
namespace APIGateway.GraphQL.Schema
{
// Extension method to register and configure GraphQL for the API Gateway.
// - The Gateway acts as a BFF (Backend for Frontend).
// - UI screens need different shapes of data.
// - GraphQL provides one endpoint (/graphql) where the client can request only required fields.
// This registration builds the GraphQL schema (contract) that clients will use.
public static class GatewayGraphQLSchemaExtensions
{
// Adds GraphQL server and registers:
// - Root operations (Query + Mutation)
// - Input types (for client request payloads)
// - Output types (for response shapes)
// - Enum types (for strict allowed values)
public static IServiceCollection AddGatewayGraphQL(this IServiceCollection services)
{
services
// Registers Hot Chocolate GraphQL server in ASP.NET Core DI container.
.AddGraphQLServer()
// Enables [Authorize] attribute support for Query/Mutation methods.
// This integrates GraphQL with ASP.NET Core authentication/authorization.
.AddAuthorization()
// Root Operation Types (Entry points for clients)
// Query = read operations (similar to REST GET)
// Mutation = write operations (similar to REST POST/PUT/DELETE)
.AddQueryType<GatewayQuery>()
.AddMutationType<GatewayMutation>()
// Input Types (Client request shapes)
// These define how input DTOs appear in GraphQL schema.
// They control: input object names, field types, nullability, etc.
.AddType<PlaceOrderInputType>()
.AddType<OrderItemInputType>()
.AddType<AddressInputType>()
// Output Types (Response shapes)
// These define how response DTOs appear in GraphQL schema.
// They control: output type names, field types, nested types, nullability, etc.
// These types are designed to be "screen-ready", meaning UI can fetch
// exactly what it needs without multiple REST calls.
.AddType<OrderSummaryOutputType>()
.AddType<OrderInfoOutputType>()
.AddType<CustomerInfoOutputType>()
.AddType<OrderProductInfoOutputType>()
.AddType<PaymentInfoOutputType>()
.AddType<OrderOutputType>()
.AddType<OrderItemOutputType>()
.AddType<PlaceOrderPayloadType>()
.AddType<UserErrorType>()
// Enum Types (Strict allowed values)
// Exposes C# enums as GraphQL enums.
// This improves contract clarity because the client can see
// the exact set of allowed values in schema (instead of random strings).
.AddType<EnumType<PaymentMethodEnum>>()
.AddType<EnumType<OrderStatusEnum>>();
// Return IServiceCollection so this can be chained in Program.cs
return services;
}
}
}
Program Class:
Please modify the Program class as follows. This class configures the API Gateway application. It wires up GraphQL, authentication, service discovery, logging, caching, and middleware, and exposes the /graphql endpoint.
using APIGateway.Extensions;
using APIGateway.GraphQL.Schema;
using APIGateway.Middlewares;
using APIGateway.Models;
using APIGateway.ServiceDiscovery;
using APIGateway.Services;
using ECommerce.Common.ServiceDiscovery.Extensions;
using HotChocolate.AspNetCore;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json.Serialization;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Ocelot.Provider.Consul;
using Serilog;
using System.Text;
namespace APIGateway
{
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Controllers (REST endpoints - optional in gateway)
builder.Services
.AddControllers()
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.ContractResolver =
new DefaultContractResolver
{
NamingStrategy = new DefaultNamingStrategy()
};
});
// HttpClient Factory (REQUIRED for Gateway → Microservices)
// GraphQL resolvers and aggregators call downstream microservices via HTTP.
// IHttpClientFactory avoids socket exhaustion and is best practice in ASP.NET Core.
builder.Services.AddHttpClient();
// Consul Service Discovery (Registration)
// Registers this API Gateway instance to Consul so other services can discover it.
// Also helps when gateway needs to resolve downstream services dynamically.
builder.Services.AddConsulRegistration(builder.Configuration);
// Ocelot (API Gateway routing for REST paths)
// Ocelot handles:
// - routing requests to microservices
// - load balancing (via Consul provider)
// - optional caching / auth / rate limiting (built-in or custom)
// ocelot.json contains route definitions for microservices.
builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);
builder.Services
.AddOcelot(builder.Configuration)
.AddConsul<ConsulServiceBuilder>();
// Custom Rate Limiting
builder.Services.AddCustomRateLimiting(builder.Configuration);
// Response Compression Settings (Custom)
builder.Services.Configure<CompressionSettings>(
builder.Configuration.GetSection("CompressionSettings"));
// Serilog (Structured Logging)
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration)
.Enrich.FromLogContext()
.CreateLogger();
builder.Host.UseSerilog();
// Swagger (REST only)
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// 9) JWT Authentication + Authorization
// This allows both:
// - REST endpoints to be protected
// - GraphQL resolvers to use [Authorize]
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
// Token must come from the configured issuer
ValidateIssuer = true,
ValidIssuer = builder.Configuration["JwtSettings:Issuer"],
// Audience validation disabled (common in internal systems)
ValidateAudience = false,
// Validate token expiry
ValidateLifetime = true,
// Validate signature using secret key
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["JwtSettings:SecretKey"]!)
),
// No extra grace time after expiry
ClockSkew = TimeSpan.Zero
};
});
builder.Services.AddAuthorization();
// IHttpContextAccessor is needed if you want to:
// - Forward Authorization header to microservices
// - Read user claims in gateway services/resolvers
builder.Services.AddHttpContextAccessor();
// Aggregation & Gateway Services (BFF logic)
// These are the gateway-side orchestration services.
// GraphQL resolvers call these services rather than calling HttpClient directly.
builder.Services.AddScoped<IOrderSummaryAggregator, OrderSummaryAggregator>();
builder.Services.AddScoped<IOrderApiClient, OrderApiClient>();
// Redis Cache
// Used by your custom response caching middleware.
// Helps improve performance for repeated reads (example: OrderSummary screen refresh).
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration =
builder.Configuration["RedisCacheSettings:ConnectionString"];
options.InstanceName =
builder.Configuration["RedisCacheSettings:InstanceName"];
});
// 12) GraphQL (Hot Chocolate) Registration
// Adds schema: Queries, Mutations, Input/Output types, enums, auth.
// Hosted under /graphql endpoint.
builder.Services.AddGatewayGraphQL();
var app = builder.Build();
// Middleware Pipeline
if (app.Environment.IsDevelopment())
{
// Swagger enabled only for REST in development.
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
// Add a correlation id so logs across gateway + services can be linked.
app.UseCorrelationId();
// Logs request + response details (useful for debugging in gateway).
app.UseRequestResponseLogging();
app.UseRouting();
// Authentication must come before Authorization.
app.UseAuthentication();
app.UseAuthorization();
// IMPORTANT:
// Apply Gateway middlewares ONLY to non-GraphQL requests
// Reason:
// GraphQL requests are often POST /graphql with a different payload structure.
// Applying gateway-specific middlewares (bearer validation, caching, compression, rate limiting)
// may cause unexpected behavior for GraphQL.
//
// So we exclude /graphql from those custom middlewares.
app.UseWhen(
ctx => !ctx.Request.Path.StartsWithSegments("/graphql"),
nonGraph =>
{
// Custom bearer validation (extra checks beyond JWT if any)
nonGraph.UseGatewayBearerValidation();
// Custom rate limiting for REST routes
nonGraph.UseCustomRateLimiting();
// Redis response caching for REST routes
nonGraph.UseRedisResponseCaching();
// Conditional compression for REST routes (skip small payloads, etc.)
nonGraph.UseMiddleware<ConditionalResponseCompressionMiddleware>();
});
// Health Check Endpoint
// Lightweight ping endpoint for uptime monitoring / load balancers.
app.MapGet("/health", async context =>
{
context.Response.StatusCode = StatusCodes.Status200OK;
await context.Response.WriteAsync("Gateway Healthy");
});
// Map REST controllers (if any)
app.MapControllers();
// GraphQL Endpoint
// Exposes Hot Chocolate GraphQL server at /graphql.
// EnableGetRequests = false means:
// - Only POST requests allowed
// - Improves security and prevents accidental query execution via URL
app.MapGraphQL("/graphql")
.WithOptions(new GraphQLServerOptions
{
EnableGetRequests = false
});
// Ocelot Pipeline (EXCLUDING GraphQL)
// Ocelot handles routing for REST endpoints defined in ocelot.json.
// We exclude /graphql so GraphQL requests are handled ONLY by Hot Chocolate.
app.UseWhen(
context => !context.Request.Path.StartsWithSegments("/graphql"),
async appBuilder =>
{
await appBuilder.UseOcelot();
});
await app.RunAsync();
}
}
}
Run Consul in Development Mode
Please run the following command in Command Prompt.
consul agent -dev -client=0.0.0.0 -ui -node=consul-dev
Note: Keep this command window open while working with your microservices. If you close it, Consul stops, and services cannot register or be discovered.
Open Consul UI
Once Consul is running, we can monitor everything from the browser. Open your browser and navigate to: http://localhost:8500
C# Names vs GraphQL Names (Hot Chocolate Naming Rules)
When we build GraphQL in .NET using Hot Chocolate, we usually don’t manually write the GraphQL schema. Instead, Hot Chocolate reads our C# code (Classes, Methods, Properties) and auto-generates GraphQL names using naming conventions.
Think of it like this: C# is written for developers (PascalCase, DTO suffixes, Async methods). GraphQL is written for clients (clean names, camelCase fields, simple type names). So, Hot Chocolate converts names to keep GraphQL clean and standard.
Class name → GraphQL Type name
GraphQL type names are meant to be short and readable for frontend/mobile developers.
- C#: public class OrderSummaryResponseDTO
- GraphQL: type OrderSummary
What Hot Chocolate does:
- Removes common suffix words like: DTO, Dto, Input, Payload, Response, etc. (to keep schema clean)
- Keeps type names in PascalCase (GraphQL standard)
Property name → GraphQL Field name
In GraphQL, field names follow JavaScript-style camelCase.
C#
- public Guid OrderId { get; set; }
- public bool IsPartial { get; set; }
GraphQL
- orderId
- isPartial
What Hot Chocolate does:
- Converts PascalCase → camelCase
- Only changes the first letter for most names
Method name → GraphQL Query/Mutation field name
GraphQL fields should look like natural operations, not .NET method naming.
- C#: public Task<OrderSummaryResponseDTO?> GetOrderSummaryAsync(Guid orderId)
- GraphQL: orderSummary(orderId: UUID!): OrderSummary
What Hot Chocolate does:
- Removes common method prefix: Get
- Removes suffix: Async
- Converts the remaining name to camelCase
So:
- GetOrderSummaryAsync → OrderSummary → orderSummary
Parameter name → GraphQL argument name
- C#: GetOrderSummaryAsync(Guid orderId)
- GraphQL: orderSummary(orderId: UUID!)
What Hot Chocolate does:
- Uses the C# parameter name as the GraphQL argument name
- Applies camelCase (if needed)
So:
- orderId stays orderId
Special Parameters are NOT GraphQL inputs (Services)
If a parameter has [Service], it is taken from Dependency Injection, not from the client.
C#
public Task<OrderSummaryResponseDTO?> GetOrderSummaryAsync(
Guid orderId,
[Service] IOrderSummaryAggregator aggregator,
[Service] ILogger<GatewayQuery> logger)
GraphQL Input
orderSummary(orderId: UUID!)
Here:
- Only orderId becomes an input argument.
- aggregator and logger are internal server services.
What is SDL?
SDL (Schema Definition Language) is the official description of a GraphQL API. It tells every client (UI, mobile, Postman, other services):
- What operations can I call? (Query, Mutation, and sometimes Subscription)
- What inputs do those operations accept?
- What output shape can I receive back?
- What fields are available on each output type?
- Which fields/operations require authentication or other rules?
Think of SDL like Swagger/OpenAPI, but with one major difference:
- Swagger documents many endpoints (/users, /orders, etc.)
- GraphQL documents one endpoint and the data graph behind it.
Schema Block (Root Mapping)
In REST:
- Different URLs and HTTP verbs indicate the action you are performing (GET /orders, POST /orders, etc.).
In GraphQL:
- The same endpoint (/graphql) is used for everything. So GraphQL must still separate:
- read operations
- write operations
GraphQL does not use URLs like this, so it needs a replacement. That replacement is the schema block.
The schema block tells GraphQL:
- Which type is the starting point for reading data?
- Which type is the starting point for writing data?
Example:
schema {
query: GatewayQuery
mutation: GatewayMutation
}
When a request comes:
- If the request starts with query → GraphQL looks inside the GatewayQuery type
- If it starts with mutation → GraphQL looks inside the GatewayMutation type
In simple words, the schema block tells GraphQL where to start.
Type (Object Types – Output Models)
A type describes the Shape of the Response. When we write a query, we select fields from these types. It defines:
- Field Names (what you can ask).
- Field Types (string, number, other type).
- Whether fields can be null (!).
Example:
type CustomerInfo {
userId: UUID!
fullName: String
email: String
mobile: String
profilePhotoUrl: String
}
This is a GraphQL Object Type. It defines what fields the CustomerInfo object contains.
- UUID! means required (non-null)
- String without ! means optional (can be null)
So:
- userId will always be present
- fullName, email, etc. may come as null
Like this, in our example, we have also defined the following output types:
- Order
- OrderInfo
- OrderItem
- OrderProductInfo
- OrderSummary
- PaymentInfo
- PlaceOrderPayload
- UserError
Input (Input Object Types – Request Models)
An input defines the shape of data that the client is allowed to send. It contains:
- input field names
- input field types
- required/optional flags
GraphQL does not allow type objects as inputs. Inputs must be input. It is used mainly for mutations (creation or update).
Examples:
input AddressInput {
id: UUID
addressLine1: String!
addressLine2: String
city: String!
state: String!
postalCode: String!
country: String!
isDefaultBilling: Boolean!
isDefaultShipping: Boolean!
}
Enums (Fixed Allowed Values)
An enum defines a limited set of allowed values. To prevent invalid values and to make APIs self-documenting. Used in inputs and outputs.
Examples:
enum OrderStatusEnum {
PENDING
CONFIRMED
PACKED
SHIPPED
DELIVERED
CANCELLED
PARTIAL_CANCELLED
RETURNED
PARTIAL_RETURNED
}
Scalars
Scalars are primitive-like types.
- DateTime: ISO-8601 datetime.
- Decimal: Decimal numbers (money).
- UUID: Guid format.
Query Type – Read Operations
Used to fetch data (like GET). This defines our query operations.
Example:
type GatewayQuery {
orderSummary(orderId: UUID!): OrderSummary @authorize @cost(weight: "10")
}
Here:
- Name: orderSummary
- Parameter: orderId: UUID! required
- Return: OrderSummary
Directives:
- @authorize → JWT Required
- @cost(weight: “10”) → Query Cost
So, to call this, we must:
- Send Authorization: Bearer <token>
- Send the orderId variable/argument
Mutation Type – Write Operations
Used to change data (like POST/PUT/DELETE)
type GatewayMutation {
placeOrder(input: PlaceOrderInput!): PlaceOrderPayload!
@authorize
@cost(weight: "10")
}
This defines our mutation operations.
Operation: placeOrder
- Name: placeOrder
- Input: input: PlaceOrderInput! → You must pass input
- Return: PlaceOrderPayload! → Payload always returned (even if the order is null, errors can be present)
Directives used:
- @authorize → User must be authenticated (JWT token required)
- @cost(weight: “10”) → Cost analysis rule (prevents expensive queries, rate limiting by query cost).
What is the GraphQL Endpoint?
The GraphQL endpoint is the single HTTP endpoint through which all GraphQL operations are executed. In our application, this endpoint is configured in Program.cs using the following mapping:
- endpoints.MapGraphQL(“/graphql”);
This configuration exposes GraphQL at the URL:
- https://<your-gateway-host>/graphql
- Example: https://localhost:7204/graphql
Unlike REST APIs, which use multiple endpoints for different operations, GraphQL uses a single endpoint for both reading and writing data. Whether the client wants to fetch data (Query) or modify data (Mutation), all requests are sent to this same /graphql endpoint. The actual operation to perform is determined by the GraphQL query or mutation inside the request body, not by the URL.
Key Points
- GraphQL exposes a single endpoint
- Both Queries (read) and Mutations (write) use the same endpoint
- The request body defines what operation is executed
- Simplifies API design and routing at the gateway level
GraphQL Query Syntax:
The following is the GraphQL Query Syntax.
{
"query": "query ($orderId: UUID!) {
orderSummary(orderId: $orderId) {
orderId
order {
orderNumber
orderDate
status
totalAmount
}
customer {
userId
fullName
email
}
products {
productId
name
quantity
unitPrice
lineTotal
}
payment {
paymentId
status
method
}
isPartial
warnings
}
}",
"variables": {
"orderId": "5CFE4EEA-B26A-4FEE-803D-0EDCA3399213"
}
}
Request Body Has 2 Parts
Part A – “query”
This is the GraphQL Query Language (not JSON). It tells the server:
- Which operation to call
- What input to use
- Which output fields to return
Part B – “variables”
This is normal JSON. It supplies the actual values for the variables we declared in the query.
Inside “query”: query ($orderId: UUID!)
query ($orderId: UUID!)
Meaning
- query → We are doing a Read Operation (like GET in REST, but still sent via POST)
- ($orderId: UUID!) → We declare a variable:
-
- $orderId = Variable Name (placeholder)
- UUID = GraphQL Data Type (Guid)
- ! = Required (cannot be null)
-
So, this line means: This query requires an input called orderId of type UUID (Guid).
Calling the operation: orderSummary(orderId: $orderId)
orderSummary(orderId: $orderId)
Meaning
- orderSummary → The Query Operation we are calling (defined in our schema)
- orderId: → The Argument Name that the operation expects
- $orderId → Use the variable declared earlier
So it means call orderSummary and pass orderId with the variable value.
The most important part: return fields (selection)
Everything inside this block will be returned by GraphQL:
{
orderId
order { ... }
customer { ... }
products { ... }
payment { ... }
isPartial
warnings
}
GraphQL will return only what we list here – nothing less and nothing more.
Postman Setup (POST Request Only)
Method Post:
Endpoint: https://localhost:7204/graphql/
Headers:
- Content-Type: application/json
- Authorization: Bearer <JWT>
Request 1: Return only orderId
{
"query": "query ($orderId: UUID!) { orderSummary(orderId: $orderId) { orderId } }",
"variables": {
"orderId": "5CFE4EEA-B26A-4FEE-803D-0EDCA3399213"
}
}
Request 2: OrderInfo (Order Summary Fields)
{
"query": "query ($orderId: UUID!) { orderSummary(orderId: $orderId) { order { orderNumber orderDate status subTotalAmount discountAmount shippingCharges taxAmount totalAmount currency paymentMethod } } }",
"variables": {
"orderId": "5CFE4EEA-B26A-4FEE-803D-0EDCA3399213"
}
}
Request 3: Return Only CustomerInfo
{
"query": "query ($orderId: UUID!) { orderSummary(orderId: $orderId) { customer { userId fullName email mobile profilePhotoUrl } } }",
"variables": {
"orderId": "5CFE4EEA-B26A-4FEE-803D-0EDCA3399213"
}
}
Request 4: Return only Products List
{
"query": "query ($orderId: UUID!) { orderSummary(orderId: $orderId) { products { productId name sku imageUrl quantity unitPrice lineTotal } } }",
"variables": {
"orderId": "5CFE4EEA-B26A-4FEE-803D-0EDCA3399213"
}
}
Request 5: Return Only PaymentInfo
{
"query": "query ($orderId: UUID!) { orderSummary(orderId: $orderId) { payment { paymentId status method paidOn transactionReference } } }",
"variables": {
"orderId": "5CFE4EEA-B26A-4FEE-803D-0EDCA3399213"
}
}
Request 6: Return Everything Useful (Full OrderSummary)
{
"query": "query ($orderId: UUID!) { orderSummary(orderId: $orderId) { orderId order { orderNumber orderDate status subTotalAmount discountAmount shippingCharges taxAmount totalAmount currency paymentMethod } customer { userId fullName email mobile profilePhotoUrl } products { productId name sku imageUrl quantity unitPrice lineTotal } payment { paymentId status method paidOn transactionReference } isPartial warnings } }",
"variables": {
"orderId": "5CFE4EEA-B26A-4FEE-803D-0EDCA3399213"
}
}
Request 7: Creating New Order
Existing Address (use ShippingAddressId + BillingAddressId)
{
"query": "mutation ($input: PlaceOrderInput!) { placeOrder(input: $input) { order { orderId orderNumber orderStatus paymentMethod orderDate subTotalAmount discountAmount shippingCharges taxAmount totalAmount } errors { code message } } }",
"variables": {
"input": {
"userId": "40112CDE-DE93-487E-B2C1-6B6BA8E5FE62",
"paymentMethod": "COD",
"shippingAddressId": "0fd56334-7fbb-4431-b727-4752ac9f051b",
"billingAddressId": "0fd56334-7fbb-4431-b727-4752ac9f051b",
"items": [
{
"productId": "E2F3D4C5-7A8B-4B23-8D9E-3C456789ABCD",
"quantity": 2
},
{
"productId": "A4B5C6D7-9C0D-4D45-AF10-5E6789ABCDEF",
"quantity": 1
}
]
}
}
}
Request 8: Creating New Order
This one does not use AddressIds. It sends full address objects.
{
"query": "mutation ($input: PlaceOrderInput!) { placeOrder(input: $input) { order { orderId orderNumber orderStatus paymentMethod orderDate subTotalAmount discountAmount shippingCharges taxAmount totalAmount } errors { code message } } }",
"variables": {
"input": {
"userId": "40112CDE-DE93-487E-B2C1-6B6BA8E5FE62",
"paymentMethod": "COD",
"shippingAddress": {
"addressLine1": "House/Flat No, Street Name",
"addressLine2": "Landmark (optional)",
"city": "YourCity",
"state": "YourState",
"postalCode": "000000",
"country": "India",
"isDefaultBilling": false,
"isDefaultShipping": true
},
"billingAddress": {
"addressLine1": "House/Flat No, Street Name",
"addressLine2": "Landmark (optional)",
"city": "YourCity",
"state": "YourState",
"postalCode": "000000",
"country": "India",
"isDefaultBilling": true,
"isDefaultShipping": false
},
"items": [
{
"productId": "E2F3D4C5-7A8B-4B23-8D9E-3C456789ABCD",
"quantity": 2
},
{
"productId": "A4B5C6D7-9C0D-4D45-AF10-5E6789ABCDEF",
"quantity": 1
}
]
}
}
}
Can I define multiple public methods in GatewayQuery and GatewayMutation?
Yes. We can define multiple public methods inside both GatewayQuery and GatewayMutation. Each public method represents one GraphQL operation.
- Public methods in GatewayQuery become GraphQL queries (read operations)
- Public methods in GatewayMutation become GraphQL mutations (write operations)
GraphQL automatically exposes each public method as a separate field in the schema.
GraphQL is designed to group operations by purpose, not by endpoint.
- GatewayQuery groups all read-related operations
- GatewayMutation groups all write-related operations
As your application grows, you naturally add more queries and mutations for different screens and use cases.
Example:
- GatewayQuery
-
- GetOrderSummary
- GetUserProfile
- GetProductDetails
-
- GatewayMutation
-
- PlaceOrder
- CancelOrder
- UpdateAddress
-
Each method serves a different UI requirement, but all are exposed through the same /graphql endpoint.
Implementing GraphQL in an ASP.NET Core microservices architecture allows us to efficiently aggregate data from multiple services and return only what the frontend needs. GraphQL fits naturally in the BFF layer and works smoothly with existing REST APIs and microservices. When used correctly, it reduces the number of API calls, simplifies frontend development, and makes the overall system more flexible and maintainable.
