Understanding GraphQL in .NET - A Modern Approach to API Development
In today’s API landscape, GraphQL has emerged as a powerful alternative to REST, offering clients exactly the data they need in a single request. This article explores GraphQL implementation in .NET, focusing on practical patterns and real-world considerations that go beyond basic tutorials.
Incase you are new to GraphQL have a look at this Introduction to GraphQL
Why GraphQL in .NET?
The Case for GraphQL
- Precise Data Fetching: Clients request only what they need
- Strong Typing: Built-in validation through schema
- Rapid Iteration: Frontend can evolve without backend changes
- Aggregation: Combine multiple data sources seamlessly
.NET’s GraphQL Ecosystem
- Hot Chocolate: The leading GraphQL server implementation
- Entity Framework Integration: Smooth data layer interaction
- Performance: .NET’s optimized runtime for graph operations
Core Concepts in Practice
Schema-First vs Code-First
While GraphQL supports both approaches, .NET’s Hot Chocolate shines with code-first:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Code-first type definition
public class ProductType : ObjectType<Product>
{
protected override void Configure(IObjectTypeDescriptor<Product> descriptor)
{
descriptor.Description("Represents a sellable product");
descriptor.Field(p => p.Id)
.Description("The unique identifier")
.ID();
descriptor.Field(p => p.Price)
.Type<DecimalType>()
.Description("The product's price in USD");
}
}
The Resolver Pattern
Resolvers handle field-level data fetching:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ProductResolvers
{
public string GetFormattedPrice([Parent] Product product)
{
return product.Price.ToString("C");
}
public async Task<InventoryStatus> GetInventory(
[Parent] Product product,
[Service] IInventoryService service)
{
return await service.GetStatus(product.Id);
}
}
Advanced Implementation Patterns
Batching and Caching with DataLoaders
Solving the N+1 problem elegantly:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ProductReviewsDataLoader : BatchDataLoader<int, List<ProductReview>>
{
private readonly IReviewRepository _repository;
public ProductReviewsDataLoader(
IReviewRepository repository,
IBatchScheduler scheduler)
: base(scheduler)
{
_repository = repository;
}
protected override async Task<IReadOnlyDictionary<int, List<ProductReview>>>
LoadBatchAsync(IReadOnlyList<int> productIds, CancellationToken ct)
{
var reviews = await _repository.GetForProducts(productIds);
return reviews.ToDictionary(r => r.ProductId, r => r.ToList());
}
}
Schema Stitching for Microservices
Combine multiple GraphQL services:
1
2
3
4
services.AddGraphQLServer()
.AddRemoteSchemaFromHttp("inventory")
.AddRemoteSchemaFromHttp("reviews")
.AddTypeExtensionsFromFile("./SchemaExtensions.graphql");
Real-World Considerations
Performance Optimization
- Query Analysis:
1 2 3
services.AddGraphQLServer() .AddMaxExecutionDepthRule(5) .AddOperationComplexityAnalyzer(c => c.MaximumAllowed = 1000);
- Persisted Queries:
1 2
services.AddGraphQLServer() .AddReadOnlyFileSystemQueryStorage("./persisted_queries");
Security Practices
- Authentication:
1 2 3
descriptor.Field("adminData") .Authorize("AdminPolicy") .Resolve(...);
- Rate Limiting:
1 2
services.AddGraphQLServer() .AddRequestExecutorOptions(c => c.ExecutionTimeout = TimeSpan.FromSeconds(30));
Testing Strategies
Unit Testing Resolvers
1
2
3
4
5
6
7
8
9
10
11
12
13
[Fact]
public async Task ProductResolver_ReturnsFormattedPrice()
{
// Arrange
var resolver = new ProductResolvers();
var product = new Product { Price = 19.99m };
// Act
var result = resolver.GetFormattedPrice(product);
// Assert
Assert.Equal("$19.99", result);
}
Integration Testing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[Fact]
public async Task ProductQuery_ReturnsFilteredResults()
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.PostAsJsonAsync("/graphql", new
{
query = @"{
products(where: { price: { gt: 100 } }) {
nodes { id name }
}
}"
});
// Assert
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("expensiveItem", content);
}
Monitoring and Diagnostics
Query Logging
1
2
3
4
5
6
7
8
9
10
11
services.AddGraphQLServer()
.AddDiagnosticEventListener<ConsoleQueryLogger>();
public class ConsoleQueryLogger : ExecutionDiagnosticEventListener
{
public override IDisposable ExecuteRequest(IRequestContext context)
{
Console.WriteLine($"Request started: {context.Request.Query}");
return base.ExecuteRequest(context);
}
}
Apollo Tracing
1
2
services.AddGraphQLServer()
.AddApolloTracing();
Migration Story
Incremental Adoption
- Proxy Existing REST Endpoints:
1 2 3 4 5 6 7 8 9 10
public class LegacyRestResolver { [GraphQLName("legacyOrder")] public async Task<Order> GetOrderAsync( [ID] int id, [Service] ILegacyOrderService service) { return await service.GetOrderFromRestApi(id); } }
- Hybrid Approach:
1 2
app.MapGraphQL(); // /graphql app.MapControllers(); // Keep existing REST endpoints
Conclusion
GraphQL in .NET offers a robust solution for modern API challenges. The ecosystem provides:
- Developer Productivity: Strong typing and IntelliSense support
- Performance: Optimized query execution pipelines
- Flexibility: Adaptable to both monoliths and microservices
For teams building complex applications with evolving data requirements, investing in GraphQL can yield significant long-term benefits in maintainability and performance.
In my next article we will get into actual implementation with a demo project to see how to do the actual implementation.