GraphQL
24 minute read
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
Feature | GraphQL | REST |
---|---|---|
Number of Endpoints | Single endpoint | Multiple endpoints |
Data Fetching | Client specifies data | Server defines responses |
Versioning | Evolve API without versions | Often requires versioning |
Over-fetching | Eliminated by design | Common issue |
Under-fetching | Eliminated by design | Often requires multiple requests |
Caching | Requires additional work | Built into HTTP |
Error Handling | Always returns 200 with errors in response | Uses HTTP status codes |
File Uploads | Requires special handling | Native support |
Tooling | Rich ecosystem of tools | Established standards and tools |
When to Use GraphQL Instead of REST
GraphQL is particularly well-suited for:
- Complex Domain Models - When your data structure has complex relationships
- Mobile Applications - Where bandwidth efficiency is crucial
- Aggregation APIs - When combining data from multiple sources
- Rapidly Evolving APIs - For frequent changes without versioning
- Client-Specific Data Requirements - When different clients need different data
- 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
- Install required packages:
dotnet add package StrawberryShake.Transport.Http
dotnet add package StrawberryShake.Tools.Configuration
- 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
- 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
}
}
- 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()
};
}
}