GraphQL

Comparing GraphQL to REST and implementing GraphQL APIs in .NET applications

Introduction to GraphQL

GraphQL is a query language and runtime for APIs that was developed by Facebook in 2012 and open-sourced in 2015. It represents a significant departure from traditional REST APIs by enabling clients to request exactly the data they need, no more and no less. Unlike REST endpoints which return fixed data structures, GraphQL provides a flexible, client-driven approach where the client specifies the exact shape of the response.

Key Concepts

  • Schema Definition Language (SDL): A strongly-typed schema that defines the capabilities of the API
  • Single Endpoint: Unlike REST’s multiple endpoints, GraphQL typically exposes a single endpoint
  • Client-Specified Queries: Clients define the structure of the response
  • Resolvers: Functions that determine how the requested data is retrieved
  • Introspection: Built-in capability to query the schema for documentation

GraphQL vs REST

FeatureGraphQLREST
Number of EndpointsSingle endpointMultiple endpoints
Data FetchingClient specifies dataServer defines responses
VersioningEvolve API without versionsOften requires versioning
Over-fetchingEliminated by designCommon issue
Under-fetchingEliminated by designOften requires multiple requests
CachingRequires additional workBuilt into HTTP
Error HandlingAlways returns 200 with errors in responseUses HTTP status codes
File UploadsRequires special handlingNative support
ToolingRich ecosystem of toolsEstablished standards and tools

When to Use GraphQL Instead of REST

GraphQL is particularly well-suited for:

  1. Complex Domain Models - When your data structure has complex relationships
  2. Mobile Applications - Where bandwidth efficiency is crucial
  3. Aggregation APIs - When combining data from multiple sources
  4. Rapidly Evolving APIs - For frequent changes without versioning
  5. Client-Specific Data Requirements - When different clients need different data
  6. Microservice Architectures - As a gateway to underlying services

Implementing GraphQL in .NET

In the .NET ecosystem, there are several libraries for implementing GraphQL APIs, with Hot Chocolate being one of the most popular and full-featured options.

Setting Up a GraphQL Server with Hot Chocolate

First, install the required packages:

dotnet add package HotChocolate.AspNetCore
dotnet add package HotChocolate.Data.EntityFramework

Defining GraphQL Schema in C#

GraphQL schemas in Hot Chocolate can be defined using code-first or schema-first approaches:

Code-First Schema Example

using HotChocolate.Types;

// Define a type for a book
public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Author { get; set; }
    public int Year { get; set; }
    public int? AuthorId { get; set; }
}

// Define a GraphQL object type
public class BookType : ObjectType<Book>
{
    protected override void Configure(IObjectTypeDescriptor<Book> descriptor)
    {
        descriptor.Field(b => b.Id).Type<NonNullType<IdType>>();
        descriptor.Field(b => b.Title).Type<NonNullType<StringType>>();
        descriptor.Field(b => b.Author).Type<NonNullType<StringType>>();
        descriptor.Field(b => b.Year).Type<NonNullType<IntType>>();
    }
}

// Define a query type
public class Query
{
    public IQueryable<Book> GetBooks([Service] IBookRepository repository)
    {
        return repository.GetBooks();
    }
    
    public Book GetBook([Service] IBookRepository repository, int id)
    {
        return repository.GetBookById(id);
    }
}

Schema-First Approach Example

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services
            .AddGraphQLServer()
            .AddDocumentFromString(@"
                type Book {
                    id: ID!
                    title: String!
                    author: String!
                    year: Int!
                }

                type Query {
                    books: [Book!]!
                    book(id: ID!): Book
                }
            ")
            .BindComplexType<Book>()
            .AddResolver("Query", "books", (context, ct) => 
            {
                IBookRepository repository = context.Service<IBookRepository>();
                return repository.GetBooks();
            })
            .AddResolver("Query", "book", (context, ct) => 
            {
                IBookRepository repository = context.Service<IBookRepository>();
                int id = context.ArgumentValue<int>("id");
                return repository.GetBookById(id);
            });
    }
}

Setting Up in ASP.NET Core

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using HotChocolate.AspNetCore;

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Register your services
        services.AddSingleton<IBookRepository, BookRepository>();
        
        // Add GraphQL services
        services
            .AddGraphQLServer()
            .AddQueryType<Query>()
            .AddMutationType<Mutation>()
            .AddType<BookType>()
            .AddFiltering()
            .AddSorting()
            .AddProjections()
            .AddAuthorization();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();
        
        app.UseEndpoints(endpoints =>
        {
            // Map GraphQL endpoint
            endpoints.MapGraphQL();
            
            // Optionally map Banana Cake Pop UI
            endpoints.MapBananaCakePop("/graphql-ui");
        });
    }
}

Queries

A GraphQL query allows clients to request specific fields from objects:

query {
  books {
    id
    title
    author
  }
}

Here’s how to handle queries in Hot Chocolate:

public class Query
{
    [UseDbContext(typeof(LibraryDbContext))]
    [UsePaging]
    [UseProjection]
    [UseFiltering]
    [UseSorting]
    public IQueryable<Book> GetBooks([ScopedService] LibraryDbContext context)
    {
        return context.Books;
    }
    
    [UseDbContext(typeof(LibraryDbContext))]
    [UseFirstOrDefault]
    [UseProjection]
    public IQueryable<Book> GetBookById([ScopedService] LibraryDbContext context, int id)
    {
        return context.Books.Where(b => b.Id == id);
    }
}

Mutations

Mutations are used to modify data:

mutation {
  addBook(book: { title: "GraphQL in Action", author: "John Smith", year: 2021 }) {
    id
    title
  }
}

Implementation in Hot Chocolate:

public class Mutation
{
    [UseDbContext(typeof(LibraryDbContext))]
    public async Task<Book> AddBookAsync(
        [ScopedService] LibraryDbContext context,
        BookInput input)
    {
        Book book = new Book
        {
            Title = input.Title,
            Author = input.Author,
            Year = input.Year
        };
        
        context.Books.Add(book);
        await context.SaveChangesAsync();
        
        return book;
    }
    
    [UseDbContext(typeof(LibraryDbContext))]
    public async Task<Book> UpdateBookAsync(
        [ScopedService] LibraryDbContext context,
        int id,
        BookInput input)
    {
        Book book = await context.Books.FindAsync(id);
        
        if (book == null)
        {
            throw new GraphQLException("Book not found.");
        }
        
        book.Title = input.Title ?? book.Title;
        book.Author = input.Author ?? book.Author;
        book.Year = input.Year ?? book.Year;
        
        context.Books.Update(book);
        await context.SaveChangesAsync();
        
        return book;
    }
    
    [UseDbContext(typeof(LibraryDbContext))]
    public async Task<bool> DeleteBookAsync(
        [ScopedService] LibraryDbContext context,
        int id)
    {
        Book book = await context.Books.FindAsync(id);
        
        if (book == null)
        {
            return false;
        }
        
        context.Books.Remove(book);
        await context.SaveChangesAsync();
        
        return true;
    }
}

public class BookInput
{
    public string Title { get; set; }
    public string Author { get; set; }
    public int? Year { get; set; }
}

Subscriptions

Subscriptions allow clients to receive real-time updates:

subscription {
  bookAdded {
    id
    title
    author
  }
}

Implementation in Hot Chocolate:

public class Subscription
{
    [Subscribe]
    [Topic]
    public Book BookAdded([EventMessage] Book book)
    {
        return book;
    }
}

// In the Mutation class, publish the event
public class Mutation
{
    [UseDbContext(typeof(LibraryDbContext))]
    public async Task<Book> AddBookAsync(
        [ScopedService] LibraryDbContext context,
        [Service] ITopicEventSender eventSender,
        BookInput input)
    {
        Book book = new Book
        {
            Title = input.Title,
            Author = input.Author,
            Year = input.Year
        };
        
        context.Books.Add(book);
        await context.SaveChangesAsync();
        
        await eventSender.SendAsync(nameof(Subscription.BookAdded), book);
        
        return book;
    }
}

Configure subscriptions in Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services
        .AddGraphQLServer()
        .AddQueryType<Query>()
        .AddMutationType<Mutation>()
        .AddSubscriptionType<Subscription>()
        .AddInMemorySubscriptions(); // Or use Redis for distributed systems
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseWebSockets(); // Enable WebSockets for subscriptions
    
    app.UseRouting();
    
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGraphQL();
    });
}

Advanced GraphQL Features

Data Loader Pattern

GraphQL APIs can often suffer from the N+1 problem. Data Loaders help solve this by batching and caching:

public class Query
{
    public async Task<Book> GetBookAsync(
        int id,
        BookDataLoader dataLoader,
        CancellationToken cancellationToken)
    {
        return await dataLoader.LoadAsync(id, cancellationToken);
    }
}

public class BookDataLoader : BatchDataLoader<int, Book>
{
    private readonly IBookRepository _repository;

    public BookDataLoader(
        IBookRepository repository,
        IBatchScheduler batchScheduler,
        DataLoaderOptions options = null)
        : base(batchScheduler, options)
    {
        _repository = repository;
    }

    protected override async Task<IReadOnlyDictionary<int, Book>> LoadBatchAsync(
        IReadOnlyList<int> keys,
        CancellationToken cancellationToken)
    {
        // Load all books in a single database query
        IEnumerable<Book> books = await _repository.GetBooksByIdsAsync(keys, cancellationToken);
        return books.ToDictionary(b => b.Id);
    }
}

// Register in Startup
services.AddDataLoaderRegistry();

Error Handling and Validation

GraphQL errors can be handled in various ways:

public class Query
{
    public Book GetBook(int id, [Service] IBookRepository repository)
    {
        Book book = repository.GetBookById(id);
        
        if (book == null)
        {
            throw new GraphQLException(
                new Error("Book not found", "BOOK_NOT_FOUND")
                {
                    Extensions =
                    {
                        { "bookId", id }
                    }
                });
        }
        
        return book;
    }
}

Using validation with input objects:

public class BookInputType : InputObjectType<BookInput>
{
    protected override void Configure(IInputObjectTypeDescriptor<BookInput> descriptor)
    {
        descriptor
            .Field(f => f.Title)
            .Type<NonNullType<StringType>>()
            .Validator(validator => validator.MinLength(3).MaxLength(100));
            
        descriptor
            .Field(f => f.Author)
            .Type<NonNullType<StringType>>()
            .Validator(validator => validator.MinLength(2).MaxLength(50));
            
        descriptor
            .Field(f => f.Year)
            .Type<IntType>()
            .Validator(validator => validator.Range(1000, DateTime.Now.Year));
    }
}

// Add validation in Startup
services.AddGraphQLServer()
    .AddQueryType<Query>()
    .AddMutationType<Mutation>()
    .AddType<BookInputType>()
    .AddFluentValidation();

Authorization

GraphQL APIs can implement authorization at various levels:

[Authorize]
public class Query
{
    [Authorize(Roles = new[] { "Admin" })]
    public IQueryable<User> GetUsers([Service] IUserRepository repository)
    {
        return repository.GetUsers();
    }
    
    [Authorize(Policy = "CanViewBooks")]
    public IQueryable<Book> GetBooks([Service] IBookRepository repository)
    {
        return repository.GetBooks();
    }
}

// Register in Startup
services
    .AddGraphQLServer()
    .AddAuthorization()
    .AddQueryType<Query>();

Pagination

Hot Chocolate supports various pagination models:

public class Query
{
    [UsePaging(IncludeTotalCount = true)]
    public IQueryable<Book> GetBooks([Service] IBookRepository repository)
    {
        return repository.GetBooks();
    }
}

Filtering and Sorting

public class Query
{
    [UseFiltering]
    [UseSorting]
    public IQueryable<Book> GetBooks([Service] IBookRepository repository)
    {
        return repository.GetBooks();
    }
}

Example client query using filtering and sorting:

query {
  books(
    where: { 
      year: { gt: 2000 }, 
      author: { contains: "Smith" } 
    },
    order: [
      { title: ASC }
    ]
  ) {
    id
    title
    author
    year
  }
}

File Uploads

Hot Chocolate supports file uploads using a multipart request:

public class Mutation
{
    public async Task<string> UploadBookCover(
        int bookId, 
        IFile file,
        [Service] IFileService fileService,
        CancellationToken cancellationToken)
    {
        // Get file info
        string fileName = $"{bookId}_cover_{Path.GetFileName(file.Name)}";
        
        // Stream file content to storage
        await using Stream stream = file.OpenReadStream();
        string url = await fileService.UploadFileAsync(fileName, stream, cancellationToken);
        
        // Update book with cover URL
        await fileService.UpdateBookCoverUrlAsync(bookId, url, cancellationToken);
        
        return url;
    }
}

// Configure file uploads in Startup
services
    .AddGraphQLServer()
    .AddMutationType<Mutation>()
    .AddType<UploadType>();

Performance Optimization and Monitoring

Advanced Performance Techniques

Query Plan Analysis

Hot Chocolate allows you to analyze and optimize the execution plan:

services
    .AddGraphQLServer()
    .AddQueryType<Query>()
    .AddDiagnosticEventListener<DiagnosticEventListener>()
    .AddDiagnosticEventListener(sp => 
        new QueryExecutionPlanEventListener(
            sp.GetApplicationService<ILogger<QueryExecutionPlanEventListener>>()));
public class QueryExecutionPlanEventListener : DiagnosticEventListener
{
    private readonly ILogger<QueryExecutionPlanEventListener> _logger;

    public QueryExecutionPlanEventListener(ILogger<QueryExecutionPlanEventListener> logger)
    {
        _logger = logger;
    }

    public override IActivityScope ExecuteRequest(IRequestContext context)
    {
        _logger.LogInformation("Executing GraphQL request: {RequestId}", context.RequestId);
        return EmptyScope;
    }

    public override void ExecutionPlanned(IExecutionPlanInfo plan)
    {
        _logger.LogInformation(
            "Execution plan created: {OperationName} ({OperationKind})",
            plan.OperationName ?? "unnamed",
            plan.Operation.Kind);
        
        // Log details about the planned execution
        _logger.LogDebug(
            "Plan: {Plan}",
            plan.Print());
    }
}

Optimized Custom Resolvers

Writing performance-optimized resolvers:

public class OptimizedBookResolvers
{
    [UseProjection]
    [UseFiltering]
    [UseSorting]
    public IQueryable<Book> GetBooks([Service] IDbContextFactory<LibraryDbContext> dbContextFactory)
    {
        LibraryDbContext dbContext = dbContextFactory.CreateDbContext();
        
        // Start with a base query
        IQueryable<Book> query = dbContext.Books;
        
        // EF Core will only materialize the fields requested in the GraphQL query
        return query;
    }
    
    public async Task<Author> GetAuthor(
        [Parent] Book book, 
        [Service] AuthorByIdDataLoader authorLoader,
        CancellationToken cancellationToken)
    {
        return await authorLoader.LoadAsync(book.AuthorId, cancellationToken);
    }
}

Response Caching for Public Queries

public class QueryType : ObjectType<Query>
{
    protected override void Configure(IObjectTypeDescriptor<Query> descriptor)
    {
        descriptor
            .Field("mostPopularBooks")
            .ResolveWith<BookResolvers>(r => r.GetMostPopularBooks(default))
            .UsePaging<BookType>()
            // Cache for 5 minutes
            .UseCacheDirective(maxAge: 300);
            
        descriptor
            .Field("bookOfTheDay")
            .ResolveWith<BookResolvers>(r => r.GetBookOfTheDay(default))
            .Type<BookType>()
            // Cache for 24 hours
            .UseCacheDirective(maxAge: 86400);
    }
}

Memory Optimization with Projections

public class Query
{
    [UseProjection]
    public IQueryable<Book> GetBooks([Service] LibraryDbContext context)
    {
        return context.Books;
    }
}

// Registering projection support
services
    .AddGraphQLServer()
    .AddProjections()
    .AddFiltering()
    .AddSorting()
    .AddQueryType<Query>();

Real-Time Monitoring

Instrumentation with Application Insights

public class ApplicationInsightsEventListener : DiagnosticEventListener
{
    private readonly TelemetryClient _telemetryClient;

    public ApplicationInsightsEventListener(TelemetryClient telemetryClient)
    {
        _telemetryClient = telemetryClient;
    }

    public override IActivityScope ExecuteRequest(IRequestContext context)
    {
        // Start operation for the request
        IOperationHolder<RequestTelemetry> operation = _telemetryClient.StartOperation<RequestTelemetry>(
            "GraphQL Request");
        
        // Add custom properties
        operation.Telemetry.Properties["GraphQL.OperationName"] = context.Operation?.Name;
        operation.Telemetry.Properties["GraphQL.OperationKind"] = context.Operation?.Kind.ToString();
        
        return new ApplicationInsightsScope(operation, _telemetryClient);
    }
    
    public override void RequestError(IRequestContext context, Exception exception)
    {
        _telemetryClient.TrackException(exception, new Dictionary<string, string>
        {
            ["GraphQL.OperationName"] = context.Operation?.Name,
            ["GraphQL.Path"] = context.Path?.ToString()
        });
    }
    
    private class ApplicationInsightsScope : IActivityScope
    {
        private readonly IOperationHolder<RequestTelemetry> _operation;
        private readonly TelemetryClient _telemetryClient;

        public ApplicationInsightsScope(
            IOperationHolder<RequestTelemetry> operation, 
            TelemetryClient telemetryClient)
        {
            _operation = operation;
            _telemetryClient = telemetryClient;
        }

        public void Dispose()
        {
            _telemetryClient.StopOperation(_operation);
        }
    }
}

// Register in Startup
services
    .AddGraphQLServer()
    .AddDiagnosticEventListener(sp =>
        new ApplicationInsightsEventListener(
            sp.GetRequiredService<TelemetryClient>()));

Custom Metrics Collection

public class GraphQLMetricsMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IMetricsRegistry _metrics;

    public GraphQLMetricsMiddleware(RequestDelegate next, IMetricsRegistry metrics)
    {
        _next = next;
        _metrics = metrics;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Start timing
        Stopwatch watch = Stopwatch.StartNew();
        
        try
        {
            // Call next middleware
            await _next(context);
        }
        finally
        {
            // Record metrics
            watch.Stop();
            
            if (context.Request.Path.StartsWithSegments("/graphql"))
            {
                _metrics.RecordGraphQLRequestDuration(watch.ElapsedMilliseconds);
                
                // Get operation name from context items (set by a previous middleware)
                if (context.Items.TryGetValue("GraphQLOperationName", out object operationName) && 
                    operationName is string name)
                {
                    _metrics.RecordGraphQLOperationExecuted(name);
                }
            }
        }
    }
}

// Extension method for easier use
public static class GraphQLMetricsMiddlewareExtensions
{
    public static IApplicationBuilder UseGraphQLMetrics(this IApplicationBuilder app)
    {
        return app.UseMiddleware<GraphQLMetricsMiddleware>();
    }
}

Tracing with OpenTelemetry

public void ConfigureServices(IServiceCollection services)
{
    services.AddOpenTelemetryTracing(builder =>
    {
        builder
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation()
            .AddSource("HotChocolate")
            .AddConsoleExporter();
    });
    
    services
        .AddGraphQLServer()
        .AddDiagnosticEventListener<OpenTelemetryDiagnosticEventListener>()
        .AddQueryType<Query>();
}

public class OpenTelemetryDiagnosticEventListener : DiagnosticEventListener
{
    private static readonly ActivitySource Source = new ActivitySource("HotChocolate");

    public override IActivityScope ExecuteRequest(IRequestContext context)
    {
        Activity activity = Source.StartActivity("GraphQL.ExecuteRequest");
        
        if (activity != null)
        {
            activity.SetTag("graphql.operation.name", context.Operation?.Name);
            activity.SetTag("graphql.operation.kind", context.Operation?.Kind.ToString());
        }
        
        return new OpenTelemetryScope(activity);
    }
    
    public override void ResolverError(
        IMiddlewareContext context, 
        IError error)
    {
        Activity activity = Activity.Current;
        
        if (activity != null)
        {
            activity.SetTag("graphql.error", error.Message);
            activity.SetTag("graphql.path", context.Path.ToString());
        }
    }
    
    private class OpenTelemetryScope : IActivityScope
    {
        private readonly Activity _activity;

        public OpenTelemetryScope(Activity activity)
        {
            _activity = activity;
        }

        public void Dispose()
        {
            _activity?.Dispose();
        }
    }
}

Integration with ASP.NET Core REST APIs

GraphQL can be integrated with existing REST APIs, allowing a gradual migration:

public class ExternalBookType : ObjectType<ExternalBookDto>
{
    protected override void Configure(IObjectTypeDescriptor<ExternalBookDto> descriptor)
    {
        descriptor.Field(b => b.Id).Type<NonNullType<IdType>>();
        descriptor.Field(b => b.Title).Type<NonNullType<StringType>>();
        descriptor.Field(b => b.AuthorId).Ignore(); // Hide the AuthorId field
        
        // Resolve the author field from another REST API
        descriptor
            .Field("author")
            .ResolveWith<ExternalBookResolvers>(r => r.GetAuthor(default, default))
            .Type<AuthorType>();
    }
}

public class ExternalBookResolvers
{
    public async Task<AuthorDto> GetAuthor(
        [Parent] ExternalBookDto book, 
        [Service] IAuthorRestClient authorClient)
    {
        return await authorClient.GetAuthorAsync(book.AuthorId);
    }
}

// REST client for authors
public interface IAuthorRestClient
{
    Task<AuthorDto> GetAuthorAsync(int authorId);
}

public class AuthorRestClient : IAuthorRestClient
{
    private readonly HttpClient _httpClient;

    public AuthorRestClient(HttpClient httpClient)
    {
        _httpClient = httpClient;
        _httpClient.BaseAddress = new Uri("https://api.example.com/");
    }

    public async Task<AuthorDto> GetAuthorAsync(int authorId)
    {
        HttpResponseMessage response = await _httpClient.GetAsync($"authors/{authorId}");
        response.EnsureSuccessStatusCode();
        
        string json = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<AuthorDto>(json);
    }
}

Code Examples

Complete Example API

Here’s a complete example of a GraphQL API with Entity Framework Core:

// Domain Models
public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public int PageCount { get; set; }
    public int AuthorId { get; set; }
    public Author Author { get; set; }
    public ICollection<Review> Reviews { get; set; } = new List<Review>();
}

public class Author
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Biography { get; set; }
    public ICollection<Book> Books { get; set; } = new List<Book>();
}

public class Review
{
    public int Id { get; set; }
    public int BookId { get; set; }
    public string ReviewerName { get; set; }
    public string Comment { get; set; }
    public int Rating { get; set; }
    public Book Book { get; set; }
}

// DbContext
public class LibraryDbContext : DbContext
{
    public LibraryDbContext(DbContextOptions<LibraryDbContext> options)
        : base(options)
    {
    }

    public DbSet<Book> Books { get; set; }
    public DbSet<Author> Authors { get; set; }
    public DbSet<Review> Reviews { get; set; }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Define relationships
        modelBuilder.Entity<Book>()
            .HasOne(b => b.Author)
            .WithMany(a => a.Books)
            .HasForeignKey(b => b.AuthorId);
            
        modelBuilder.Entity<Review>()
            .HasOne(r => r.Book)
            .WithMany(b => b.Reviews)
            .HasForeignKey(r => r.BookId);
    }
}

// GraphQL Types
public class BookType : ObjectType<Book>
{
    protected override void Configure(IObjectTypeDescriptor<Book> descriptor)
    {
        descriptor.Field(b => b.Id).Type<NonNullType<IdType>>();
        descriptor.Field(b => b.Title).Type<NonNullType<StringType>>();
        descriptor.Field(b => b.Description).Type<StringType>();
        descriptor.Field(b => b.PageCount).Type<NonNullType<IntType>>();
        
        // Include author in query without N+1 issue
        descriptor
            .Field(b => b.Author)
            .ResolveWith<BookResolvers>(r => r.GetAuthor(default, default, default))
            .UseDbContext<LibraryDbContext>()
            .Name("author");
            
        // Include reviews in query without N+1 issue
        descriptor
            .Field(b => b.Reviews)
            .ResolveWith<BookResolvers>(r => r.GetReviews(default, default, default))
            .UseDbContext<LibraryDbContext>()
            .UsePaging<ReviewType>()
            .Name("reviews");
            
        descriptor.Field(b => b.AuthorId).Ignore();
    }
}

public class AuthorType : ObjectType<Author>
{
    protected override void Configure(IObjectTypeDescriptor<Author> descriptor)
    {
        descriptor.Field(a => a.Id).Type<NonNullType<IdType>>();
        descriptor.Field(a => a.Name).Type<NonNullType<StringType>>();
        descriptor.Field(a => a.Biography).Type<StringType>();
        
        // Include books in query without N+1 issue
        descriptor
            .Field(a => a.Books)
            .ResolveWith<AuthorResolvers>(r => r.GetBooks(default, default, default))
            .UseDbContext<LibraryDbContext>()
            .UsePaging<BookType>()
            .Name("books");
    }
}

public class ReviewType : ObjectType<Review>
{
    protected override void Configure(IObjectTypeDescriptor<Review> descriptor)
    {
        descriptor.Field(r => r.Id).Type<NonNullType<IdType>>();
        descriptor.Field(r => r.ReviewerName).Type<NonNullType<StringType>>();
        descriptor.Field(r => r.Comment).Type<StringType>();
        descriptor.Field(r => r.Rating).Type<NonNullType<IntType>>();
        descriptor.Field(r => r.BookId).Ignore();
    }
}

// Data Loaders
public class BookDataLoader : BatchDataLoader<int, Book>
{
    private readonly IDbContextFactory<LibraryDbContext> _dbContextFactory;

    public BookDataLoader(
        IDbContextFactory<LibraryDbContext> dbContextFactory,
        IBatchScheduler batchScheduler)
        : base(batchScheduler)
    {
        _dbContextFactory = dbContextFactory;
    }

    protected override async Task<IReadOnlyDictionary<int, Book>> LoadBatchAsync(
        IReadOnlyList<int> keys,
        CancellationToken cancellationToken)
    {
        await using LibraryDbContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
        
        return await dbContext.Books
            .Where(b => keys.Contains(b.Id))
            .ToDictionaryAsync(b => b.Id, cancellationToken);
    }
}

public class AuthorDataLoader : BatchDataLoader<int, Author>
{
    private readonly IDbContextFactory<LibraryDbContext> _dbContextFactory;

    public AuthorDataLoader(
        IDbContextFactory<LibraryDbContext> dbContextFactory,
        IBatchScheduler batchScheduler)
        : base(batchScheduler)
    {
        _dbContextFactory = dbContextFactory;
    }

    protected override async Task<IReadOnlyDictionary<int, Author>> LoadBatchAsync(
        IReadOnlyList<int> keys,
        CancellationToken cancellationToken)
    {
        await using LibraryDbContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
        
        return await dbContext.Authors
            .Where(a => keys.Contains(a.Id))
            .ToDictionaryAsync(a => a.Id, cancellationToken);
    }
}

// Resolvers
public class BookResolvers
{
    public async Task<Author> GetAuthor(
        [Parent] Book book,
        AuthorDataLoader dataLoader,
        CancellationToken cancellationToken)
    {
        return await dataLoader.LoadAsync(book.AuthorId, cancellationToken);
    }
    
    public async Task<IEnumerable<Review>> GetReviews(
        [Parent] Book book,
        [ScopedService] LibraryDbContext dbContext,
        CancellationToken cancellationToken)
    {
        return await dbContext.Reviews
            .Where(r => r.BookId == book.Id)
            .ToListAsync(cancellationToken);
    }
}

public class AuthorResolvers
{
    public async Task<IEnumerable<Book>> GetBooks(
        [Parent] Author author,
        [ScopedService] LibraryDbContext dbContext,
        CancellationToken cancellationToken)
    {
        return await dbContext.Books
            .Where(b => b.AuthorId == author.Id)
            .ToListAsync(cancellationToken);
    }
}

// Input Types
public class AddBookInput
{
    public string Title { get; set; }
    public string Description { get; set; }
    public int PageCount { get; set; }
    public int AuthorId { get; set; }
}

public class AddAuthorInput
{
    public string Name { get; set; }
    public string Biography { get; set; }
}

public class AddReviewInput
{
    public int BookId { get; set; }
    public string ReviewerName { get; set; }
    public string Comment { get; set; }
    public int Rating { get; set; }
}

// Query Type
public class Query
{
    [UseDbContext(typeof(LibraryDbContext))]
    [UsePaging]
    [UseFiltering]
    [UseSorting]
    public IQueryable<Book> GetBooks(
        [ScopedService] LibraryDbContext context)
    {
        return context.Books;
    }
    
    [UseDbContext(typeof(LibraryDbContext))]
    public async Task<Book> GetBookById(
        [ScopedService] LibraryDbContext context,
        int id)
    {
        return await context.Books.FindAsync(id);
    }
    
    [UseDbContext(typeof(LibraryDbContext))]
    [UsePaging]
    [UseFiltering]
    [UseSorting]
    public IQueryable<Author> GetAuthors(
        [ScopedService] LibraryDbContext context)
    {
        return context.Authors;
    }
    
    [UseDbContext(typeof(LibraryDbContext))]
    public async Task<Author> GetAuthorById(
        [ScopedService] LibraryDbContext context,
        int id)
    {
        return await context.Authors.FindAsync(id);
    }
    
    [UseDbContext(typeof(LibraryDbContext))]
    [UsePaging]
    [UseFiltering]
    [UseSorting]
    public IQueryable<Review> GetReviews(
        [ScopedService] LibraryDbContext context)
    {
        return context.Reviews;
    }
}

// Mutation Type
public class Mutation
{
    [UseDbContext(typeof(LibraryDbContext))]
    public async Task<Book> AddBookAsync(
        [ScopedService] LibraryDbContext context,
        AddBookInput input)
    {
        // Check if author exists
        bool authorExists = await context.Authors.AnyAsync(a => a.Id == input.AuthorId);
        
        if (!authorExists)
        {
            throw new GraphQLException(
                new Error("Author not found", "AUTHOR_NOT_FOUND"));
        }
        
        Book book = new Book
        {
            Title = input.Title,
            Description = input.Description,
            PageCount = input.PageCount,
            AuthorId = input.AuthorId
        };
        
        context.Books.Add(book);
        await context.SaveChangesAsync();
        
        return book;
    }
    
    [UseDbContext(typeof(LibraryDbContext))]
    public async Task<Author> AddAuthorAsync(
        [ScopedService] LibraryDbContext context,
        AddAuthorInput input)
    {
        Author author = new Author
        {
            Name = input.Name,
            Biography = input.Biography
        };
        
        context.Authors.Add(author);
        await context.SaveChangesAsync();
        
        return author;
    }
    
    [UseDbContext(typeof(LibraryDbContext))]
    public async Task<Review> AddReviewAsync(
        [ScopedService] LibraryDbContext context,
        [Service] ITopicEventSender eventSender,
        AddReviewInput input)
    {
        // Validate rating
        if (input.Rating < 1 || input.Rating > 5)
        {
            throw new GraphQLException(
                new Error("Rating must be between 1 and 5", "INVALID_RATING"));
        }
        
        // Check if book exists
        bool bookExists = await context.Books.AnyAsync(b => b.Id == input.BookId);
        
        if (!bookExists)
        {
            throw new GraphQLException(
                new Error("Book not found", "BOOK_NOT_FOUND"));
        }
        
        Review review = new Review
        {
            BookId = input.BookId,
            ReviewerName = input.ReviewerName,
            Comment = input.Comment,
            Rating = input.Rating
        };
        
        context.Reviews.Add(review);
        await context.SaveChangesAsync();
        
        // Send subscription event
        await eventSender.SendAsync(nameof(Subscription.OnReviewAdded), review);
        
        return review;
    }
}

// Subscription Type
public class Subscription
{
    [Subscribe]
    [Topic]
    public Review OnReviewAdded([EventMessage] Review review)
    {
        return review;
    }
}

// Setup in Program.cs (ASP.NET Core 6+)
var builder = WebApplication.CreateBuilder(args);

// Add database context
builder.Services.AddPooledDbContextFactory<LibraryDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// Add GraphQL server
builder.Services
    .AddGraphQLServer()
    .AddQueryType<Query>()
    .AddMutationType<Mutation>()
    .AddSubscriptionType<Subscription>()
    .AddType<BookType>()
    .AddType<AuthorType>()
    .AddType<ReviewType>()
    .AddFiltering()
    .AddSorting()
    .AddProjections()
    .AddDataLoader<BookDataLoader>()
    .AddDataLoader<AuthorDataLoader>()
    .AddInMemorySubscriptions();

var app = builder.Build();

app.UseWebSockets();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
    endpoints.MapGraphQL();
    endpoints.MapBananaCakePop();
});

app.Run();

Consuming GraphQL from .NET Clients

Using GraphQL clients in .NET applications:

With Strawberry Shake

// Install Strawberry Shake:
// dotnet add package StrawberryShake.Transport.Http
// dotnet add package StrawberryShake.CodeGeneration.CSharp

// Add schema file:
// schema.graphql

// Execute the code generator:
// dotnet build

// Generated client usage:
public class Program
{
    public static async Task Main()
    {
        ServiceCollection services = new ServiceCollection();
        
        // Register the GraphQL client
        services
            .AddGraphQLClient()
            .ConfigureHttpClient(client =>
                client.BaseAddress = new Uri("https://api.example.com/graphql"));
            
        ServiceProvider serviceProvider = services.BuildServiceProvider();
        
        // Get the client
        IGraphQLClient client = serviceProvider.GetRequiredService<IGraphQLClient>();
        
        // Execute a query
        GetBooksResult result = await client.GetBooks.ExecuteAsync();
        
        if (!result.Errors.Any())
        {
            foreach (IBook book in result.Data.Books)
            {
                Console.WriteLine($"{book.Title} by {book.Author.Name}");
            }
        }
    }
}

With GraphQL.Client

using GraphQL;
using GraphQL.Client.Http;
using GraphQL.Client.Serializer.SystemTextJson;

public class GraphQLService
{
    private readonly GraphQLHttpClient _client;

    public GraphQLService()
    {
        _client = new GraphQLHttpClient(
            "https://api.example.com/graphql", 
            new SystemTextJsonSerializer());
    }

    public async Task<IEnumerable<BookDto>> GetBooksAsync(string searchTerm = null)
    {
        GraphQLRequest request = new GraphQLRequest
        {
            Query = @"
                query GetBooks($searchTerm: String) {
                  books(where: { title: { contains: $searchTerm } }) {
                    id
                    title
                    author {
                      name
                    }
                  }
                }",
            Variables = new
            {
                searchTerm
            }
        };
          GraphQLResponse<BooksResponse> response = await _client.SendQueryAsync<BooksResponse>(request);
        
        if (response.Errors != null && response.Errors.Any())
        {
            throw new GraphQLException(response.Errors);
        }
        
        return response.Data.Books;
    }
    
    public async Task<BookDto> AddBookAsync(AddBookInputDto input)
    {
        GraphQLRequest request = new GraphQLRequest
        {
            Query = @"
                mutation AddBook($input: AddBookInput!) {
                  addBook(input: $input) {
                    id
                    title
                    description
                    pageCount
                    author {
                      name
                    }
                  }
                }",
            Variables = new
            {
                input
            }
        };
          GraphQLResponse<AddBookResponse> response = await _client.SendMutationAsync<AddBookResponse>(request);
        
        if (response.Errors != null && response.Errors.Any())
        {
            throw new GraphQLException(response.Errors);
        }
        
        return response.Data.AddBook;
    }
}

// Response types
public class BooksResponse
{
    public List<BookDto> Books { get; set; }
}

public class AddBookResponse
{
    public BookDto AddBook { get; set; }
}

// DTOs
public class BookDto
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public int PageCount { get; set; }
    public AuthorDto Author { get; set; }
}

public class AuthorDto
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class AddBookInputDto
{
    public string Title { get; set; }
    public string Description { get; set; }
    public int PageCount { get; set; }
    public int AuthorId { get; set; }
}

Comprehensive .NET Client Implementations

Strawberry Shake Deep Dive

StrawberryShake is Hot Chocolate’s client library for type-safe GraphQL operations:

Setting Up Strawberry Shake

  1. Install required packages:
dotnet add package StrawberryShake.Transport.Http
dotnet add package StrawberryShake.Tools.Configuration
  1. Create a schema file (schema.graphql) with your GraphQL schema or use schema download:
dotnet graphql init https://api.example.com/graphql -n ExampleClient -o Generated
  1. Create query files (.graphql):
# GetBooks.graphql
query GetBooks($searchTerm: String) {
  books(where: { title: { contains: $searchTerm } }) {
    id
    title
    author {
      name
    }
    reviews {
      reviewer
      rating
    }
  }
}

# AddBook.graphql
mutation AddBook($title: String!, $author: String!, $yearPublished: Int!) {
  addBook(input: {
    title: $title,
    author: $author,
    yearPublished: $yearPublished
  }) {
    id
    title
  }
}
  1. Build your project to generate client code.

Using Strawberry Shake Client

public class BookService
{
    private readonly IExampleClient _client;

    public BookService(IExampleClient client)
    {
        _client = client;
    }
    
    public async Task<IReadOnlyList<BookDto>> SearchBooksAsync(string searchTerm)
    {
        // Use the generated operation
        GetBooksResult result = await _client.GetBooks.ExecuteAsync(searchTerm);
        
        // Handle errors
        if (result.Errors.Count > 0)
        {
            foreach (GraphQLClientError error in result.Errors)
            {
                Console.Error.WriteLine($"GraphQL error: {error.Message}");
            }
            throw new Exception("Error executing GraphQL query");
        }
        
        // Convert to DTOs
        return result.Data.Books
            .Select(book => new BookDto
            {
                Id = book.Id,
                Title = book.Title,
                Author = book.Author?.Name,
                AverageRating = book.Reviews.Average(r => r.Rating)
            })
            .ToList();
    }
    
    public async Task<string> AddBookAsync(AddBookInputDto inputDto)
    {
        // Use the generated operation
        AddBookResult result = await _client.AddBook.ExecuteAsync(
            inputDto.Title,
            inputDto.Author,
            inputDto.YearPublished);
            
        // Handle errors
        if (result.Errors.Count > 0)
        {
            throw new Exception(
                $"Error adding book: {string.Join(", ", result.Errors.Select(e => e.Message))}");
        }
        
        return result.Data.AddBook.Id;
    }
}

Dependency Injection Integration

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Register the GraphQL client
        services
            .AddExampleClient()
            .ConfigureHttpClient(client =>
            {
                client.BaseAddress = new Uri("https://api.example.com/graphql");
                client.DefaultRequestHeaders.Add("Authorization", "Bearer ...");
            });
            
        // Register the book service
        services.AddScoped<BookService>();
    }
}

Handling Subscriptions

public class BookNotifier
{
    private readonly IExampleClient _client;
    private IDisposable _subscription;

    public BookNotifier(IExampleClient client)
    {
        _client = client;
    }
    
    public void StartListeningForNewBooks()
    {
        _subscription = _client.BookAdded
            .Watch()
            .Where(result => !result.Errors.Any())
            .Select(result => result.Data.BookAdded)
            .Subscribe(
                onNext: book => 
                {
                    Console.WriteLine($"New book added: {book.Title} by {book.Author.Name}");
                },
                onError: error => 
                {
                    Console.Error.WriteLine($"Error in subscription: {error.Message}");
                });
    }
    
    public void StopListening()
    {
        _subscription?.Dispose();
    }
}

GraphQL.Client with Custom Extensions

public class GraphQLClientExtensions
{
    public static GraphQLHttpClient CreateClient(string endpoint, string authToken = null)
    {
        GraphQLHttpClientOptions options = new GraphQLHttpClientOptions
        {
            EndPoint = new Uri(endpoint)
        };
        
        HttpClient httpClient = new HttpClient();
        
        if (!string.IsNullOrEmpty(authToken))
        {
            httpClient.DefaultRequestHeaders.Authorization = 
                new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", authToken);
        }
        
        return new GraphQLHttpClient(options, new NewtonsoftJsonSerializer(), httpClient);
    }
    
    public static async Task<TResponse> ExecuteQueryAsync<TResponse>(
        this GraphQLHttpClient client,
        string query,
        object variables = null,
        string operationName = null,
        CancellationToken cancellationToken = default)
    {
        GraphQLRequest request = new GraphQLRequest
        {
            Query = query,
            Variables = variables,
            OperationName = operationName
        };
        
        GraphQLResponse<TResponse> response = 
            await client.SendQueryAsync<TResponse>(request, cancellationToken);
            
        if (response.Errors != null && response.Errors.Any())
        {
            throw new GraphQLException(
                $"GraphQL query failed: {string.Join(", ", response.Errors.Select(e => e.Message))}");
        }
        
        return response.Data;
    }
    
    public static async Task<TResponse> ExecuteMutationAsync<TResponse>(
        this GraphQLHttpClient client,
        string mutation,
        object variables = null,
        string operationName = null,
        CancellationToken cancellationToken = default)
    {
        GraphQLRequest request = new GraphQLRequest
        {
            Query = mutation,
            Variables = variables,
            OperationName = operationName
        };
        
        GraphQLResponse<TResponse> response = 
            await client.SendMutationAsync<TResponse>(request, cancellationToken);
            
        if (response.Errors != null && response.Errors.Any())
        {
            throw new GraphQLException(
                $"GraphQL mutation failed: {string.Join(", ", response.Errors.Select(e => e.Message))}");
        }
        
        return response.Data;
    }
}

// Using these extensions
public class BookClient
{
    private readonly GraphQLHttpClient _client;
    
    public BookClient(string endpoint, string authToken)
    {
        _client = GraphQLClientExtensions.CreateClient(endpoint, authToken);
    }
    
    public async Task<List<BookDto>> GetBooksAsync(string searchTerm = null)
    {
        string query = @"
            query GetBooks($searchTerm: String) {
              books(where: { title: { contains: $searchTerm } }) {
                id
                title
                author {
                  name
                }
              }
            }";
            
        BooksResponse response = await _client.ExecuteQueryAsync<BooksResponse>(
            query, 
            new { searchTerm });
            
        return response.Books;
    }
    
    public async Task<BookDto> AddBookAsync(AddBookInputDto input)
    {
        string mutation = @"
            mutation AddBook($title: String!, $author: String!, $yearPublished: Int!) {
              addBook(input: { title: $title, author: $author, yearPublished: $yearPublished }) {
                id
                title
                author {
                  name
                }
              }
            }";
            
        AddBookResponse response = await _client.ExecuteMutationAsync<AddBookResponse>(
            mutation,
            new 
            { 
                title = input.Title, 
                author = input.Author, 
                yearPublished = input.YearPublished 
            });
            
        return response.AddBook;
    }
}

Enterprise Adoption and Migration Strategies

Migrating from REST to GraphQL

Incremental Migration Strategy

When migrating a large REST API to GraphQL, an incremental approach works best:

public class LegacyRestProxy
{
    private readonly HttpClient _httpClient;

    public LegacyRestProxy(IHttpClientFactory httpClientFactory)
    {
        _httpClient = httpClientFactory.CreateClient("LegacyApi");
    }
    
    public async Task<Product> GetProductAsync(int id)
    {
        HttpResponseMessage response = await _httpClient.GetAsync($"/api/products/{id}");
        response.EnsureSuccessStatusCode();
        
        string json = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<Product>(json);
    }
    
    public async Task<List<Order>> GetOrdersForCustomerAsync(int customerId)
    {
        HttpResponseMessage response = await _httpClient.GetAsync($"/api/customers/{customerId}/orders");
        response.EnsureSuccessStatusCode();
        
        string json = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<List<Order>>(json);
    }
}

public class Query
{
    // GraphQL resolver that uses the REST proxy
    public Task<Product> GetProduct(
        [Service] LegacyRestProxy restProxy,
        int id)
    {
        return restProxy.GetProductAsync(id);
    }
    
    // New pure GraphQL resolver
    public async Task<Customer> GetCustomer(
        [Service] ICustomerRepository repository,
        int id)
    {
        return await repository.GetByIdAsync(id);
    }
    
    // Mixed approach
    public async Task<CustomerWithOrders> GetCustomerWithOrders(
        [Service] ICustomerRepository customerRepo,
        [Service] LegacyRestProxy restProxy,
        int id)
    {
        Customer customer = await customerRepo.GetByIdAsync(id);
        List<Order> orders = await restProxy.GetOrdersForCustomerAsync(id);
        
        return new CustomerWithOrders
        {
            Customer = customer,
            Orders = orders
        };
    }
}

GraphQL Gateway Pattern

A common approach is to create a GraphQL gateway that aggregates multiple services:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Register HTTP clients for each service
        services.AddHttpClient("ProductService", client =>
        {
            client.BaseAddress = new Uri("https://products-api.example.com/");
        });
        
        services.AddHttpClient("CustomerService", client =>
        {
            client.BaseAddress = new Uri("https://customers-api.example.com/");
        });
        
        services.AddHttpClient("OrderService", client =>
        {
            client.BaseAddress = new Uri("https://orders-api.example.com/");
        });
        
        // Register service proxies
        services.AddScoped<IProductServiceProxy, ProductServiceRestProxy>();
        services.AddScoped<ICustomerServiceProxy, CustomerServiceRestProxy>();
        services.AddScoped<IOrderServiceProxy, OrderServiceRestProxy>();
        
        // GraphQL server configuration
        services
            .AddGraphQLServer()
            .AddQueryType<Query>()
            .AddMutationType<Mutation>()
            .AddType<ProductType>()
            .AddType<CustomerType>()
            .AddType<OrderType>();
    }
}

public class Query
{
    public async Task<Product> GetProduct(
        int id,
        [Service] IProductServiceProxy productService)
    {
        return await productService.GetProductAsync(id);
    }
    
    public async Task<Customer> GetCustomer(
        int id,
        [Service] ICustomerServiceProxy customerService)
    {
        return await customerService.GetCustomerAsync(id);
    }
    
    public async Task<IEnumerable<Order>> GetOrders(
        int customerId,
        [Service] IOrderServiceProxy orderService)
    {
        return await orderService.GetOrdersForCustomerAsync(customerId);
    }
}

public class CustomerType : ObjectType<Customer>
{
    protected override void Configure(IObjectTypeDescriptor<Customer> descriptor)
    {
        descriptor.Field(c => c.Id).Type<NonNullType<IdType>>();
        descriptor.Field(c => c.Name).Type<NonNullType<StringType>>();
        
        // Service-to-service relationship
        descriptor
            .Field("orders")
            .ResolveWith<CustomerResolvers>(r => r.GetOrders(default, default))
            .Type<NonNullType<ListType<NonNullType<OrderType>>>>();
    }
}

public class CustomerResolvers
{
    public async Task<IEnumerable<Order>> GetOrders(
        [Parent] Customer customer,
        [Service] IOrderServiceProxy orderService)
    {
        return await orderService.GetOrdersForCustomerAsync(customer.Id);
    }
}

Enterprise Security Patterns

Custom Authentication/Authorization with GraphQL

// Custom authorization policy provider
public class GraphQLAuthorizationHandler : AuthorizationHandler<GraphQLRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, 
        GraphQLRequirement requirement)
    {
        if (!context.User.Identity.IsAuthenticated)
        {
            return Task.CompletedTask;
        }
        
        // Access control based on operation
        GraphQLRequest request = context.Resource as GraphQLRequest;
        if (request != null && 
            request.OperationName == "AdminOperation" && 
            !context.User.IsInRole("Admin"))
        {
            return Task.CompletedTask;
        }
        
        context.Succeed(requirement);
        return Task.CompletedTask;
    }
}

public class GraphQLRequirement : IAuthorizationRequirement { }

// Configure in Startup
public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ValidIssuer = Configuration["Jwt:Issuer"],
                ValidAudience = Configuration["Jwt:Audience"],
                IssuerSigningKey = new SymmetricSecurityKey(
                    Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
            };
        });

    services.AddAuthorization(options =>
    {
        options.AddPolicy("GraphQLPolicy", policy =>
            policy.Requirements.Add(new GraphQLRequirement()));
    });
    
    services.AddSingleton<IAuthorizationHandler, GraphQLAuthorizationHandler>();
    
    // Add GraphQL with authorization
    services
        .AddGraphQLServer()
        .AddAuthorization()
        .AddQueryType<Query>()
        .AddMutationType<Mutation>();
}

Field-Level Security with GraphQL

public class ProductType : ObjectType<Product>
{
    protected override void Configure(IObjectTypeDescriptor<Product> descriptor)
    {
        descriptor.Field(p => p.Id).Type<NonNullType<IdType>>();
        descriptor.Field(p => p.Name).Type<NonNullType<StringType>>();
        descriptor.Field(p => p.Description).Type<StringType>();
        descriptor.Field(p => p.Price).Type<NonNullType<FloatType>>();
        
        // Only admins can see cost
        descriptor
            .Field(p => p.Cost)
            .Type<FloatType>()
            .Authorize(new[] { "Admin" });
        
        // Only authenticated users can see inventory
        descriptor
            .Field(p => p.InventoryCount)
            .Type<IntType>()
            .Authorize();
            
        // Custom resolver with security logic
        descriptor
            .Field("profitMargin")
            .Type<FloatType>()
            .ResolveWith<ProductResolvers>(r => r.GetProfitMargin(default, default))
            .Authorize(new[] { "Manager", "Admin" });
    }
}

public class ProductResolvers
{
    public float? GetProfitMargin(
        [Parent] Product product,
        [Service] ICurrentUser currentUser)
    {
        // Additional security check beyond authorization attribute
        if (!currentUser.HasPermission("VIEW_FINANCIAL_DATA"))
        {
            return null;
        }
        
        return product.Price - product.Cost;
    }
}

Integration with Large-Scale Systems

Distributed Tracing and Logging

public class DistributedTracingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<DistributedTracingMiddleware> _logger;

    public DistributedTracingMiddleware(
        RequestDelegate next,
        ILogger<DistributedTracingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(
        HttpContext context,
        [FromServices] ISchemaSerializer schemaSerializer)
    {
        // Capture original body
        string requestBody = await ReadRequestBodyAsync(context.Request);
        
        // Extract the GraphQL request
        GraphQLRequest gqlRequest = JsonSerializer.Deserialize<GraphQLRequest>(requestBody);
        
        // Generate a trace ID if not present
        string traceId = context.TraceIdentifier;
        
        // Add trace ID to log context
        using (_logger.BeginScope(new Dictionary<string, object>
        {
            ["TraceId"] = traceId,
            ["OperationName"] = gqlRequest.OperationName ?? "unnamed",
            ["Query"] = gqlRequest.Query
        }))
        {
            _logger.LogInformation("Executing GraphQL request");
            
            // Add context for downstream middlewares
            context.Items["GraphQLRequest"] = gqlRequest;
            context.Items["TraceId"] = traceId;
            
            // Create a new request body with trace ID
            gqlRequest.Extensions = gqlRequest.Extensions ?? new Dictionary<string, object>();
            gqlRequest.Extensions["traceId"] = traceId;
            
            // Replace the body
            await ReplaceRequestBodyAsync(context.Request, gqlRequest);
            
            // Call next middleware
            await _next(context);
            
            _logger.LogInformation("GraphQL request completed");
        }
    }
    
    private static async Task<string> ReadRequestBodyAsync(HttpRequest request)
    {
        request.EnableBuffering();
        
        using StreamReader reader = new StreamReader(
            request.Body, 
            encoding: Encoding.UTF8,
            detectEncodingFromByteOrderMarks: false,
            leaveOpen: true);
            
        string body = await reader.ReadToEndAsync();
        request.Body.Position = 0;
        return body;
    }
    
    private static async Task ReplaceRequestBodyAsync(HttpRequest request, GraphQLRequest gqlRequest)
    {
        string json = JsonSerializer.Serialize(gqlRequest);
        byte[] bytes = Encoding.UTF8.GetBytes(json);
        
        request.Body = new MemoryStream(bytes);
        request.ContentLength = bytes.Length;
    }
}

// Extension method
public static class DistributedTracingMiddlewareExtensions
{
    public static IApplicationBuilder UseGraphQLDistributedTracing(
        this IApplicationBuilder app)
    {
        return app.UseMiddleware<DistributedTracingMiddleware>();
    }
}

Integration with Event Sourcing

public class EventSourcingMutation
{
    public async Task<OrderDto> PlaceOrderAsync(
        PlaceOrderInput input,
        [Service] IEventStore eventStore,
        [Service] IMediator mediator)
    {
        // Create the command
        PlaceOrderCommand command = new PlaceOrderCommand(
            Guid.NewGuid(),
            input.CustomerId,
            input.Products.Select(p => new OrderLineItem(
                p.ProductId,
                p.Quantity,
                p.UnitPrice)).ToList());
        
        // Create the events
        OrderPlacedEvent orderEvent = new OrderPlacedEvent(
            command.OrderId,
            command.CustomerId,
            command.LineItems);
            
        // Store the event
        await eventStore.AppendToStreamAsync(
            $"order-{command.OrderId}",
            ExpectedVersion.NoStream,
            new[] { orderEvent });
            
        // Publish the command for processing
        await mediator.Publish(command);
        
        // Return the result
        return new OrderDto
        {
            Id = command.OrderId.ToString(),
            CustomerId = command.CustomerId.ToString(),
            Status = "Pending",
            Products = command.LineItems.Select(li => new OrderProductDto
            {
                ProductId = li.ProductId.ToString(),
                Quantity = li.Quantity,
                UnitPrice = li.UnitPrice
            }).ToList()
        };
    }
}