GraphQL Explosion: Supercharge Your APIs with HotChocolate in ASP.NET Core
Table of Contents
1. Introduction to GraphQL Revolution {#introduction}
GraphQL represents a fundamental shift in how we design and consume APIs. Unlike traditional REST APIs, GraphQL gives clients the power to request exactly what they need, nothing more and nothing less. This client-driven approach eliminates over-fetching and under-fetching problems that plague REST APIs.
What is GraphQL?
GraphQL is a query language for your API and a runtime for executing those queries by using a type system you define for your data. It was developed by Facebook in 2012 and open-sourced in 2015.
// Traditional REST vs GraphQL public class RestVsGraphQL { // REST Approach - Multiple endpoints // GET /api/users/1 // GET /api/users/1/orders // GET /api/users/1/profile // GraphQL Approach - Single endpoint // POST /graphql /* query { user(id: 1) { name email orders { id total items { product { name price } } } profile { address phone } } } */ }
2. Why GraphQL Beats REST {#graphql-vs-rest}
Key Advantages of GraphQL
public class GraphQLAdvantages { // 1. No Over-fetching - Get only what you need public class OverFetchingExample { // REST: GET /api/users/1 // Returns ALL user fields even if you only need name and email // GraphQL: /* query { user(id: 1) { name email } } */ } // 2. No Under-fetching - Get related data in one request public class UnderFetchingExample { // REST: Multiple requests needed // GET /api/users/1 // GET /api/users/1/orders // GET /api/users/1/profile // GraphQL: Single request /* query { user(id: 1) { name orders { id total } profile { address } } } */ } // 3. Strongly Typed Schema public class TypeSafetyExample { // Compile-time validation // Auto-generated documentation // IDE support with IntelliSense } // 4. Real-time Updates with Subscriptions public class RealTimeExample { // Live updates when data changes // Perfect for chat, notifications, dashboards } }
3. Setting Up HotChocolate {#setup-hotchocolate}
Complete Project Setup
Project Structure:
GraphQLDemo/ ├── Models/ ├── Services/ ├── Types/ ├── Data/ ├── Program.cs └── appsettings.json
Program.cs - Complete HotChocolate Setup
using GraphQLDemo.Models; using GraphQLDemo.Services; using GraphQLDemo.Data; var builder = WebApplication.CreateBuilder(args); // Add HotChocolate GraphQL builder.Services .AddGraphQLServer() .AddQueryType<Query>() .AddMutationType<Mutation>() .AddSubscriptionType<Subscription>() .AddFiltering() .AddSorting() .AddProjections() .AddInMemorySubscriptions() // For development .AddErrorFilter<CustomErrorFilter>(); // Add Services builder.Services.AddScoped<IUserService, UserService>(); builder.Services.AddScoped<IProductService, ProductService>(); builder.Services.AddScoped<IOrderService, OrderService>(); // Add In-Memory Database for demo builder.Services.AddSingleton<ApplicationDbContext>(); // Add Real-time subscriptions support builder.Services.AddInMemorySubscriptions(); var app = builder.Build(); app.UseWebSockets(); // Required for subscriptions app.UseRouting(); app.MapGraphQL(); // Default: /graphql app.MapGraphQLVoyager(); // UI at /voyager (optional) app.Run();
Basic Models
Models/User.cs
namespace GraphQLDemo.Models { public class User { public int Id { get; set; } public string Username { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; public string FirstName { get; set; } = string.Empty; public string LastName { get; set; } = string.Empty; public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime? UpdatedAt { get; set; } public List<Order> Orders { get; set; } = new(); public UserProfile? Profile { get; set; } } public class UserProfile { public int Id { get; set; } public int UserId { get; set; } public string Address { get; set; } = string.Empty; public string City { get; set; } = string.Empty; public string Country { get; set; } = string.Empty; public string Phone { get; set; } = string.Empty; public DateTime DateOfBirth { get; set; } } public class Product { public int Id { get; set; } public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public decimal Price { get; set; } public int StockQuantity { get; set; } public string Category { get; set; } = string.Empty; public List<string> Tags { get; set; } = new(); public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime? UpdatedAt { get; set; } public List<OrderItem> OrderItems { get; set; } = new(); } public class Order { public int Id { get; set; } public int UserId { get; set; } public User User { get; set; } = null!; public OrderStatus Status { get; set; } = OrderStatus.Pending; public decimal TotalAmount { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime? UpdatedAt { get; set; } public List<OrderItem> Items { get; set; } = new(); } public class OrderItem { public int Id { get; set; } public int OrderId { get; set; } public Order Order { get; set; } = null!; public int ProductId { get; set; } public Product Product { get; set; } = null!; public int Quantity { get; set; } public decimal UnitPrice { get; set; } public decimal TotalPrice => Quantity * UnitPrice; } public enum OrderStatus { Pending, Confirmed, Shipped, Delivered, Cancelled } }
Database Context
Data/ApplicationDbContext.cs
using GraphQLDemo.Models; namespace GraphQLDemo.Data { public class ApplicationDbContext { public List<User> Users { get; set; } = new(); public List<Product> Products { get; set; } = new(); public List<Order> Orders { get; set; } = new(); public List<UserProfile> Profiles { get; set; } = new(); public ApplicationDbContext() { SeedData(); } private void SeedData() { // Seed Users Users.AddRange(new[] { new User { Id = 1, Username = "john_doe", Email = "john@example.com", FirstName = "John", LastName = "Doe" }, new User { Id = 2, Username = "jane_smith", Email = "jane@example.com", FirstName = "Jane", LastName = "Smith" }, new User { Id = 3, Username = "bob_wilson", Email = "bob@example.com", FirstName = "Bob", LastName = "Wilson" } }); // Seed Products Products.AddRange(new[] { new Product { Id = 1, Name = "Laptop", Description = "High-performance laptop", Price = 999.99m, StockQuantity = 10, Category = "Electronics", Tags = new List<string> { "tech", "computing" } }, new Product { Id = 2, Name = "Smartphone", Description = "Latest smartphone", Price = 699.99m, StockQuantity = 25, Category = "Electronics", Tags = new List<string> { "mobile", "tech" } }, new Product { Id = 3, Name = "Headphones", Description = "Noise-cancelling headphones", Price = 199.99m, StockQuantity = 50, Category = "Electronics", Tags = new List<string> { "audio", "music" } }, new Product { Id = 4, Name = "Book", Description = "Programming guide", Price = 29.99m, StockQuantity = 100, Category = "Books", Tags = new List<string> { "education", "programming" } } }); // Seed Profiles Profiles.AddRange(new[] { new UserProfile { Id = 1, UserId = 1, Address = "123 Main St", City = "New York", Country = "USA", Phone = "+1-555-0101", DateOfBirth = new DateTime(1985, 5, 15) }, new UserProfile { Id = 2, UserId = 2, Address = "456 Oak Ave", City = "Los Angeles", Country = "USA", Phone = "+1-555-0102", DateOfBirth = new DateTime(1990, 8, 22) } }); // Seed Orders Orders.AddRange(new[] { new Order { Id = 1, UserId = 1, Status = OrderStatus.Confirmed, TotalAmount = 1699.98m, Items = new List<OrderItem> { new OrderItem { Id = 1, OrderId = 1, ProductId = 1, Quantity = 1, UnitPrice = 999.99m }, new OrderItem { Id = 2, OrderId = 1, ProductId = 3, Quantity = 1, UnitPrice = 199.99m } } }, new Order { Id = 2, UserId = 2, Status = OrderStatus.Shipped, TotalAmount = 699.99m, Items = new List<OrderItem> { new OrderItem { Id = 3, OrderId = 2, ProductId = 2, Quantity = 1, UnitPrice = 699.99m } } } }); } } }
4. GraphQL Schema Design {#schema-design}
Object Types & Resolvers
Types/UserType.cs
using GraphQLDemo.Models; using HotChocolate; using HotChocolate.Types; namespace GraphQLDemo.Types { public class UserType : ObjectType<User> { protected override void Configure(IObjectTypeDescriptor<User> descriptor) { descriptor.Description("Represents a user in the system"); descriptor .Field(u => u.Id) .Description("The unique identifier of the user"); descriptor .Field(u => u.Username) .Description("The username for login"); descriptor .Field(u => u.Email) .Description("The email address of the user"); descriptor .Field(u => u.FullName) .Description("The full name of the user") .Resolve(context => $"{context.Parent<User>().FirstName} {context.Parent<User>().LastName}"); descriptor .Field(u => u.Orders) .Description("The orders placed by this user") .UseFiltering() .UseSorting(); descriptor .Field(u => u.Profile) .Description("The profile information of the user"); } } public class UserProfileType : ObjectType<UserProfile> { protected override void Configure(IObjectTypeDescriptor<UserProfile> descriptor) { descriptor.Description("Represents user profile information"); descriptor .Field(p => p.User) .Description("The user this profile belongs to") .ResolveWith<Resolvers>(r => r.GetUser(default!, default!)) .UseDbContext<ApplicationDbContext>(); } } // Extension method for computed field public static class UserExtensions { public static string FullName(this User user) { return $"{user.FirstName} {user.LastName}"; } } }
Types/ProductType.cs
using GraphQLDemo.Models; using HotChocolate.Types; namespace GraphQLDemo.Types { public class ProductType : ObjectType<Product> { protected override void Configure(IObjectTypeDescriptor<Product> descriptor) { descriptor.Description("Represents a product in the catalog"); descriptor .Field(p => p.Id) .Description("The unique identifier of the product"); descriptor .Field(p => p.Name) .Description("The name of the product"); descriptor .Field(p => p.Price) .Description("The price of the product") .Type<DecimalType>(); descriptor .Field(p => p.InStock) .Description("Whether the product is in stock") .Resolve(context => context.Parent<Product>().StockQuantity > 0); descriptor .Field(p => p.Category) .Description("The category of the product"); descriptor .Field(p => p.Tags) .Description("The tags associated with the product") .Type<ListType<StringType>>(); descriptor .Field("discountedPrice") .Argument("discount", a => a.Type<DecimalType>().DefaultValue(0)) .Type<DecimalType>() .Resolve(context => { var product = context.Parent<Product>(); var discount = context.ArgumentValue<decimal>("discount"); return product.Price * (1 - discount / 100); }); } } // Extension method for Product public static class ProductExtensions { public static bool InStock(this Product product) { return product.StockQuantity > 0; } } }
5. Queries & Resolvers {#queries-resolvers}
Root Query Type
Query.cs
using GraphQLDemo.Models; using GraphQLDemo.Services; using HotChocolate; using HotChocolate.Data; using HotChocolate.Types; namespace GraphQLDemo { public class Query { [UseFiltering] [UseSorting] [UseProjection] public IQueryable<User> GetUsers([Service] IUserService userService) { return userService.GetUsers(); } public async Task<User?> GetUserByIdAsync( int id, [Service] IUserService userService) { return await userService.GetUserByIdAsync(id); } [UseFiltering] [UseSorting] [UseProjection] public IQueryable<Product> GetProducts([Service] IProductService productService) { return productService.GetProducts(); } public async Task<Product?> GetProductByIdAsync( int id, [Service] IProductService productService) { return await productService.GetProductByIdAsync(id); } [UseFiltering] [UseSorting] public IQueryable<Order> GetOrders([Service] IOrderService orderService) { return orderService.GetOrders(); } public async Task<Order?> GetOrderByIdAsync( int id, [Service] IOrderService orderService) { return await orderService.GetOrderByIdAsync(id); } // Advanced query with multiple parameters public IQueryable<Product> SearchProducts( [Service] IProductService productService, string? category = null, decimal? minPrice = null, decimal? maxPrice = null, string? searchTerm = null) { var query = productService.GetProducts(); if (!string.IsNullOrEmpty(category)) query = query.Where(p => p.Category == category); if (minPrice.HasValue) query = query.Where(p => p.Price >= minPrice.Value); if (maxPrice.HasValue) query = query.Where(p => p.Price <= maxPrice.Value); if (!string.IsNullOrEmpty(searchTerm)) { query = query.Where(p => p.Name.Contains(searchTerm) || p.Description.Contains(searchTerm) || p.Tags.Any(t => t.Contains(searchTerm))); } return query; } // Complex query with related data public async Task<UserOrdersResponse?> GetUserOrdersAsync( int userId, [Service] IUserService userService, [Service] IOrderService orderService) { var user = await userService.GetUserByIdAsync(userId); if (user == null) return null; var orders = orderService.GetOrders().Where(o => o.UserId == userId); return new UserOrdersResponse { User = user, Orders = orders.ToList(), TotalOrders = orders.Count(), TotalSpent = orders.Sum(o => o.TotalAmount) }; } } public class UserOrdersResponse { public User User { get; set; } = null!; public List<Order> Orders { get; set; } = new(); public int TotalOrders { get; set; } public decimal TotalSpent { get; set; } } }
Service Layer Implementation
Services/IUserService.cs
using GraphQLDemo.Models; using GraphQLDemo.Data; using Microsoft.EntityFrameworkCore; namespace GraphQLDemo.Services { public interface IUserService { IQueryable<User> GetUsers(); Task<User?> GetUserByIdAsync(int id); Task<User> CreateUserAsync(CreateUserInput input); Task<User?> UpdateUserAsync(int id, UpdateUserInput input); Task<bool> DeleteUserAsync(int id); } public class UserService : IUserService { private readonly ApplicationDbContext _context; public UserService(ApplicationDbContext context) { _context = context; } public IQueryable<User> GetUsers() { return _context.Users.AsQueryable(); } public async Task<User?> GetUserByIdAsync(int id) { return await Task.FromResult(_context.Users.FirstOrDefault(u => u.Id == id)); } public async Task<User> CreateUserAsync(CreateUserInput input) { var user = new User { Id = _context.Users.Count + 1, Username = input.Username, Email = input.Email, FirstName = input.FirstName, LastName = input.LastName, CreatedAt = DateTime.UtcNow }; _context.Users.Add(user); return await Task.FromResult(user); } public async Task<User?> UpdateUserAsync(int id, UpdateUserInput input) { var user = _context.Users.FirstOrDefault(u => u.Id == id); if (user == null) return null; if (!string.IsNullOrEmpty(input.Username)) user.Username = input.Username; if (!string.IsNullOrEmpty(input.Email)) user.Email = input.Email; if (!string.IsNullOrEmpty(input.FirstName)) user.FirstName = input.FirstName; if (!string.IsNullOrEmpty(input.LastName)) user.LastName = input.LastName; user.UpdatedAt = DateTime.UtcNow; return await Task.FromResult(user); } public async Task<bool> DeleteUserAsync(int id) { var user = _context.Users.FirstOrDefault(u => u.Id == id); if (user == null) return false; _context.Users.Remove(user); return await Task.FromResult(true); } } public interface IProductService { IQueryable<Product> GetProducts(); Task<Product?> GetProductByIdAsync(int id); Task<Product> CreateProductAsync(CreateProductInput input); Task<Product?> UpdateProductAsync(int id, UpdateProductInput input); Task<bool> DeleteProductAsync(int id); } public class ProductService : IProductService { private readonly ApplicationDbContext _context; public ProductService(ApplicationDbContext context) { _context = context; } public IQueryable<Product> GetProducts() { return _context.Products.AsQueryable(); } public async Task<Product?> GetProductByIdAsync(int id) { return await Task.FromResult(_context.Products.FirstOrDefault(p => p.Id == id)); } public async Task<Product> CreateProductAsync(CreateProductInput input) { var product = new Product { Id = _context.Products.Count + 1, Name = input.Name, Description = input.Description, Price = input.Price, StockQuantity = input.StockQuantity, Category = input.Category, Tags = input.Tags, CreatedAt = DateTime.UtcNow }; _context.Products.Add(product); return await Task.FromResult(product); } public async Task<Product?> UpdateProductAsync(int id, UpdateProductInput input) { var product = _context.Products.FirstOrDefault(p => p.Id == id); if (product == null) return null; if (!string.IsNullOrEmpty(input.Name)) product.Name = input.Name; if (!string.IsNullOrEmpty(input.Description)) product.Description = input.Description; if (input.Price.HasValue) product.Price = input.Price.Value; if (input.StockQuantity.HasValue) product.StockQuantity = input.StockQuantity.Value; if (!string.IsNullOrEmpty(input.Category)) product.Category = input.Category; if (input.Tags != null) product.Tags = input.Tags; product.UpdatedAt = DateTime.UtcNow; return await Task.FromResult(product); } public async Task<bool> DeleteProductAsync(int id) { var product = _context.Products.FirstOrDefault(p => p.Id == id); if (product == null) return false; _context.Products.Remove(product); return await Task.FromResult(true); } } }
6. Mutations & Input Types {#mutations-input}
Mutation Types
Mutation.cs
using GraphQLDemo.Models; using GraphQLDemo.Services; using HotChocolate; using HotChocolate.Types; namespace GraphQLDemo { public class Mutation { public async Task<UserPayload> CreateUserAsync( CreateUserInput input, [Service] IUserService userService) { try { var user = await userService.CreateUserAsync(input); return new UserPayload { User = user, Success = true }; } catch (Exception ex) { return new UserPayload { Error = ex.Message, Success = false }; } } public async Task<UserPayload> UpdateUserAsync( int id, UpdateUserInput input, [Service] IUserService userService) { try { var user = await userService.UpdateUserAsync(id, input); if (user == null) return new UserPayload { Error = $"User with ID {id} not found", Success = false }; return new UserPayload { User = user, Success = true }; } catch (Exception ex) { return new UserPayload { Error = ex.Message, Success = false }; } } public async Task<DeletePayload> DeleteUserAsync( int id, [Service] IUserService userService) { try { var result = await userService.DeleteUserAsync(id); return new DeletePayload { Success = result, Message = result ? "User deleted" : "User not found" }; } catch (Exception ex) { return new DeletePayload { Success = false, Message = ex.Message }; } } public async Task<ProductPayload> CreateProductAsync( CreateProductInput input, [Service] IProductService productService) { try { var product = await productService.CreateProductAsync(input); return new ProductPayload { Product = product, Success = true }; } catch (Exception ex) { return new ProductPayload { Error = ex.Message, Success = false }; } } public async Task<OrderPayload> CreateOrderAsync( CreateOrderInput input, [Service] IOrderService orderService) { try { var order = await orderService.CreateOrderAsync(input); return new OrderPayload { Order = order, Success = true }; } catch (Exception ex) { return new OrderPayload { Error = ex.Message, Success = false }; } } public async Task<OrderPayload> UpdateOrderStatusAsync( int orderId, OrderStatus status, [Service] IOrderService orderService) { try { var order = await orderService.UpdateOrderStatusAsync(orderId, status); if (order == null) return new OrderPayload { Error = $"Order with ID {orderId} not found", Success = false }; return new OrderPayload { Order = order, Success = true }; } catch (Exception ex) { return new OrderPayload { Error = ex.Message, Success = false }; } } } // Input Types public record CreateUserInput( string Username, string Email, string FirstName, string LastName); public record UpdateUserInput( string? Username = null, string? Email = null, string? FirstName = null, string? LastName = null); public record CreateProductInput( string Name, string Description, decimal Price, int StockQuantity, string Category, List<string> Tags); public record UpdateProductInput( string? Name = null, string? Description = null, decimal? Price = null, int? StockQuantity = null, string? Category = null, List<string>? Tags = null); public record CreateOrderInput( int UserId, List<OrderItemInput> Items); public record OrderItemInput( int ProductId, int Quantity); // Payload Types public class UserPayload { public User? User { get; set; } public bool Success { get; set; } public string? Error { get; set; } } public class ProductPayload { public Product? Product { get; set; } public bool Success { get; set; } public string? Error { get; set; } } public class OrderPayload { public Order? Order { get; set; } public bool Success { get; set; } public string? Error { get; set; } } public class DeletePayload { public bool Success { get; set; } public string Message { get; set; } = string.Empty; } }
7. Real-Time Subscriptions {#subscriptions}
Subscription Implementation
Subscription.cs
using GraphQLDemo.Models; using GraphQLDemo.Services; using HotChocolate; using HotChocolate.Subscriptions; namespace GraphQLDemo { public class Subscription { [Subscribe] public Order OnOrderCreated( [EventMessage] Order order) { return order; } [Subscribe] public Product OnProductUpdated( [EventMessage] Product product) { return product; } [Subscribe] public User OnUserRegistered( [EventMessage] User user) { return user; } [Subscribe] public OrderStatusUpdate OnOrderStatusChanged( [EventMessage] OrderStatusUpdate statusUpdate) { return statusUpdate; } [Subscribe] public ProductStockUpdate OnProductStockChanged( [EventMessage] ProductStockUpdate stockUpdate) { return stockUpdate; } } public class OrderStatusUpdate { public int OrderId { get; set; } public OrderStatus OldStatus { get; set; } public OrderStatus NewStatus { get; set; } public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; } public class ProductStockUpdate { public int ProductId { get; set; } public string ProductName { get; set; } = string.Empty; public int OldStock { get; set; } public int NewStock { get; set; } public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; } // Enhanced Order Service with Event Publishing public class OrderService : IOrderService { private readonly ApplicationDbContext _context; private readonly ITopicEventSender _eventSender; public OrderService(ApplicationDbContext context, ITopicEventSender eventSender) { _context = context; _eventSender = eventSender; } public IQueryable<Order> GetOrders() { return _context.Orders.AsQueryable(); } public async Task<Order?> GetOrderByIdAsync(int id) { return await Task.FromResult(_context.Orders.FirstOrDefault(o => o.Id == id)); } public async Task<Order> CreateOrderAsync(CreateOrderInput input) { // Calculate total amount decimal totalAmount = 0; var items = new List<OrderItem>(); var itemId = 1; foreach (var itemInput in input.Items) { var product = _context.Products.FirstOrDefault(p => p.Id == itemInput.ProductId); if (product == null) throw new ArgumentException($"Product with ID {itemInput.ProductId} not found"); if (product.StockQuantity < itemInput.Quantity) throw new InvalidOperationException($"Insufficient stock for product {product.Name}"); var orderItem = new OrderItem { Id = itemId++, ProductId = itemInput.ProductId, Quantity = itemInput.Quantity, UnitPrice = product.Price }; items.Add(orderItem); totalAmount += orderItem.TotalPrice; // Update product stock product.StockQuantity -= itemInput.Quantity; } var order = new Order { Id = _context.Orders.Count + 1, UserId = input.UserId, Status = OrderStatus.Pending, TotalAmount = totalAmount, CreatedAt = DateTime.UtcNow, Items = items }; _context.Orders.Add(order); // Publish subscription event await _eventSender.SendAsync(nameof(Subscription.OnOrderCreated), order); return await Task.FromResult(order); } public async Task<Order?> UpdateOrderStatusAsync(int orderId, OrderStatus newStatus) { var order = _context.Orders.FirstOrDefault(o => o.Id == orderId); if (order == null) return null; var oldStatus = order.Status; order.Status = newStatus; order.UpdatedAt = DateTime.UtcNow; // Publish status change event var statusUpdate = new OrderStatusUpdate { OrderId = orderId, OldStatus = oldStatus, NewStatus = newStatus }; await _eventSender.SendAsync(nameof(Subscription.OnOrderStatusChanged), statusUpdate); return await Task.FromResult(order); } } }
8. DataLoader & Performance {#dataloader-performance}
Batch Data Loading
DataLoaders/UserDataLoader.cs
using GraphQLDemo.Models; using GraphQLDemo.Data; using GreenDonut; using Microsoft.EntityFrameworkCore; namespace GraphQLDemo.DataLoaders { public class UserByIdDataLoader : BatchDataLoader<int, User> { private readonly ApplicationDbContext _context; public UserByIdDataLoader( ApplicationDbContext context, IBatchScheduler batchScheduler, DataLoaderOptions? options = null) : base(batchScheduler, options) { _context = context; } protected override async Task<IReadOnlyDictionary<int, User>> LoadBatchAsync( IReadOnlyList<int> keys, CancellationToken cancellationToken) { var users = await _context.Users .Where(u => keys.Contains(u.Id)) .ToDictionaryAsync(u => u.Id, cancellationToken); return users; } } public class ProductByIdDataLoader : BatchDataLoader<int, Product> { private readonly ApplicationDbContext _context; public ProductByIdDataLoader( ApplicationDbContext context, IBatchScheduler batchScheduler, DataLoaderOptions? options = null) : base(batchScheduler, options) { _context = context; } protected override async Task<IReadOnlyDictionary<int, Product>> LoadBatchAsync( IReadOnlyList<int> keys, CancellationToken cancellationToken) { var products = await _context.Products .Where(p => keys.Contains(p.Id)) .ToDictionaryAsync(p => p.Id, cancellationToken); return products; } } public class OrderByUserIdDataLoader : GroupedDataLoader<int, Order> { private readonly ApplicationDbContext _context; public OrderByUserIdDataLoader( ApplicationDbContext context, IBatchScheduler batchScheduler, DataLoaderOptions? options = null) : base(batchScheduler, options) { _context = context; } protected override async Task<ILookup<int, Order>> LoadGroupedBatchAsync( IReadOnlyList<int> keys, CancellationToken cancellationToken) { var orders = await _context.Orders .Where(o => keys.Contains(o.UserId)) .ToListAsync(cancellationToken); return orders.ToLookup(o => o.UserId); } } } // Enhanced Types with DataLoaders public class OrderType : ObjectType<Order> { protected override void Configure(IObjectTypeDescriptor<Order> descriptor) { descriptor.Description("Represents an order in the system"); descriptor .Field(o => o.User) .Description("The user who placed the order") .ResolveWith<Resolvers>(r => r.GetUserAsync(default!, default!, default!)); descriptor .Field(o => o.Items) .Description("The items in the order") .ResolveWith<Resolvers>(r => r.GetOrderItemsAsync(default!, default!, default!)); } private class Resolvers { public async Task<User> GetUserAsync( Order order, UserByIdDataLoader userDataLoader, CancellationToken cancellationToken) { return await userDataLoader.LoadAsync(order.UserId, cancellationToken); } public async Task<IEnumerable<OrderItem>> GetOrderItemsAsync( Order order, ProductByIdDataLoader productDataLoader, CancellationToken cancellationToken) { // Load products for all order items var productIds = order.Items.Select(i => i.ProductId).Distinct(); await productDataLoader.LoadAsync(productIds, cancellationToken); return order.Items; } } }
9. Filtering & Sorting {#filtering-sorting}
Advanced Filtering Implementation
Types/FilterTypes.cs
using GraphQLDemo.Models; using HotChocolate.Data; namespace GraphQLDemo.Types { public class ProductFilterType : FilterInputType<Product> { protected override void Configure(IFilterInputTypeDescriptor<Product> descriptor) { descriptor.BindFieldsExplicitly(); descriptor.Field(p => p.Id); descriptor.Field(p => p.Name).Ignore(); descriptor.Field(p => p.Price); descriptor.Field(p => p.StockQuantity); descriptor.Field(p => p.Category); descriptor.Field(p => p.Tags); descriptor.Field(p => p.CreatedAt); // Custom filter for name (contains search) descriptor.Field("name_contains") .Type<StringOperationFilterInputType>() .Description("Filters products where name contains the given string") .Extend() .OnBeforeCreate(x => x.Handler = (y, z) => { if (y is string searchTerm) { z.Where(p => p.Name.Contains(searchTerm)); } }); // Price range filter descriptor.Field("price_range") .Type<PriceRangeFilterInputType>() .Description("Filters products within a price range") .Extend() .OnBeforeCreate(x => x.Handler = (y, z) => { if (y is PriceRange range) { z.Where(p => p.Price >= range.Min && p.Price <= range.Max); } }); // In stock filter descriptor.Field("in_stock") .Type<BooleanType>() .Description("Filters products that are in stock") .Extend() .OnBeforeCreate(x => x.Handler = (y, z) => { if (y is bool inStock) { z.Where(p => p.StockQuantity > 0 == inStock); } }); } } public class PriceRangeFilterInputType : InputObjectType<PriceRange> { protected override void Configure(IInputObjectTypeDescriptor<PriceRange> descriptor) { descriptor.Field(r => r.Min).Type<DecimalType>(); descriptor.Field(r => r.Max).Type<DecimalType>(); } } public class PriceRange { public decimal Min { get; set; } public decimal Max { get; set; } } public class ProductSortType : SortInputType<Product> { protected override void Configure(ISortInputTypeDescriptor<Product> descriptor) { descriptor.BindFieldsExplicitly(); descriptor.Field(p => p.Name).Description("Sort by product name"); descriptor.Field(p => p.Price).Description("Sort by product price"); descriptor.Field(p => p.StockQuantity).Description("Sort by stock quantity"); descriptor.Field(p => p.Category).Description("Sort by category"); descriptor.Field(p => p.CreatedAt).Description("Sort by creation date"); // Custom sort by popularity (would need additional field) descriptor.Field("popularity") .Description("Sort by product popularity") .Extend() .OnBeforeCreate(x => x.Handler = (y, z) => { // This would require a popularity field in the Product model // z.OrderBy(p => p.Popularity); }); } } } // Enhanced Query with Filtering and Sorting public class EnhancedQuery { [UseFiltering(typeof(ProductFilterType))] [UseSorting(typeof(ProductSortType))] public IQueryable<Product> GetFilteredProducts([Service] IProductService productService) { return productService.GetProducts(); } [UsePaging(IncludeTotalCount = true, MaxPageSize = 50)] [UseFiltering] [UseSorting] public IQueryable<User> GetUsersPaged([Service] IUserService userService) { return userService.GetUsers(); } [UseOffsetPaging(IncludeTotalCount = true)] [UseFiltering] [UseSorting] public IQueryable<Order> GetOrdersPaged([Service] IOrderService orderService) { return orderService.GetOrders(); } }
10. Authentication & Authorization {#authentication}
GraphQL Security Implementation
Authentication/GraphQLAuth.cs
using System.Security.Claims; using HotChocolate.AspNetCore; using HotChocolate.AspNetCore.Authorization; using Microsoft.AspNetCore.Authentication.JwtBearer; namespace GraphQLDemo.Authentication { public class GraphQLAuth { // Custom authorization handler public class CustomAuthorizationHandler : DefaultHttpRequestInterceptor { public override ValueTask OnCreateAsync( HttpContext context, IRequestExecutor requestExecutor, IQueryRequestBuilder requestBuilder, CancellationToken cancellationToken) { // Extract user from context and add to GraphQL context if (context.User.Identity?.IsAuthenticated == true) { requestBuilder.SetProperty("User", context.User); } return base.OnCreateAsync(context, requestExecutor, requestBuilder, cancellationToken); } } } // Authorization directives public static class AuthDirectives { public const string Admin = "admin"; public const string User = "user"; public const string Authenticated = "authenticated"; } // Custom error filter for authorization public class AuthErrorFilter : IErrorFilter { public IError OnError(IError error) { if (error.Exception is UnauthorizedAccessException) { return error.WithCode("UNAUTHORIZED") .WithMessage("You are not authorized to perform this action."); } return error; } } } // Secure Mutations with Authorization public class SecureMutation { [Authorize(AuthDirectives.Admin)] public async Task<ProductPayload> CreateProductAdminAsync( CreateProductInput input, [Service] IProductService productService, [GlobalState("User")] ClaimsPrincipal user) { // Only admins can create products var product = await productService.CreateProductAsync(input); return new ProductPayload { Product = product, Success = true }; } [Authorize(AuthDirectives.Authenticated)] public async Task<OrderPayload> CreateOrderAsync( CreateOrderInput input, [Service] IOrderService orderService, [GlobalState("User")] ClaimsPrincipal user) { // Users can only create orders for themselves var userId = int.Parse(user.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "0"); if (input.UserId != userId) { throw new UnauthorizedAccessException("You can only create orders for yourself"); } var order = await orderService.CreateOrderAsync(input); return new OrderPayload { Order = order, Success = true }; } [Authorize(AuthDirectives.Admin)] public async Task<UserPayload> UpdateUserAdminAsync( int id, UpdateUserInput input, [Service] IUserService userService) { var user = await userService.UpdateUserAsync(id, input); if (user == null) return new UserPayload { Error = $"User with ID {id} not found", Success = false }; return new UserPayload { User = user, Success = true }; } } // Program.cs with Authentication public class ProgramWithAuth { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add JWT Authentication builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Authority = builder.Configuration["Auth0:Authority"]; options.Audience = builder.Configuration["Auth0:Audience"]; }); builder.Services.AddAuthorization(); // Add GraphQL with Authorization builder.Services .AddGraphQLServer() .AddQueryType<Query>() .AddMutationType<SecureMutation>() .AddSubscriptionType<Subscription>() .AddAuthorization() .AddHttpRequestInterceptor<GraphQLAuth.CustomAuthorizationHandler>() .AddErrorFilter<AuthErrorFilter>(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapGraphQL(); app.Run(); } }
11. Error Handling & Validation {#error-handling}
Comprehensive Error Management
ErrorHandling/CustomErrorFilter.cs
using FluentValidation; using HotChocolate; namespace GraphQLDemo.ErrorHandling { public class CustomErrorFilter : IErrorFilter { public IError OnError(IError error) { // Handle specific exception types return error.Exception switch { ValidationException validationEx => HandleValidationException(error, validationEx), ArgumentException argEx => HandleArgumentException(error, argEx), UnauthorizedAccessException authEx => HandleUnauthorizedException(error, authEx), KeyNotFoundException notFoundEx => HandleNotFoundException(error, notFoundEx), _ => HandleGenericError(error) }; } private IError HandleValidationException(IError error, ValidationException ex) { var errors = ex.Errors.Select(e => new ErrorProperty { Name = e.PropertyName, Value = e.ErrorMessage }); return error .WithCode("VALIDATION_ERROR") .WithMessage("One or more validation errors occurred.") .SetExtension("validationErrors", errors); } private IError HandleArgumentException(IError error, ArgumentException ex) { return error .WithCode("INVALID_ARGUMENT") .WithMessage(ex.Message) .RemoveException(); } private IError HandleUnauthorizedException(IError error, UnauthorizedAccessException ex) { return error .WithCode("UNAUTHORIZED") .WithMessage("You are not authorized to perform this action.") .RemoveException(); } private IError HandleNotFoundException(IError error, KeyNotFoundException ex) { return error .WithCode("NOT_FOUND") .WithMessage("The requested resource was not found.") .RemoveException(); } private IError HandleGenericError(IError error) { // Don't expose internal errors in production if (IsDevelopment()) { return error; } return error .WithCode("INTERNAL_ERROR") .WithMessage("An internal error occurred.") .RemoveException() .RemoveLocations() .RemovePath(); } private bool IsDevelopment() { return Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development"; } } public class ErrorProperty { public string Name { get; set; } = string.Empty; public string Value { get; set; } = string.Empty; } // Custom Exceptions public class GraphQLDomainException : Exception { public string Code { get; } public GraphQLDomainException(string code, string message) : base(message) { Code = code; } } public class InsufficientStockException : GraphQLDomainException { public InsufficientStockException(string productName, int requested, int available) : base("INSUFFICIENT_STOCK", $"Insufficient stock for {productName}. Requested: {requested}, Available: {available}") { } } public class ProductNotFoundException : GraphQLDomainException { public ProductNotFoundException(int productId) : base("PRODUCT_NOT_FOUND", $"Product with ID {productId} was not found.") { } } } // Validation with FluentValidation public class CreateUserInputValidator : AbstractValidator<CreateUserInput> { public CreateUserInputValidator() { RuleFor(x => x.Username) .NotEmpty().WithMessage("Username is required") .Length(3, 50).WithMessage("Username must be between 3 and 50 characters") .Matches("^[a-zA-Z0-9_]+$").WithMessage("Username can only contain letters, numbers, and underscores"); RuleFor(x => x.Email) .NotEmpty().WithMessage("Email is required") .EmailAddress().WithMessage("A valid email address is required"); RuleFor(x => x.FirstName) .NotEmpty().WithMessage("First name is required") .MaximumLength(100).WithMessage("First name cannot exceed 100 characters"); RuleFor(x => x.LastName) .NotEmpty().WithMessage("Last name is required") .MaximumLength(100).WithMessage("Last name cannot exceed 100 characters"); } } public class CreateProductInputValidator : AbstractValidator<CreateProductInput> { public CreateProductInputValidator() { RuleFor(x => x.Name) .NotEmpty().WithMessage("Product name is required") .MaximumLength(100).WithMessage("Product name cannot exceed 100 characters"); RuleFor(x => x.Price) .GreaterThan(0).WithMessage("Price must be greater than 0") .LessThan(1000000).WithMessage("Price must be less than 1,000,000"); RuleFor(x => x.StockQuantity) .GreaterThanOrEqualTo(0).WithMessage("Stock quantity cannot be negative"); RuleFor(x => x.Category) .NotEmpty().WithMessage("Category is required") .MaximumLength(50).WithMessage("Category cannot exceed 50 characters"); RuleForEach(x => x.Tags) .MaximumLength(30).WithMessage("Tag cannot exceed 30 characters"); } } // Validation Middleware public class ValidationMiddleware { private readonly FieldDelegate _next; public ValidationMiddleware(FieldDelegate next) { _next = next; } public async Task InvokeAsync(IMiddlewareContext context) { // Validate inputs before executing resolver if (context.Selection.Field.Arguments.Any()) { foreach (var argument in context.Selection.Field.Arguments) { var value = context.ArgumentValue<object?>(argument.Name); // Add custom validation logic here } } await _next(context); } }
12. Advanced Resolver Patterns {#resolver-patterns}
Complex Resolver Implementations
Resolvers/AdvancedResolvers.cs
using GraphQLDemo.Models; using GraphQLDemo.Services; using HotChocolate; using HotChocolate.Resolvers; using HotChocolate.Types; namespace GraphQLDemo.Resolvers { public class AdvancedResolvers { // Field Resolver with Business Logic public static async Task<decimal> GetOrderDiscountAsync( [Parent] Order order, [Service] IDiscountService discountService) { return await discountService.CalculateDiscountAsync(order); } // Complex Field with Multiple Dependencies public static async Task<OrderSummary> GetOrderSummaryAsync( [Parent] Order order, [Service] IOrderService orderService, [Service] IProductService productService) { var items = order.Items; var productIds = items.Select(i => i.ProductId).Distinct(); var products = await productService.GetProductsByIdsAsync(productIds); var summary = new OrderSummary { OrderId = order.Id, TotalItems = items.Sum(i => i.Quantity), UniqueProducts = items.Select(i => i.ProductId).Distinct().Count(), EstimatedDelivery = order.CreatedAt.AddDays(3), Products = products.ToList() }; return summary; } // Batch Resolver for Performance public static async Task<Dictionary<int, UserProfile>> GetUserProfilesAsync( [Parent] IReadOnlyList<User> users, [Service] IUserService userService) { var userIds = users.Select(u => u.Id).ToList(); return await userService.GetProfilesByUserIdsAsync(userIds); } // Conditional Resolver public static async Task<object?> GetUserPrivateDataAsync( [Parent] User user, [Service] IUserService userService, [GlobalState("User")] ClaimsPrincipal currentUser) { var currentUserId = int.Parse(currentUser.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "0"); // Users can only see their own private data if (currentUserId != user.Id && !currentUser.IsInRole("Admin")) { return null; } return await userService.GetPrivateDataAsync(user.Id); } } public class OrderSummary { public int OrderId { get; set; } public int TotalItems { get; set; } public int UniqueProducts { get; set; } public DateTime EstimatedDelivery { get; set; } public List<Product> Products { get; set; } = new(); public decimal Subtotal => Products.Sum(p => p.Price); } // Custom Directive public class UpperCaseDirectiveType : DirectiveType { protected override void Configure(IDirectiveTypeDescriptor descriptor) { descriptor.Name("upperCase"); descriptor.Location(DirectiveLocation.FieldDefinition); descriptor.Use(next => async context => { await next(context); if (context.Result is string str) { context.Result = str.ToUpperInvariant(); } }); } } // Custom Middleware public class LoggingMiddleware { private readonly FieldDelegate _next; private readonly ILogger<LoggingMiddleware> _logger; public LoggingMiddleware(FieldDelegate next, ILogger<LoggingMiddleware> logger) { _next = next; _logger = logger; } public async Task InvokeAsync(IMiddlewareContext context) { var fieldName = context.Selection.Field.Name; var startTime = DateTime.UtcNow; _logger.LogInformation("Executing field: {FieldName}", fieldName); try { await _next(context); var duration = DateTime.UtcNow - startTime; _logger.LogInformation("Field {FieldName} executed in {Duration}ms", fieldName, duration.TotalMilliseconds); } catch (Exception ex) { _logger.LogError(ex, "Error executing field: {FieldName}", fieldName); throw; } } } } // Enhanced Types with Advanced Resolvers public class EnhancedUserType : ObjectType<User> { protected override void Configure(IObjectTypeDescriptor<User> descriptor) { descriptor.Field("discountEligibility") .Type<BooleanType>() .ResolveWith<Resolvers>(r => r.GetDiscountEligibilityAsync(default!, default!)); descriptor.Field("orderHistory") .Type<ListType<OrderType>>() .Argument("status", a => a.Type<OrderStatusType>()) .ResolveWith<Resolvers>(r => r.GetUserOrdersByStatusAsync(default!, default!, default!)); descriptor.Field("recentOrders") .Type<ListType<OrderType>>() .Argument("count", a => a.Type<IntType>().DefaultValue(5)) .ResolveWith<Resolvers>(r => r.GetRecentUserOrdersAsync(default!, default!, default!)); // Apply custom directive descriptor.Field(u => u.Email).Directive("upperCase"); } private class Resolvers { public async Task<bool> GetDiscountEligibilityAsync( [Parent] User user, [Service] IDiscountService discountService) { return await discountService.IsEligibleForDiscountAsync(user); } public async Task<IEnumerable<Order>> GetUserOrdersByStatusAsync( [Parent] User user, [Service] IOrderService orderService, OrderStatus? status) { var query = orderService.GetOrders().Where(o => o.UserId == user.Id); if (status.HasValue) { query = query.Where(o => o.Status == status.Value); } return await Task.FromResult(query.ToList()); } public async Task<IEnumerable<Order>> GetRecentUserOrdersAsync( [Parent] User user, [Service] IOrderService orderService, int count) { return await Task.FromResult( orderService.GetOrders() .Where(o => o.UserId == user.Id) .OrderByDescending(o => o.CreatedAt) .Take(count) .ToList()); } } }
This comprehensive GraphQL guide covers everything from basic setup to advanced enterprise patterns using HotChocolate in ASP.NET Core. The examples provide real-world implementations that you can adapt for your projects.

0 Comments
thanks for your comments!