Skip to content

GraphQL in ConnectSoft Microservice Template

Purpose & Overview

GraphQL is a query language for APIs that provides a flexible and efficient approach to data fetching. In the ConnectSoft Microservice Template, GraphQL can be implemented as an alternative service model, allowing clients to request exactly the data they need while integrating seamlessly with the template's Clean Architecture and CQRS patterns.

Why GraphQL?

GraphQL offers several advantages over traditional REST APIs:

  • Single Endpoint: One endpoint (/graphql) handles all queries and mutations
  • Client-Specified Data: Clients request only the fields they need, reducing over-fetching
  • Type System: Strong typing with introspection capabilities
  • Efficient Data Fetching: Reduce number of round trips with nested queries
  • Versioning: Evolve APIs without breaking existing clients
  • Real-time Subscriptions: Built-in support for real-time updates
  • Self-Documenting: Schema serves as documentation

Architecture Overview

GraphQL integrates with the template's architecture:

GraphQL Client
GraphQL Schema (ServiceModel.GraphQL)
    ├── Queries (read operations)
    ├── Mutations (write operations)
    └── Subscriptions (real-time updates)
GraphQL Resolvers
    ├── Query Resolvers → Retrievers (DomainModel)
    ├── Mutation Resolvers → Processors (DomainModel)
    └── Subscription Resolvers → Messaging (MessagingModel)
Domain Model (DomainModel)
    ├── Processors (Commands/Writes)
    └── Retrievers (Queries/Reads)
Repository Layer (PersistenceModel)

Implementation Approach

The template uses Hot Chocolate GraphQL server for .NET, which provides:

  • Type-first or code-first schema definition
  • Built-in dependency injection support
  • Subscription support
  • Query validation and optimization
  • Extensions for authorization, filtering, sorting, pagination

1. GraphQL Schema Definition

Using Code-First Approach

// MicroserviceAggregateRootType.cs
[ExtendObjectType(OperationTypeNames.Query)]
public class MicroserviceAggregateRootQueries
{
    /// <summary>
    /// Gets MicroserviceAggregateRoot details.
    /// </summary>
    [GraphQLDescription("Retrieve a MicroserviceAggregateRoot by ID")]
    public async Task<MicroserviceAggregateRoot?> GetMicroserviceAggregateRoot(
        Guid objectId,
        [Service] IMicroserviceAggregateRootsRetriever retriever,
        CancellationToken cancellationToken)
    {
        var input = new GetMicroserviceAggregateRootDetailsInput
        {
            ObjectId = objectId
        };

        var entity = await retriever.GetMicroserviceAggregateRootDetails(input, cancellationToken);

        return entity != null ? new MicroserviceAggregateRoot
        {
            ObjectId = entity.ObjectId,
            // Map other properties
        } : null;
    }

    /// <summary>
    /// Search MicroserviceAggregateRoots with filters.
    /// </summary>
    [GraphQLDescription("Search MicroserviceAggregateRoots with optional filters")]
    [UseFiltering]
    [UseSorting]
    [UsePaging]
    public async Task<Connection<MicroserviceAggregateRoot>> GetMicroserviceAggregateRoots(
        [Service] IMicroserviceAggregateRootsRetriever retriever,
        CancellationToken cancellationToken)
    {
        var input = new SearchMicroserviceAggregateRootsInput();
        var results = await retriever.SearchMicroserviceAggregateRoots(input, cancellationToken);

        return results.Select(e => new MicroserviceAggregateRoot
        {
            ObjectId = e.ObjectId,
            // Map other properties
        }).ToConnection();
    }
}

[ExtendObjectType(OperationTypeNames.Mutation)]
public class MicroserviceAggregateRootMutations
{
    /// <summary>
    /// Create a new MicroserviceAggregateRoot.
    /// </summary>
    [GraphQLDescription("Create a new MicroserviceAggregateRoot")]
    public async Task<MicroserviceAggregateRoot> CreateMicroserviceAggregateRoot(
        CreateMicroserviceAggregateRootInput input,
        [Service] IMicroserviceAggregateRootsProcessor processor,
        CancellationToken cancellationToken)
    {
        var domainInput = new CreateMicroserviceAggregateRootInput
        {
            ObjectId = input.ObjectId
        };

        var entity = await processor.CreateMicroserviceAggregateRoot(domainInput, cancellationToken);

        return new MicroserviceAggregateRoot
        {
            ObjectId = entity.ObjectId,
            // Map other properties
        };
    }

    /// <summary>
    /// Delete a MicroserviceAggregateRoot.
    /// </summary>
    [GraphQLDescription("Delete a MicroserviceAggregateRoot")]
    public async Task<bool> DeleteMicroserviceAggregateRoot(
        Guid objectId,
        [Service] IMicroserviceAggregateRootsProcessor processor,
        CancellationToken cancellationToken)
    {
        var input = new DeleteMicroserviceAggregateRootInput
        {
            ObjectId = objectId
        };

        await processor.DeleteMicroserviceAggregateRoot(input, cancellationToken);
        return true;
    }
}

GraphQL Types

// MicroserviceAggregateRoot.cs
public class MicroserviceAggregateRoot
{
    [GraphQLDescription("The unique identifier of the MicroserviceAggregateRoot")]
    public Guid ObjectId { get; set; }

    [GraphQLDescription("The name of the MicroserviceAggregateRoot")]
    public string? Name { get; set; }

    [GraphQLDescription("The status of the MicroserviceAggregateRoot")]
    public string? Status { get; set; }

    [GraphQLDescription("The creation date")]
    public DateTimeOffset CreatedOn { get; set; }

    // Nested types can be resolved
    [GraphQLDescription("Related entities")]
    public async Task<List<RelatedEntity>> GetRelatedEntities(
        [Parent] MicroserviceAggregateRoot parent,
        [Service] IRelatedEntityRetriever retriever,
        CancellationToken cancellationToken)
    {
        // Resolve nested data
        return await retriever.GetByAggregateRootId(parent.ObjectId, cancellationToken);
    }
}

// Input Types
public class CreateMicroserviceAggregateRootInput
{
    [GraphQLDescription("The unique identifier for the new MicroserviceAggregateRoot")]
    [GraphQLNonNullType]
    public Guid ObjectId { get; set; }

    [GraphQLDescription("Optional name for the MicroserviceAggregateRoot")]
    public string? Name { get; set; }
}

2. GraphQL Server Configuration

// GraphQLExtensions.cs
internal static IServiceCollection AddGraphQL(this IServiceCollection services)
{
    services
        .AddGraphQLServer()
        .AddQueryType(d => d.Name("Query"))
            .AddTypeExtension<MicroserviceAggregateRootQueries>()
        .AddMutationType(d => d.Name("Mutation"))
            .AddTypeExtension<MicroserviceAggregateRootMutations>()
        .AddSubscriptionType(d => d.Name("Subscription"))
            .AddTypeExtension<MicroserviceAggregateRootSubscriptions>()
        .AddType<MicroserviceAggregateRoot>()
        .AddFiltering()
        .AddSorting()
        .AddProjections()
        .AddAuthorization()
        .AddApolloTracing()
        .ModifyRequestOptions(opt => opt.IncludeExceptionDetails = true)
        .AddErrorFilter<GraphQLErrorFilter>();

    return services;
}

internal static IApplicationBuilder UseGraphQL(this IApplicationBuilder application)
{
    application.UseWebSockets(); // Required for subscriptions

    return application;
}

internal static IEndpointRouteBuilder MapGraphQL(this IEndpointRouteBuilder endpoints)
{
    endpoints
        .MapGraphQL()
        .RequireAuthorization(); // If authentication is required

    endpoints
        .MapGraphQLSchema()
        .RequireAuthorization();

    return endpoints;
}

3. Integration with CQRS

GraphQL queries map to Retrievers, mutations map to Processors:

// Query Resolver (Read Operation)
public class MicroserviceAggregateRootQueries
{
    public async Task<MicroserviceAggregateRoot?> GetMicroserviceAggregateRoot(
        Guid objectId,
        [Service] IMicroserviceAggregateRootsRetriever retriever, // Query side
        CancellationToken cancellationToken)
    {
        var input = new GetMicroserviceAggregateRootDetailsInput { ObjectId = objectId };
        var entity = await retriever.GetMicroserviceAggregateRootDetails(input, cancellationToken);
        return MapToGraphQLType(entity);
    }
}

// Mutation Resolver (Write Operation)
public class MicroserviceAggregateRootMutations
{
    public async Task<MicroserviceAggregateRoot> CreateMicroserviceAggregateRoot(
        CreateMicroserviceAggregateRootInput input,
        [Service] IMicroserviceAggregateRootsProcessor processor, // Command side
        CancellationToken cancellationToken)
    {
        var domainInput = MapToDomainInput(input);
        var entity = await processor.CreateMicroserviceAggregateRoot(domainInput, cancellationToken);
        return MapToGraphQLType(entity);
    }
}

4. Subscriptions (Real-time Updates)

GraphQL subscriptions can integrate with messaging:

// MicroserviceAggregateRootSubscriptions.cs
[ExtendObjectType(OperationTypeNames.Subscription)]
public class MicroserviceAggregateRootSubscriptions
{
    [Subscribe]
    [Topic("MicroserviceAggregateRootCreated")]
    public MicroserviceAggregateRoot OnMicroserviceAggregateRootCreated(
        [EventMessage] MicroserviceAggregateRootCreatedEvent eventData)
    {
        return new MicroserviceAggregateRoot
        {
            ObjectId = eventData.ObjectId,
            // Map from event
        };
    }

    [Subscribe]
    [Topic("MicroserviceAggregateRootUpdated")]
    public MicroserviceAggregateRoot OnMicroserviceAggregateRootUpdated(
        [EventMessage] MicroserviceAggregateRootUpdatedEvent eventData)
    {
        return new MicroserviceAggregateRoot
        {
            ObjectId = eventData.ObjectId,
            // Map from event
        };
    }
}

// Publish events from domain services
public class MicroserviceAggregateRootsProcessor
{
    private readonly ITopicEventSender eventSender;

    public async Task<IMicroserviceAggregateRoot> CreateMicroserviceAggregateRoot(
        CreateMicroserviceAggregateRootInput input,
        CancellationToken token)
    {
        // Create aggregate...

        // Publish event for subscription
        await eventSender.SendAsync(
            "MicroserviceAggregateRootCreated",
            new MicroserviceAggregateRootCreatedEvent { ObjectId = entity.ObjectId },
            token);

        return entity;
    }
}

5. Advanced Features

Filtering

// Enable filtering on queries
[UseFiltering]
public async Task<IQueryable<MicroserviceAggregateRoot>> GetMicroserviceAggregateRoots(
    [Service] IMicroserviceAggregateRootsRepository repository)
{
    return repository.AsQueryable();
}

// Query example:
// {
//   microserviceAggregateRoots(
//     where: {
//       status: { eq: "Active" }
//       createdOn: { gte: "2024-01-01" }
//     }
//   ) {
//     objectId
//     name
//   }
// }

Sorting

[UseSorting]
public async Task<IQueryable<MicroserviceAggregateRoot>> GetMicroserviceAggregateRoots(
    [Service] IMicroserviceAggregateRootsRepository repository)
{
    return repository.AsQueryable();
}

// Query example:
// {
//   microserviceAggregateRoots(
//     order: [
//       { createdOn: DESC }
//       { name: ASC }
//     ]
//   ) {
//     objectId
//     name
//   }
// }

Pagination

[UsePaging]
public async Task<Connection<MicroserviceAggregateRoot>> GetMicroserviceAggregateRoots(
    [Service] IMicroserviceAggregateRootsRepository repository)
{
    var items = await repository.GetAllAsync();
    return items.Select(MapToGraphQLType).ToConnection();
}

// Query example:
// {
//   microserviceAggregateRoots(
//     first: 10
//     after: "cursor"
//   ) {
//     edges {
//       node {
//         objectId
//         name
//       }
//       cursor
//     }
//     pageInfo {
//       hasNextPage
//       hasPreviousPage
//     }
//   }
// }

Authorization

[Authorize]
public async Task<MicroserviceAggregateRoot> GetMicroserviceAggregateRoot(
    Guid objectId,
    [Service] IMicroserviceAggregateRootsRetriever retriever)
{
    // Only authenticated users can access
}

[Authorize(Policy = "AdminOnly")]
public async Task<MicroserviceAggregateRoot> DeleteMicroserviceAggregateRoot(
    Guid objectId,
    [Service] IMicroserviceAggregateRootsProcessor processor)
{
    // Only admins can delete
}

6. Error Handling

// GraphQLErrorFilter.cs
public class GraphQLErrorFilter : IErrorFilter
{
    public IError OnError(IError error)
    {
        if (error.Exception is DomainException domainException)
        {
            return ErrorBuilder
                .New()
                .SetMessage(domainException.Message)
                .SetCode("DOMAIN_ERROR")
                .SetExtension("errorCode", domainException.ErrorCode)
                .Build();
        }

        if (error.Exception is ValidationException validationException)
        {
            return ErrorBuilder
                .New()
                .SetMessage("Validation failed")
                .SetCode("VALIDATION_ERROR")
                .SetExtension("validationErrors", validationException.Errors)
                .Build();
        }

        // Log unexpected errors
        // Return generic error message in production
        return error;
    }
}

Query Examples

Simple Query

query GetMicroserviceAggregateRoot {
  microserviceAggregateRoot(objectId: "123e4567-e89b-12d3-a456-426614174000") {
    objectId
    name
    status
    createdOn
  }
}

Query with Filters

query SearchMicroserviceAggregateRoots {
  microserviceAggregateRoots(
    where: {
      status: { eq: "Active" }
      createdOn: { gte: "2024-01-01T00:00:00Z" }
    }
    order: [{ createdOn: DESC }]
    first: 20
  ) {
    edges {
      node {
        objectId
        name
        status
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

Mutation

mutation CreateMicroserviceAggregateRoot {
  createMicroserviceAggregateRoot(
    input: {
      objectId: "123e4567-e89b-12d3-a456-426614174000"
      name: "New Aggregate"
    }
  ) {
    objectId
    name
    status
    createdOn
  }
}

Subscription

subscription OnMicroserviceAggregateRootCreated {
  onMicroserviceAggregateRootCreated {
    objectId
    name
    status
    createdOn
  }
}

Best Practices

Do's

  1. Leverage CQRS separation
  2. Use Retrievers for queries
  3. Use Processors for mutations
  4. Maintain clear separation

  5. Use type-safe resolvers

  6. Leverage Hot Chocolate's code-first approach
  7. Use strongly-typed inputs and outputs
  8. Enable schema validation

  9. Implement field-level authorization

  10. Protect sensitive fields
  11. Use role-based access control
  12. Validate permissions in resolvers

  13. Optimize data fetching

  14. Use DataLoader for batch loading
  15. Implement field-level resolvers for nested data
  16. Consider projection optimizations

  17. Provide clear error messages

  18. Use custom error filters
  19. Include error codes for client handling
  20. Don't expose internal implementation details

  21. Document your schema

  22. Use [GraphQLDescription] attributes
  23. Provide examples in documentation
  24. Keep schema documentation up to date

Don'ts

  1. Don't bypass domain services
  2. Always go through Processors/Retrievers
  3. Don't access repositories directly from resolvers
  4. Maintain architectural boundaries

  5. Don't expose internal implementation

  6. Map domain entities to GraphQL types
  7. Don't expose database structure
  8. Use DTOs for client-facing types

  9. Don't create circular dependencies

  10. Use DataLoader for relationships
  11. Avoid N+1 query problems
  12. Optimize nested queries

  13. Don't ignore performance

  14. Implement pagination for large datasets
  15. Use filtering and sorting efficiently
  16. Consider query complexity limits

Testing GraphQL

Unit Testing Resolvers

[Fact]
public async Task GetMicroserviceAggregateRoot_Should_Return_Entity()
{
    // Arrange
    var mockRetriever = new Mock<IMicroserviceAggregateRootsRetriever>();
    var resolver = new MicroserviceAggregateRootQueries();
    var entity = Mock.Of<IMicroserviceAggregateRoot>(e => e.ObjectId == Guid.NewGuid());

    mockRetriever
        .Setup(r => r.GetMicroserviceAggregateRootDetails(
            It.IsAny<GetMicroserviceAggregateRootDetailsInput>(),
            It.IsAny<CancellationToken>()))
        .ReturnsAsync(entity);

    // Act
    var result = await resolver.GetMicroserviceAggregateRoot(
        entity.ObjectId,
        mockRetriever.Object,
        CancellationToken.None);

    // Assert
    Assert.NotNull(result);
    Assert.Equal(entity.ObjectId, result.ObjectId);
}

Integration Testing

[Fact]
public async Task GraphQL_Query_Should_Return_Data()
{
    // Arrange
    var client = _factory.CreateClient();
    var query = @"
        query {
            microserviceAggregateRoot(objectId: ""123e4567-e89b-12d3-a456-426614174000"") {
                objectId
                name
            }
        }";

    // Act
    var response = await client.PostAsync("/graphql", 
        new StringContent(
            JsonSerializer.Serialize(new { query }),
            Encoding.UTF8,
            "application/json"));

    // Assert
    response.EnsureSuccessStatusCode();
    var content = await response.Content.ReadAsStringAsync();
    var result = JsonSerializer.Deserialize<GraphQLResponse>(content);
    Assert.NotNull(result.Data);
}

Configuration

GraphQL Options

{
  "GraphQLOptions": {
    "EnableGraphQL": true,
    "EnableGraphQLPlayground": true,
    "EnableGraphQLVoyager": false,
    "MaxQueryDepth": 10,
    "MaxQueryComplexity": 1000
  }
}

Dependency Injection

// Register GraphQL services
services.AddGraphQL();

// Register domain services (Processors/Retrievers)
services.AddMicroserviceDomainModel();

// Register repositories
services.AddMicroservicePersistenceModel();

Summary

GraphQL in the ConnectSoft Microservice Template provides:

  • CQRS Integration: Queries use Retrievers, mutations use Processors
  • Type Safety: Strong typing with Hot Chocolate
  • Flexibility: Clients request only needed fields
  • Real-time Updates: Subscription support
  • Advanced Features: Filtering, sorting, pagination
  • Authorization: Field-level and operation-level security
  • Performance: DataLoader support, query optimization

By following these patterns, GraphQL becomes a powerful alternative service model that maintains architectural integrity while providing flexible, efficient data access.