Skip to main content
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.
Send
Task
Sends a command without expecting a response.Parameters:
  • request: The command implementing IRequest
  • cancellationToken: Optional cancellation token
Send<TResponse>
Task<TResponse>
Sends a command or query and returns a response.Parameters:
  • request: The command/query implementing IRequest<TResponse>
  • cancellationToken: Optional cancellation token
Publish<TNotification>
Task
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.
PageIndex
uint
The page number (0-based or 1-based depending on implementation).
PageSize
uint
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.
Items
IEnumerable<TEntity>
The collection of items for the current page.
Count
long
Total count of items across all pages.
PageIndex
uint
The current page index.
PageSize
uint
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.

Build docs developers (and LLMs) love