Skip to main content

Overview

CQRS (Command Query Responsibility Segregation) is a pattern that separates read and write operations into distinct models. Intent Architect implements CQRS using the MediatR library through the Intent.Application.MediatR module, generating all the necessary infrastructure for commands, queries, and their handlers.

Core Principle

Commands change state and don’t return data. Queries return data and don’t change state. This separation enables independent optimization, scaling, and reasoning about each operation type.

Why Use CQRS?

Separation of Concerns

Read and write models can be optimized independently for their specific purposes

Scalability

Scale read and write operations separately based on actual load patterns

Pipeline Behaviors

Apply cross-cutting concerns like validation, logging, and transactions per operation type

Loose Coupling

Mediator pattern reduces direct dependencies between components

Commands

Commands represent intentions to change the application’s state. They encapsulate all the information needed to perform an action.

Command Structure

public class CreateCustomerCommand : IRequest<Guid>, ICommand
{
    public CreateCustomerCommand(string name, string email)
    {
        Name = name;
        Email = email;
    }

    public string Name { get; set; }
    public string Email { get; set; }
}

Command Characteristics

  • Intent to Change State: Commands express what should happen
  • Return Minimal Data: Typically return only an ID or success indicator
  • Validation: Can be validated using FluentValidation before execution
  • Side Effects: Commands modify data, trigger events, or perform actions
Commands that don’t return a value implement IRequest (from MediatR). Commands that return a value implement IRequest<TResponse>.

Queries

Queries retrieve data without modifying state. They are optimized for reading and can return complex result sets.

Query Structure

public class GetCustomerByIdQuery : IRequest<CustomerDto>, IQuery
{
    public GetCustomerByIdQuery(Guid id)
    {
        Id = id;
    }

    public Guid Id { get; set; }
}

Query Characteristics

  • No State Changes: Queries are read-only operations
  • Return Data: Always return DTOs or view models
  • Optimized for Reading: Can use projections, includes, and database views
  • Idempotent: Calling the same query multiple times produces the same result

Modeling CQRS in Intent Architect

Commands and Queries are modeled in the Services Designer using the CQRS paradigm:
1

Create a Service Package

In the Services Designer, create or select a service package where your operations will be defined.
2

Add Commands

Create Command elements representing state-changing operations:
  • Right-click → New Command
  • Define properties for the command data
  • Specify return type (optional, typically Guid or void)
3

Add Queries

Create Query elements representing data retrieval operations:
  • Right-click → New Query
  • Define query parameters as properties
  • Specify return type (DTO or collection of DTOs)
4

Run Software Factory

Execute the Software Factory to generate:
  • Command/Query classes
  • Handler classes with constructor injection
  • Validation classes (if FluentValidation module is installed)
CQRS modeling in Services Designer

Generated Artifacts

The Intent.Application.MediatR module generates the following artifacts:

Core Interfaces

public interface ICommand : IRequest
{
}

public interface IQuery : IRequest
{
}
These marker interfaces help identify commands and queries throughout your application.

File Organization

Intent Architect provides flexible file organization through settings:
Each command/query has its own folder with handler and validator alongside:
Application/
├── Customers/
│   ├── CreateCustomer/
│   │   ├── CreateCustomerCommand.cs
│   │   ├── CreateCustomerCommandHandler.cs
│   │   └── CreateCustomerCommandValidator.cs
│   └── GetCustomerById/
│       ├── GetCustomerByIdQuery.cs
│       └── GetCustomerByIdQueryHandler.cs
Configure file consolidation in Application Settings → CQRS Settings → “Consolidate Command/Query associated files into single file”

MediatR Pipeline Behaviors

One of CQRS’s key benefits is the ability to apply cross-cutting concerns through MediatR pipeline behaviors.

Validation Behavior

public class ValidationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehaviour(IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        if (_validators.Any())
        {
            var context = new ValidationContext<TRequest>(request);
            var validationResults = await Task.WhenAll(
                _validators.Select(v => v.ValidateAsync(context, cancellationToken)));
            
            var failures = validationResults
                .SelectMany(r => r.Errors)
                .Where(f => f != null)
                .ToList();

            if (failures.Count != 0)
                throw new ValidationException(failures);
        }

        return await next();
    }
}

Transaction Behavior

public class UnitOfWorkBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IUnitOfWork _unitOfWork;

    public UnitOfWorkBehaviour(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        var response = await next();
        await _unitOfWork.SaveChangesAsync(cancellationToken);
        return response;
    }
}
Install Intent.Application.MediatR.Behaviours module to automatically generate common pipeline behaviors like logging, validation, and performance monitoring.

Integration with API Layer

Commands and queries integrate seamlessly with ASP.NET Core controllers:
[ApiController]
public class CustomersController : ControllerBase
{
    private readonly ISender _mediator;

    public CustomersController(ISender mediator)
    {
        _mediator = mediator;
    }

    [HttpPost("api/customers")]
    [ProducesResponseType(typeof(Guid), StatusCodes.Status201Created)]
    public async Task<ActionResult<Guid>> CreateCustomer(
        [FromBody] CreateCustomerCommand command,
        CancellationToken cancellationToken)
    {
        var customerId = await _mediator.Send(command, cancellationToken);
        return CreatedAtAction(
            nameof(GetCustomerById), 
            new { id = customerId }, 
            customerId);
    }

    [HttpGet("api/customers/{id}")]
    [ProducesResponseType(typeof(CustomerDto), StatusCodes.Status200OK)]
    public async Task<ActionResult<CustomerDto>> GetCustomerById(
        [FromRoute] Guid id,
        CancellationToken cancellationToken)
    {
        var customer = await _mediator.Send(
            new GetCustomerByIdQuery(id), 
            cancellationToken);
        return Ok(customer);
    }

    [HttpPut("api/customers/{id}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<ActionResult> UpdateCustomer(
        [FromRoute] Guid id,
        [FromBody] UpdateCustomerCommand command,
        CancellationToken cancellationToken)
    {
        if (id != command.Id)
            return BadRequest();

        await _mediator.Send(command, cancellationToken);
        return NoContent();
    }
}
The Intent.AspNetCore.Controllers.Dispatch.MediatR module automatically generates controllers that dispatch to MediatR handlers.

CQRS vs Traditional Services

When to Use:
  • Operations have distinct read/write optimization needs
  • You want pipeline behaviors (validation, logging, transactions)
  • Loose coupling via mediator pattern is important
  • Individual operations have complex, unique concerns
Module: Intent.Application.MediatRStructure:
// Each operation is a separate class
var customerId = await mediator.Send(new CreateCustomerCommand(...));
var customer = await mediator.Send(new GetCustomerByIdQuery(id));

Advanced Scenarios

CRUD Operations

The Intent.Application.MediatR.CRUD module generates complete CRUD operations:
// Auto-generated CRUD commands and queries
- CreateCustomerCommand / CreateCustomerCommandHandler
- UpdateCustomerCommand / UpdateCustomerCommandHandler
- DeleteCustomerCommand / DeleteCustomerCommandHandler
- GetCustomerByIdQuery / GetCustomerByIdQueryHandler
- GetCustomersQuery / GetCustomersQueryHandler

Property Default Values

Commands support default parameter values in constructors:
public class CreateOrderCommand : IRequest<Guid>, ICommand
{
    // Default values must come after required parameters
    public CreateOrderCommand(
        Guid customerId,
        string notes = "",
        OrderPriority priority = OrderPriority.Normal)
    {
        CustomerId = customerId;
        Notes = notes;
        Priority = priority;
    }

    public Guid CustomerId { get; set; }
    public string Notes { get; set; }
    public OrderPriority Priority { get; set; }
}
Properties with default values must be defined after properties without defaults, otherwise the default value won’t be applied to the constructor parameter.

MediatR Licensing

Starting with MediatR v13.0, a commercial license is required. Configure this in your application settings:
Enable “Use Pre-Commercial Version” setting to use MediatR v12.x (the last free version):
// No license key needed
License keys can be obtained from Jimmy Bogard’s announcement.

Testing CQRS Operations

Unit Testing Handlers

public class CreateCustomerCommandHandlerTests
{
    [Fact]
    public async Task Handle_ValidCommand_CreatesCustomer()
    {
        // Arrange
        var repository = new Mock<ICustomerRepository>();
        var handler = new CreateCustomerCommandHandler(repository.Object);
        var command = new CreateCustomerCommand("John Doe", "[email protected]");

        // Act
        var result = await handler.Handle(command, CancellationToken.None);

        // Assert
        Assert.NotEqual(Guid.Empty, result);
        repository.Verify(x => x.Add(It.IsAny<Customer>()), Times.Once);
    }
}

Integration Testing

public class CustomerIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public CustomerIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Fact]
    public async Task CreateCustomer_ReturnsCreatedCustomerId()
    {
        // Arrange
        var client = _factory.CreateClient();
        var command = new CreateCustomerCommand("Jane Doe", "[email protected]");

        // Act
        var response = await client.PostAsJsonAsync("/api/customers", command);

        // Assert
        response.EnsureSuccessStatusCode();
        var customerId = await response.Content.ReadFromJsonAsync<Guid>();
        Assert.NotEqual(Guid.Empty, customerId);
    }
}

FluentValidation

Intent.Application.MediatR.FluentValidation - Automatic validation for commands and queries

CRUD Generation

Intent.Application.MediatR.CRUD - Auto-generate common CRUD operations

Behaviors

Intent.Application.MediatR.Behaviours - Pre-built pipeline behaviors

Best Practices

Each command or query should do one thing:
  • Single Responsibility Principle applies
  • Easier to test and maintain
  • Clearer intent and naming
Commands change state and should be validated:
  • Use FluentValidation for command validation
  • Queries typically don’t need validation
  • Invalid query parameters can return empty results or 404
Never return domain entities from queries:
  • Map to DTOs using AutoMapper or Mapperly
  • DTOs can be optimized for specific views
  • Protects domain model from API changes
Apply cross-cutting concerns via behaviors:
  • Validation before execution
  • Logging and performance monitoring
  • Transaction management
  • Authorization checks

Additional Resources

Build docs developers (and LLMs) love