The Command Query Bus provides a clean separation between write operations (commands) and read operations (queries) following the CQRS pattern. It’s built on top of MediatR and provides a unified interface for handling commands, queries, and domain notifications.
Core Interfaces
ICommandQueryBus
The main bus interface that dispatches commands and queries to their respective handlers.
Sends a command without expecting a response. Parameters:
request: The command implementing IRequest
cancellationToken: Optional cancellation token
Sends a command or query and returns a response. Parameters:
request: The command/query implementing IRequest<TResponse>
cancellationToken: Optional cancellation token
Publishes a domain notification to multiple handlers. Parameters:
notification: The notification implementing INotification
cancellationToken: Optional cancellation token
Core.Application.ComandQueryBus/Buses/ICommandQueryBus.cs
using MediatR ;
namespace Core . Application
{
public interface ICommandQueryBus
{
Task Publish < TNotification >( TNotification notification , CancellationToken cancellationToken = default )
where TNotification : INotification ;
Task Send ( IRequest request , CancellationToken cancellationToken = default );
Task < TResponse > Send < TResponse >( IRequest < TResponse > request , CancellationToken cancellationToken = default );
}
}
Command Interfaces
IRequestCommand
Marker interface for commands that don’t return a value.
Core.Application.ComandQueryBus/Commands/IRequestCommand.cs
public interface IRequestCommand : IRequest
{
}
IRequestCommand<TResponse>
Interface for commands that return a response value.
public interface IRequestCommand < out TResponse > : IRequest < TResponse >
{
}
IRequestCommandHandler
Handler interface for processing commands.
Core.Application.ComandQueryBus/Commands/IRequestCommandHandler.cs
public interface IRequestCommandHandler < in TRequest > : IRequestHandler < TRequest >
where TRequest : IRequest
{
}
public interface IRequestCommandHandler < TRequest , TResponse > : IRequestHandler < TRequest , TResponse >
where TRequest : IRequest < TResponse >
{
}
Query Interfaces
IRequestQuery
Marker interface for queries that don’t return a value (rare case).
Core.Application.ComandQueryBus/Queries/IRequestQuery.cs
public interface IRequestQuery : IRequest
{
}
IRequestQuery<TResponse>
Interface for queries that return a response value.
public interface IRequestQuery < out TResponse > : IRequest < TResponse >
{
}
IRequestQueryHandler
Handler interface for processing queries.
Core.Application.ComandQueryBus/Queries/IRequestQueryHandler.cs
public interface IRequestQueryHandler < in TRequest > : IRequestHandler < TRequest >
where TRequest : IRequest
{
}
public interface IRequestQueryHandler < TRequest , TResponse > : IRequestHandler < TRequest , TResponse >
where TRequest : IRequest < TResponse >
{
}
Query Helpers
QueryRequest<TResponse>
Base class for paginated queries.
The page number (0-based or 1-based depending on implementation).
Number of items per page.
Core.Application.ComandQueryBus/Queries/QueryRequest.cs
public class QueryRequest < TResponse > : IRequestQuery < TResponse >
where TResponse : class
{
public uint PageIndex { get ; set ; }
public uint PageSize { get ; set ; }
}
QueryResult<TEntity>
Standard response for paginated queries.
The collection of items for the current page.
Total count of items across all pages.
Number of items per page.
Core.Application.ComandQueryBus/Queries/QueryResult.cs
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 ;
}
}
Registration
Register the Command Query Bus in your service collection:
Application/Registrations/ApplicationServicesRegistration.cs
public static IServiceCollection AddApplicationServices ( this IServiceCollection services )
{
// Register MediatR and scan for handlers
services . AddMediatR ( config =>
config . RegisterServicesFromAssembly ( Assembly . GetExecutingAssembly ()));
// Register the command query bus
services . AddScoped < ICommandQueryBus , MediatrCommandQueryBus >();
return services ;
}
Creating a Command
Define a command that creates a new entity:
Application/UseCases/DummyEntity/Commands/CreateDummyEntity/CreateDummyEntityCommand.cs
using Core . Application ;
using System . ComponentModel . DataAnnotations ;
public class CreateDummyEntityCommand : IRequestCommand < string >
{
[ Required ]
public string dummyPropertyOne { get ; set ; }
public DummyValues dummyPropertyTwo { get ; set ; }
}
Command Handler
Implement the handler for the command:
Application/UseCases/DummyEntity/Commands/CreateDummyEntity/CreateDummyEntityHandler.cs
using Core . Application ;
internal sealed class CreateDummyEntityHandler : IRequestCommandHandler < CreateDummyEntityCommand , string >
{
private readonly ICommandQueryBus _domainBus ;
private readonly IDummyEntityRepository _repository ;
private readonly IDummyEntityApplicationService _applicationService ;
public CreateDummyEntityHandler (
ICommandQueryBus domainBus ,
IDummyEntityRepository repository ,
IDummyEntityApplicationService applicationService )
{
_domainBus = domainBus ?? throw new ArgumentNullException ( nameof ( domainBus ));
_repository = repository ?? throw new ArgumentNullException ( nameof ( repository ));
_applicationService = applicationService ?? throw new ArgumentNullException ( nameof ( applicationService ));
}
public async Task < string > Handle ( CreateDummyEntityCommand request , CancellationToken cancellationToken )
{
var entity = new DummyEntity ( request . dummyPropertyOne , request . dummyPropertyTwo );
if ( ! entity . IsValid )
throw new InvalidEntityDataException ( entity . GetErrors ());
if ( _applicationService . DummyEntityExist ( entity . Id ))
throw new EntityDoesExistException ();
object createdId = await _repository . AddAsync ( entity );
// Publish domain event
await _domainBus . Publish ( entity . To < DummyEntityCreated >(), cancellationToken );
return createdId . ToString ();
}
}
Creating a Query
Define a query that retrieves all entities with pagination:
Application/UseCases/DummyEntity/Queries/GetAllDummyEntities/GetAllDummyEntitiesQuery.cs
using Core . Application ;
public class GetAllDummyEntitiesQuery : QueryRequest < QueryResult < DummyEntityDto >>
{
}
Query Handler
Implement the handler for the query:
Application/UseCases/DummyEntity/Queries/GetAllDummyEntities/GetAllDummyEntitiesHandler.cs
using Core . Application ;
internal class GetAllDummyEntitiesHandler : IRequestQueryHandler < GetAllDummyEntitiesQuery , QueryResult < DummyEntityDto >>
{
private readonly IDummyEntityRepository _repository ;
public GetAllDummyEntitiesHandler ( IDummyEntityRepository repository )
{
_repository = repository ?? throw new ArgumentNullException ( nameof ( repository ));
}
public async Task < QueryResult < DummyEntityDto >> Handle (
GetAllDummyEntitiesQuery request ,
CancellationToken cancellationToken )
{
IList < DummyEntity > entities = await _repository . FindAllAsync ();
return new QueryResult < DummyEntityDto >(
entities . To < DummyEntityDto >(),
entities . Count ,
request . PageIndex ,
request . PageSize );
}
}
Usage in Controllers
Inject and use the Command Query Bus in your API controllers:
Template-API/Controllers/DummyEntityController.cs
using Core . Application ;
using Microsoft . AspNetCore . Mvc ;
[ ApiController ]
public class DummyEntityController : BaseController
{
private readonly ICommandQueryBus _commandQueryBus ;
public DummyEntityController ( ICommandQueryBus commandQueryBus )
{
_commandQueryBus = commandQueryBus ?? throw new ArgumentNullException ( nameof ( commandQueryBus ));
}
[ HttpGet ( "api/v1/[Controller]" )]
public async Task < IActionResult > GetAll ( uint pageIndex = 1 , uint pageSize = 10 )
{
var entities = await _commandQueryBus . Send (
new GetAllDummyEntitiesQuery
{
PageIndex = pageIndex ,
PageSize = pageSize
});
return Ok ( entities );
}
[ HttpPost ( "api/v1/[Controller]" )]
public async Task < IActionResult > Create ( CreateDummyEntityCommand command )
{
if ( command is null ) return BadRequest ();
var id = await _commandQueryBus . Send ( command );
return Created ( $"api/[Controller]/ { id } " , new { Id = id });
}
[ HttpPut ( "api/v1/[Controller]" )]
public async Task < IActionResult > Update ( UpdateDummyEntityCommand command )
{
if ( command is null ) return BadRequest ();
await _commandQueryBus . Send ( command );
return NoContent ();
}
}
Benefits
Separation of Concerns Commands handle writes, queries handle reads, keeping concerns separate.
Testability Each handler can be unit tested independently from the infrastructure.
Validation Centralized validation and error handling through MediatR pipeline behaviors.
Scalability Query and command paths can be optimized and scaled independently.
Best Practices
Keep commands and queries simple. Each should represent a single use case or business operation.
Commands should never return domain entities directly. Return IDs or DTOs instead to avoid tight coupling.
Use QueryRequest<T> and QueryResult<T> for consistent pagination across your application.