Post

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

  1. Query Analysis:
    1
    2
    3
    
    services.AddGraphQLServer()
     .AddMaxExecutionDepthRule(5)
     .AddOperationComplexityAnalyzer(c => c.MaximumAllowed = 1000);
    
  2. 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

  1. 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);
     }
    }
    
  2. 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.

Further Resources

This post is licensed under CC BY 4.0 by the author.