Introduction
CQRS (Command Query Responsibility Segregation) is a pattern that separates read and write operations into distinct models. The Hybrid DDD Architecture implements CQRS using MediatR to provide clear separation between commands (writes) and queries (reads).
CQRS Principles
Core Concept
CQRS is based on the principle that the methods that change system state (commands) should be separated from methods that read system state (queries).
Command: Writes → Change state → Returns success/ID
Query: Reads → No side effects → Returns data
Benefits
Scalability Read and write workloads can be scaled independently
Optimization Queries can be optimized differently from commands
Clarity Clear distinction between operations that change state vs read state
Flexibility Different models for reading and writing (e.g., denormalized read models)
Architecture Overview
The CQRS implementation in this template uses the following components:
Key Components
ICommandQueryBus : Mediator that routes commands/queries to handlers
Commands : Requests that change system state
Command Handlers : Process commands and coordinate domain logic
Queries : Requests that retrieve data
Query Handlers : Process queries and return data
Domain Events : Published after successful command execution
Commands
Commands represent operations that change the state of the system. They express user intent and trigger business processes.
Command Interface
All commands implement one of these interfaces:
// Command without return value
public interface IRequestCommand : IRequest
{
}
// Command with return value
public interface IRequestCommand < out TResponse > : IRequest < TResponse >
{
}
Source : Core.Application.CommandQueryBus/Commands/IRequestCommand.cs:5-11
Command Example
public class CreateDummyEntityCommand : IRequestCommand < string >
{
[ Required ]
public string dummyPropertyOne { get ; set ; }
public DummyValues dummyPropertyTwo { get ; set ; }
public CreateDummyEntityCommand ()
{
}
}
Source : Application/UseCases/DummyEntity/Commands/CreateDummyEntity/CreateDummyEntityCommand.cs:13-22
Command Characteristics
Commands use imperative names that express what should happen: CreateDummyEntityCommand, UpdateDummyEntityCommand, DeleteDummyEntityCommand.
Commands represent tasks or actions in the system, not just CRUD operations.
Can include data annotations for basic validation before reaching the handler.
Typically return IDs, success indicators, or result objects—not full entities.
More Command Examples
// Update command
public class UpdateDummyEntityCommand : IRequestCommand
{
[ Required ]
public string Id { get ; set ; }
[ Required ]
public string dummyPropertyOne { get ; set ; }
public DummyValues dummyPropertyTwo { get ; set ; }
}
// Delete command
public class DeleteDummyEntityCommand : IRequestCommand
{
[ Required ]
public string Id { get ; set ; }
}
Command Handlers
Command Handlers process commands and orchestrate the domain logic to fulfill the request.
Command Handler Interface
// Handler without return value
public interface IRequestCommandHandler < in TRequest > : IRequestHandler < TRequest >
where TRequest : IRequest
{
}
// Handler with return value
public interface IRequestCommandHandler < TRequest , TResponse > : IRequestHandler < TRequest , TResponse >
where TRequest : IRequest < TResponse >
{
}
Source : Core.Application.CommandQueryBus/Commands/IRequestCommandHandler.cs:5-13
Command Handler Example
internal sealed class CreateDummyEntityHandler : IRequestCommandHandler < CreateDummyEntityCommand , string >
{
private readonly ICommandQueryBus _domainBus ;
private readonly IDummyEntityRepository _context ;
private readonly IDummyEntityApplicationService _dummyEntityApplicationService ;
public CreateDummyEntityHandler (
ICommandQueryBus domainBus ,
IDummyEntityRepository dummyEntityRepository ,
IDummyEntityApplicationService dummyEntityApplicationService )
{
_domainBus = domainBus ?? throw new ArgumentNullException ( nameof ( domainBus ));
_context = dummyEntityRepository ?? throw new ArgumentNullException ( nameof ( dummyEntityRepository ));
_dummyEntityApplicationService = dummyEntityApplicationService ?? throw new ArgumentNullException ( nameof ( dummyEntityApplicationService ));
}
public async Task < string > Handle ( CreateDummyEntityCommand request , CancellationToken cancellationToken )
{
// 1. Create domain entity
Domain . Entities . DummyEntity entity = new ( request . dummyPropertyOne , request . dummyPropertyTwo );
// 2. Validate domain entity
if ( ! entity . IsValid )
throw new InvalidEntityDataException ( entity . GetErrors ());
// 3. Check business rules
if ( _dummyEntityApplicationService . DummyEntityExist ( entity . Id ))
throw new EntityDoesExistException ();
try
{
// 4. Persist entity
object createdId = await _context . AddAsync ( entity );
// 5. Publish domain event
await _domainBus . Publish ( entity . To < DummyEntityCreated >(), cancellationToken );
return createdId . ToString ();
}
catch ( Exception ex )
{
throw new BussinessException ( ApplicationConstants . PROCESS_EXECUTION_EXCEPTION , ex . InnerException );
}
}
}
Source : Application/UseCases/DummyEntity/Commands/CreateDummyEntity/CreateDummyEntityHandler.cs:17-43
Command Handler Workflow
A typical command handler follows this pattern:
Create/Retrieve Domain Entities : Instantiate or fetch domain objects
Validate : Ensure domain entity is in a valid state
Check Business Rules : Use application services to verify business constraints
Persist Changes : Save to the repository
Publish Events : Notify other parts of the system about what happened
Return Result : Return success indicator or created ID
Command handlers should not return full entities. Return IDs, success flags, or result objects instead.
Queries
Queries retrieve data from the system without causing side effects. They are read-only operations.
Query Interface
// Query without return value (rare)
public interface IRequestQuery : IRequest
{
}
// Query with return value (typical)
public interface IRequestQuery < out TResponse > : IRequest < TResponse >
{
}
Source : Core.Application.CommandQueryBus/Queries/IRequestQuery.cs:5-11
Query Base Class
For paginated queries, inherit from QueryRequest<TResponse>:
public class QueryRequest < TResponse > : IRequestQuery < TResponse >
where TResponse : class
{
public uint PageIndex { get ; set ; }
public uint PageSize { get ; set ; }
}
Source : Core.Application.CommandQueryBus/Queries/QueryRequest.cs:3-8
Query Examples
// Get all entities (paginated)
public class GetAllDummyEntitiesQuery : QueryRequest < QueryResult < DummyEntityDto >>
{
}
Source : Application/UseCases/DummyEntity/Queries/GetAllDummyEntities/GetAllDummyEntitiesQuery.cs:6-8
// Get single entity by ID
public class GetDummyEntityByQuery : IRequestQuery < DummyEntityDto >
{
[ Required ]
public string Id { get ; set ; }
}
Query Characteristics
Queries should never modify system state. They are purely read operations.
Queries return Data Transfer Objects (DTOs), not domain entities, to avoid exposing domain internals.
Can include parameters for filtering, sorting, and searching.
Query Handlers
Query Handlers process queries and return data, typically mapped to DTOs.
Query Handler Interface
// Handler without return value
public interface IRequestQueryHandler < in TRequest > : IRequestHandler < TRequest >
where TRequest : IRequest
{
}
// Handler with return value
public interface IRequestQueryHandler < TRequest , TResponse > : IRequestHandler < TRequest , TResponse >
where TRequest : IRequest < TResponse >
{
}
Source : Core.Application.CommandQueryBus/Queries/IRequestQueryHandler.cs:5-14
Query Handler Example
internal class GetAllDummyEntitiesHandler : IRequestQueryHandler < GetAllDummyEntitiesQuery , QueryResult < DummyEntityDto >>
{
private readonly IDummyEntityRepository _context ;
public GetAllDummyEntitiesHandler ( IDummyEntityRepository context )
{
_context = context ?? throw new ArgumentNullException ( nameof ( context ));
}
public async Task < QueryResult < DummyEntityDto >> Handle ( GetAllDummyEntitiesQuery request , CancellationToken cancellationToken )
{
// 1. Retrieve entities from repository
IList < Domain . Entities . DummyEntity > entities = await _context . FindAllAsync ();
// 2. Map to DTOs and return paginated result
return new QueryResult < DummyEntityDto >(
entities . To < DummyEntityDto >(),
entities . Count ,
request . PageIndex ,
request . PageSize );
}
}
Source : Application/UseCases/DummyEntity/Queries/GetAllDummyEntities/GetAllDummyEntitiesHandler.cs:7-17
Query Result
The QueryResult<T> class provides paginated responses:
public class QueryResult < TEntity >
where TEntity : class
{
public long Count { get ; private set ; }
public IEnumerable < TEntity > Items { get ; private set ; }
public uint PageIndex { get ; private set ; }
public uint PageSize { get ; private set ; }
public QueryResult ( IEnumerable < TEntity > items , long count , uint pageSize , uint pageIndex )
{
Items = items ;
Count = count ;
PageIndex = pageIndex ;
PageSize = pageSize ;
}
}
Source : Core.Application.CommandQueryBus/Queries/QueryResult.cs:3-17
Query Handler Workflow
Retrieve Data : Fetch entities from repository
Map to DTOs : Convert domain entities to data transfer objects
Apply Pagination : Use PageIndex and PageSize from request
Return Results : Return QueryResult with items and metadata
Query handlers should be lightweight and focused solely on data retrieval and mapping.
Command/Query Bus
The ICommandQueryBus is the central mediator that routes commands and queries to their handlers.
Interface
public interface ICommandQueryBus
{
// Publish notifications (domain events)
Task Publish < TNotification >( TNotification notification , CancellationToken cancellationToken = default )
where TNotification : INotification ;
// Send command/query without return value
Task Send ( IRequest request , CancellationToken cancellationToken = default );
// Send command/query with return value
Task < TResponse > Send < TResponse >( IRequest < TResponse > request , CancellationToken cancellationToken = default );
}
Source : Core.Application.CommandQueryBus/Buses/ICommandQueryBus.cs:5-10
Usage in API Controllers
[ ApiController ]
[ Route ( "api/[controller]" )]
public class DummyEntityController : ControllerBase
{
private readonly ICommandQueryBus _bus ;
public DummyEntityController ( ICommandQueryBus bus )
{
_bus = bus ;
}
// POST: api/DummyEntity
[ HttpPost ]
public async Task < IActionResult > Create ([ FromBody ] CreateDummyEntityCommand command )
{
var id = await _bus . Send ( command );
return CreatedAtAction ( nameof ( GetById ), new { id }, id );
}
// GET: api/DummyEntity
[ HttpGet ]
public async Task < IActionResult > GetAll ([ FromQuery ] GetAllDummyEntitiesQuery query )
{
var result = await _bus . Send ( query );
return Ok ( result );
}
// GET: api/DummyEntity/{id}
[ HttpGet ( "{id}" )]
public async Task < IActionResult > GetById ( string id )
{
var query = new GetDummyEntityByQuery { Id = id };
var result = await _bus . Send ( query );
return Ok ( result );
}
}
The command/query bus provides a clean abstraction layer between controllers and business logic.
Domain Events and Notifications
After successful command execution, domain events can be published to trigger side effects.
Domain Event
public class DomainEvent : IRequestNotification
{
public DateTime EventDateUtc { get ; private set ; }
public DomainEvent ()
{
EventDateUtc = DateTime . UtcNow ;
}
}
Source : Core.Application.CommandQueryBus/Notifications/DomainEvent.cs:3-10
Domain Event Example
internal sealed class DummyEntityCreated : DomainEvent
{
public string Id { get ; set ; }
public string DummyPropertyOne { get ; set ; }
public DummyValues DummyPropertyTwo { get ; set ; }
}
Source : Application/DomainEvents/DummyEntityCreated.cs:10-15
Event Handler
internal class NotificateDummyEntityCreatedHandler : IRequestNotificationHandler < DummyEntityCreated >
{
private readonly ILogger < NotificateDummyEntityCreatedHandler > _logger ;
public NotificateDummyEntityCreatedHandler ( ILogger < NotificateDummyEntityCreatedHandler > logger )
{
_logger = logger ;
}
public Task Handle ( DummyEntityCreated notification , CancellationToken cancellationToken )
{
// Handle the event (e.g., send email, update cache, publish integration event)
_logger . LogInformation ( $"DummyEntity created with ID: { notification . Id } " );
return Task . CompletedTask ;
}
}
Publishing Events
Events are published from command handlers:
// In command handler
await _domainBus . Publish ( entity . To < DummyEntityCreated >(), cancellationToken );
Use domain events for in-process notifications and integration events for cross-service communication.
CQRS Best Practices
Command Best Practices
Each command should represent one business action or use case.
Validate commands at multiple levels: data annotations, application rules, and domain rules.
Consider making commands idempotent to handle retries safely.
Always publish domain events after successful state changes.
Query Best Practices
Queries must never modify state—they should be pure read operations.
Always return DTOs, never domain entities, to maintain encapsulation.
Optimize queries independently from commands (e.g., use read models, caching).
Handler Best Practices
Single Handler per Command/Query
Each command or query should have exactly one handler.
Use constructor injection for all dependencies.
Use specific exceptions for different error scenarios.
Use async operations for I/O-bound work (database, external APIs).
CQRS Flow Diagram
Command Flow
Query Flow
Advanced CQRS Patterns
Separate Read and Write Models
For high-scale applications, you can use different models for reads and writes:
Write Model : Domain entities with business logic
Read Model : Denormalized views optimized for queries
Synchronization : Use domain events to update read models
Event Sourcing
Combine CQRS with event sourcing:
Store events instead of current state
Rebuild state by replaying events
Natural audit trail
Time-travel debugging
Event sourcing adds complexity. Only use it when you have specific requirements for audit trails or temporal queries.
Testing CQRS Components
Testing Commands
[ Fact ]
public async Task CreateDummyEntity_ValidCommand_ReturnsId ()
{
// Arrange
var command = new CreateDummyEntityCommand
{
dummyPropertyOne = "Test" ,
dummyPropertyTwo = DummyValues . Value1
};
var mockRepo = new Mock < IDummyEntityRepository >();
var mockBus = new Mock < ICommandQueryBus >();
var mockService = new Mock < IDummyEntityApplicationService >();
mockRepo . Setup ( r => r . AddAsync ( It . IsAny < DummyEntity >()))
. ReturnsAsync ( "test-id" );
var handler = new CreateDummyEntityHandler ( mockBus . Object , mockRepo . Object , mockService . Object );
// Act
var result = await handler . Handle ( command , CancellationToken . None );
// Assert
Assert . NotNull ( result );
mockRepo . Verify ( r => r . AddAsync ( It . IsAny < DummyEntity >()), Times . Once );
mockBus . Verify ( b => b . Publish ( It . IsAny < DummyEntityCreated >(), It . IsAny < CancellationToken >()), Times . Once );
}
Testing Queries
[ Fact ]
public async Task GetAllDummyEntities_ReturnsPagedResult ()
{
// Arrange
var query = new GetAllDummyEntitiesQuery
{
PageIndex = 0 ,
PageSize = 10
};
var mockRepo = new Mock < IDummyEntityRepository >();
var entities = new List < DummyEntity >
{
new DummyEntity ( "value1" , DummyValues . Value1 ),
new DummyEntity ( "value2" , DummyValues . Value2 )
};
mockRepo . Setup ( r => r . FindAllAsync ()). ReturnsAsync ( entities );
var handler = new GetAllDummyEntitiesHandler ( mockRepo . Object );
// Act
var result = await handler . Handle ( query , CancellationToken . None );
// Assert
Assert . Equal ( 2 , result . Count );
Assert . Equal ( 2 , result . Items . Count ());
}
Next Steps
Create Commands Learn how to implement commands and handlers
Create Queries Build queries for data retrieval
Domain Events Work with domain events and notifications
API Controllers Expose commands and queries via REST API