Query Instruction Rule (CQRS, Clean Architecture, C#)
Purpose
Define how to implement Query objects and handlers in a C# microservices architecture using Clean Architecture and CQRS, following Martin Fowler and Udi Dahan.
Principles
- A Query represents a read-only business request (no state change).
- Queries are explicit, intention-revealing, immutable after creation.
- Queries return data relevant to the business use case.
- Queries are not reused for commands.
Structure
- Place Query objects and handlers in
Application/Queries/. - Prefer existing query interfaces from shared kernel or libraries when available.
- Create custom query interfaces only when shared ones donât exist.
- Each Query must implement an
IQuery<TResult>interface, and each handler must implement anIQueryHandler<TQuery, TResult>interface. - Use a dispatcher (e.g.
IQueryDispatcher) to decouple sender and handler, but only if one is not already present in the SharedKernel or provided by a library/framework.
Typical Folder Structure
Application/
[feature]/
GetOrderDetailsUseCase.cs
Queries/
GetOrderDetailsQuery.cs
GetOrderDetailsQueryHandler.cs
IQueryDispatcher.cs
Example (C#)
// Use existing interface from shared kernel if available
// Example: using SharedKernel.Queries.IQuery<TResult>
// Otherwise, define locally:
public interface IQuery<TResult> { }
// Query object (immutable)
public sealed class GetOrderDetailsQuery : IQuery<OrderDetailsDto>
{
public Guid OrderId { get; }
public GetOrderDetailsQuery(Guid orderId) => OrderId = orderId;
}
// Use existing handler interface from shared kernel if available
// Example: using SharedKernel.Queries.IQueryHandler<TQuery, TResult>
// Otherwise, define locally:
public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
Task<TResult> Handle(TQuery query, CancellationToken ct);
}
// Query handler
public sealed class GetOrderDetailsQueryHandler : IQueryHandler<GetOrderDetailsQuery, OrderDetailsDto>
{
public Task<OrderDetailsDto> Handle(GetOrderDetailsQuery query, CancellationToken ct)
{
// Data retrieval logic
}
}
// Dispatcher interface
public interface IQueryDispatcher
{
Task<TResult> Dispatch<TQuery, TResult>(TQuery query, CancellationToken ct) where TQuery : IQuery<TResult>;
}
Flow
- API or UseCase creates a Query and sends it to IQueryDispatcher.
- Dispatcher locates and invokes the correct handler.
- Handler retrieves data and returns result.
Best Practices
- One Query per business use case.
- No state change in query handlers.
- Use intention-revealing names.
- Use existing query interfaces from shared kernel or libraries when available.
- Create custom interfaces only when shared ones donât exist or donât fit the requirements.
- Document queries with business references.
References
- Martin Fowler, âCommand Query Separationâ https://martinfowler.com/bliki/CommandQuerySeparation.html
- Udi Dahan, âClarified CQRSâ https://udidahan.com/2009/12/09/clarified-cqrs/