Skip to main content
The BuildingBlocks library provides CQRS (Command Query Responsibility Segregation) abstractions built on top of MediatR. These interfaces help separate write operations (commands) from read operations (queries).

Overview

CQRS separates the responsibility of handling commands that change state from queries that read state. This separation provides several benefits:
  • Clarity: Commands and queries have distinct interfaces and purposes
  • Scalability: Read and write operations can be optimized independently
  • Security: Different authorization rules can apply to commands vs queries
  • Testing: Easier to test and mock handlers

Command Interfaces

ICommand

The ICommand interface represents a command that doesn’t return a value (returns Unit).
BuildingBlocks/CQRS/ICommand.cs
using MediatR;

namespace BuildingBlocks.CQRS;

public interface ICommand : ICommand<Unit>
{
}

public interface ICommand<out TResponse> : IRequest<TResponse>
{
}
Usage Example:
public record CreateOrderCommand(
    Guid CustomerId,
    List<OrderItemDto> Items
) : ICommand<CreateOrderResult>;

public record DeleteOrderCommand(Guid OrderId) : ICommand;

ICommandHandler

The ICommandHandler interface defines handlers that process commands.
BuildingBlocks/CQRS/ICommandHandler.cs
using MediatR;

namespace BuildingBlocks.CQRS;

public interface ICommandHandler<in TCommand> 
    : ICommandHandler<TCommand, Unit>
    where TCommand : ICommand<Unit>
{ 
}

public interface ICommandHandler<in TCommand, TResponse> 
    : IRequestHandler<TCommand, TResponse> 
    where TCommand : ICommand<TResponse>
    where TResponse : notnull
{
}
Usage Example:
public class CreateOrderCommandHandler 
    : ICommandHandler<CreateOrderCommand, CreateOrderResult>
{
    private readonly IApplicationDbContext _context;
    
    public CreateOrderCommandHandler(IApplicationDbContext context)
    {
        _context = context;
    }
    
    public async Task<CreateOrderResult> Handle(
        CreateOrderCommand command, 
        CancellationToken cancellationToken)
    {
        var order = new Order
        {
            CustomerId = command.CustomerId,
            OrderDate = DateTime.UtcNow,
            Items = command.Items.Select(i => new OrderItem
            {
                ProductId = i.ProductId,
                Quantity = i.Quantity,
                Price = i.Price
            }).ToList()
        };
        
        _context.Orders.Add(order);
        await _context.SaveChangesAsync(cancellationToken);
        
        return new CreateOrderResult(order.Id);
    }
}

Query Interfaces

IQuery

The IQuery interface represents a query that returns data.
BuildingBlocks/CQRS/IQuery.cs
using MediatR;

namespace BuildingBlocks.CQRS;

public interface IQuery<out TResponse> : IRequest<TResponse>  
    where TResponse : notnull
{
}
Usage Example:
public record GetOrderByIdQuery(Guid OrderId) : IQuery<OrderDto>;

public record GetOrdersQuery(
    PaginationRequest Pagination
) : IQuery<PaginatedResult<OrderDto>>;

IQueryHandler

The IQueryHandler interface defines handlers that process queries.
BuildingBlocks/CQRS/IQueryHandler.cs
using MediatR;

namespace BuildingBlocks.CQRS;

public interface IQueryHandler<in TQuery, TResponse>
    : IRequestHandler<TQuery, TResponse>
    where TQuery : IQuery<TResponse>
    where TResponse : notnull
{
}
Usage Example:
public class GetOrderByIdQueryHandler 
    : IQueryHandler<GetOrderByIdQuery, OrderDto>
{
    private readonly IApplicationDbContext _context;
    private readonly IMapper _mapper;
    
    public GetOrderByIdQueryHandler(
        IApplicationDbContext context,
        IMapper mapper)
    {
        _context = context;
        _mapper = mapper;
    }
    
    public async Task<OrderDto> Handle(
        GetOrderByIdQuery query, 
        CancellationToken cancellationToken)
    {
        var order = await _context.Orders
            .AsNoTracking()
            .Include(o => o.Items)
            .FirstOrDefaultAsync(o => o.Id == query.OrderId, cancellationToken);
            
        if (order == null)
            throw new NotFoundException(nameof(Order), query.OrderId);
            
        return _mapper.Map<OrderDto>(order);
    }
}

Registration

Register MediatR and all handlers in your service’s DependencyInjection.cs:
public static IServiceCollection AddApplicationServices(
    this IServiceCollection services)
{
    services.AddMediatR(config =>
    {
        config.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
    });
    
    return services;
}

Usage in Controllers

Inject IMediator and send commands or queries:
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly IMediator _mediator;
    
    public OrdersController(IMediator mediator)
    {
        _mediator = mediator;
    }
    
    [HttpPost]
    public async Task<ActionResult<CreateOrderResult>> CreateOrder(
        [FromBody] CreateOrderCommand command)
    {
        var result = await _mediator.Send(command);
        return CreatedAtAction(nameof(GetOrder), new { id = result.OrderId }, result);
    }
    
    [HttpGet("{id}")]
    public async Task<ActionResult<OrderDto>> GetOrder(Guid id)
    {
        var query = new GetOrderByIdQuery(id);
        var result = await _mediator.Send(query);
        return Ok(result);
    }
}

Best Practices

Records provide immutability and value-based equality, making them ideal for commands and queries.
// Good
public record CreateProductCommand(string Name, decimal Price) : ICommand<Guid>;

// Avoid
public class CreateProductCommand : ICommand<Guid>
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}
Each handler should have a single responsibility. Don’t add multiple operations to one handler.
// Good - Single responsibility
public class CreateOrderCommandHandler : ICommandHandler<CreateOrderCommand, Guid>
{
    public async Task<Guid> Handle(CreateOrderCommand command, CancellationToken ct)
    {
        // Only create order
    }
}

// Avoid - Multiple responsibilities
public class CreateOrderCommandHandler : ICommandHandler<CreateOrderCommand, Guid>
{
    public async Task<Guid> Handle(CreateOrderCommand command, CancellationToken ct)
    {
        // Create order
        // Send email
        // Update inventory
        // Process payment
    }
}
Queries should be read-only operations. Use AsNoTracking() with Entity Framework.
public async Task<OrderDto> Handle(GetOrderByIdQuery query, CancellationToken ct)
{
    var order = await _context.Orders
        .AsNoTracking() // Read-only
        .FirstOrDefaultAsync(o => o.Id == query.OrderId, ct);
        
    return _mapper.Map<OrderDto>(order);
}
Commands should return only essential data (like ID), not full entities.
// Good
public record CreateOrderResult(Guid OrderId);

// Avoid
public record CreateOrderResult(
    Guid OrderId,
    DateTime CreatedAt,
    OrderStatus Status,
    List<OrderItem> Items,
    decimal Total
);

Validation Behavior

Automatically validate commands before execution

Exception Handling

Handle errors in command and query handlers

Build docs developers (and LLMs) love