Understanding GraphQL in .NET - A Modern Approach to API Development
Billy Okeyo

Billy Okeyo @billy_de_cartel

About: I am a software engineer building products in Python(Django), Flutter, JavaScript, TypeScript(Angular) and .NET.

Location:
Nairobi
Joined:
Apr 8, 2020

Understanding GraphQL in .NET - A Modern Approach to API Development

Publish Date: May 28
0 0

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:

// 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");
    }
}

Enter fullscreen mode Exit fullscreen mode

The Resolver Pattern

Resolvers handle field-level data fetching:

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);
    }
}

Enter fullscreen mode Exit fullscreen mode

Advanced Implementation Patterns

Batching and Caching with DataLoaders

Solving the N+1 problem elegantly:

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());
    }
}

Enter fullscreen mode Exit fullscreen mode

Schema Stitching for Microservices

Combine multiple GraphQL services:

services.AddGraphQLServer()
    .AddRemoteSchemaFromHttp("inventory")
    .AddRemoteSchemaFromHttp("reviews")
    .AddTypeExtensionsFromFile("./SchemaExtensions.graphql");

Enter fullscreen mode Exit fullscreen mode

Real-World Considerations

Performance Optimization

  1. Query Analysis :

services.AddGraphQLServer()
 .AddMaxExecutionDepthRule(5)
 .AddOperationComplexityAnalyzer(c => c.MaximumAllowed = 1000);

Enter fullscreen mode Exit fullscreen mode
  1. Persisted Queries :
services.AddGraphQLServer()
 .AddReadOnlyFileSystemQueryStorage("./persisted_queries");

Enter fullscreen mode Exit fullscreen mode

Security Practices

  • Authentication :
descriptor.Field("adminData")
  .Authorize("AdminPolicy")
  .Resolve(...);

Enter fullscreen mode Exit fullscreen mode
  • Rate Limiting :
services.AddGraphQLServer()
  .AddRequestExecutorOptions(c => c.ExecutionTimeout = TimeSpan.FromSeconds(30));

Enter fullscreen mode Exit fullscreen mode

Testing Strategies

Unit Testing Resolvers

[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);
}

Enter fullscreen mode Exit fullscreen mode

Integration Testing

[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);
}

Enter fullscreen mode Exit fullscreen mode

Monitoring and Diagnostics

Query Logging

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);
    }
}

Enter fullscreen mode Exit fullscreen mode

Apollo Tracing

services.AddGraphQLServer()
    .AddApolloTracing();

Enter fullscreen mode Exit fullscreen mode

Migration Story

Incremental Adoption

  1. Proxy Existing REST Endpoints :
public class LegacyRestResolver
{
 [GraphQLName("legacyOrder")]
 public async Task<Order> GetOrderAsync(
     [ID] int id,
     [Service] ILegacyOrderService service)
 {
     return await service.GetOrderFromRestApi(id);
 }
}

Enter fullscreen mode Exit fullscreen mode
  1. Hybrid Approach :
app.MapGraphQL(); // /graphql
app.MapControllers(); // Keep existing REST endpoints

Enter fullscreen mode Exit fullscreen mode

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

Comments 0 total

    Add comment