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¶
- Leverage CQRS separation
- Use Retrievers for queries
- Use Processors for mutations
-
Maintain clear separation
-
Use type-safe resolvers
- Leverage Hot Chocolate's code-first approach
- Use strongly-typed inputs and outputs
-
Enable schema validation
-
Implement field-level authorization
- Protect sensitive fields
- Use role-based access control
-
Validate permissions in resolvers
-
Optimize data fetching
- Use DataLoader for batch loading
- Implement field-level resolvers for nested data
-
Consider projection optimizations
-
Provide clear error messages
- Use custom error filters
- Include error codes for client handling
-
Don't expose internal implementation details
-
Document your schema
- Use
[GraphQLDescription]attributes - Provide examples in documentation
- Keep schema documentation up to date
Don'ts¶
- Don't bypass domain services
- Always go through Processors/Retrievers
- Don't access repositories directly from resolvers
-
Maintain architectural boundaries
-
Don't expose internal implementation
- Map domain entities to GraphQL types
- Don't expose database structure
-
Use DTOs for client-facing types
-
Don't create circular dependencies
- Use DataLoader for relationships
- Avoid N+1 query problems
-
Optimize nested queries
-
Don't ignore performance
- Implement pagination for large datasets
- Use filtering and sorting efficiently
- 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.