GraphQL Explosion: HotChocolate ASP.NET Core Complete Guide with Real-World Examples (Part-11 of 40) - FreeLearning365.com

 

GraphQL Explosion: HotChocolate ASP.NET Core Complete Guide with Real-World Examples (Part-11 of 40) - FreeLearning365.com

GraphQL Explosion: Supercharge Your APIs with HotChocolate in ASP.NET Core


Module Sequence: Part 11 of 40 - Intermediate Level Core Development Series

Master GraphQL in ASP.NET Core with HotChocolate. Learn schemas, resolvers, real-time subscriptions, and build client-driven APIs with complete examples.

Table of Contents

  1. Introduction to GraphQL Revolution

  2. Why GraphQL Beats REST

  3. Setting Up HotChocolate

  4. GraphQL Schema Design

  5. Queries & Resolvers

  6. Mutations & Input Types

  7. Real-Time Subscriptions

  8. DataLoader & Performance

  9. Filtering & Sorting

  10. Authentication & Authorization

  11. Error Handling & Validation

  12. Advanced Resolver Patterns

  13. Federation & Microservices

  14. Real-World E-Commerce Implementation

  15. Testing GraphQL APIs

  16. Performance Optimization

  17. Monitoring & Logging

  18. Deployment Strategies

  19. Client Integration

  20. Future of GraphQL

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.

csharp
// 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

csharp
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:

text
GraphQLDemo/
├── Models/
├── Services/
├── Types/
├── Data/
├── Program.cs
└── appsettings.json

Program.cs - Complete HotChocolate Setup

csharp
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

csharp
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

csharp
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

csharp
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

csharp
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

csharp
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

csharp
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

csharp
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

csharp
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

csharp
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

csharp
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

csharp
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

csharp
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

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

Post a Comment

0 Comments