Command and Query Responsibility Segregation (CQRS)

Use Case: When applications change frequently, you can updated the Queries & Commands without affecting each other. When the Application is read heavy, you can then split the database into a Read (performance tuned for read) and Write (Fully normalized)

Definition

“The Command and Query Responsibility Segregation (CQRS) pattern separates read and update operations for a data store. Implementing CQRS in your application can maximize its performance, scalability, and security. The flexibility created by migrating to CQRS allows a system to better evolve over time and prevents update commands from causing merge conflicts at the domain level.”

CQRS has 3 building blocks

Building block Description
Command / Query All the data we need to execute the Query or Command. This is represented as an object instance.
Handler Business logic to execute the Command or Query. This returns a response.
Response Return data that we want to return encapsulated as an object.

MediatR

All of the examples below are from Jonathan Williams: CQRS using C# and MediatR and use .Net 5 and C# 9 but the principle will carry to others.

Dependancy libraries

Jonathan keeps the code together in a static class for findability and describes this as a container for the building blocks. Type of record is recommended for Query, Command and Responses as they are immutable, these are DTO’s (data transfer objects) so we dont want them to change.

Implementation

All the code below has been simplified for brevity, the full source is on github. The Repository is just a data repository that returns a List<Todo> Todos where type Todo is a domain entity with properties Id, Name and Complete.

  1. In a Command folder create the static class AddTodo
  2. Add the record Command, class Handler and record Response
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
namespace CQRSTest.Commands
{
public static class AddTodo
{
// Command
public record Command(string Name) : IRequest<int>;

// Handler
public class Handler : IRequestHandler<Command, int>
{
private readonly Repository repository;

public Handler(Repository repository)
{
this.repository = repository;
}

public async Task<Response> Handle(Command request, CancellationToken cancellationToken)
{
var hardCodedId = 42; // we dont have a real database for this demo
var todo = new Todo { Id = hardCodedId, Name = request.Name };
repository.Todos.Add(todo);

return hardCodedId;
}
}

// Response (the code above just returned an int, I included this for completeness as the handler could return a complex type)
public record Response(int Id);
}
}
  1. In a Queries folder create the static class GetTodoById.
  2. Add the record Query, Handler and record Response
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
namespace CQRSTest.Queries
{
public static class GetTodoById
{
// Query
public record Query(int Id) : IRequest<Response>;

// Handler
public class Handler : IRequestHandler<Query, Response>
{
private readonly Repository repository;

public Handler(Repository repository)
{
this.repository = repository;
}

public async Task<Response> Handle(Query request, CancellationToken cancellationToken)
{
var todo = repository.Todos.FirstOrDefault(x => x.Id == request.Id);
return todo == null ? null : new Response { Id = todo.Id, Name = todo.Name, Completed = todo.Completed };
}
}

// Response
public record Response (int Id, string Name, bool Completed);
}
}
  1. In the controller use constructor injection to inject IMediator mediator, then use mediator.Send(requestObject) to infer the relative Command or Query.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
namespace CQRSTest.Controllers
{
[ApiController]
public class TodoController : ControllerBase
{
private readonly IMediator mediator;

public TodoController(IMediator mediator)
{
this.mediator = mediator;
}

[HttpGet("/{id}")]
public async Task<IActionResult> GetTodoById(int id)
{
var query = new GetTodoById.Query(id);
var response = await mediator.Send(query);
return response == null ? NotFound() : Ok(response);
}

[HttpPost("")]
public async Task<IActionResult> AddTodo(AddTodo.Command command) => Ok(await mediator.Send(command));
}
}
  1. In the application startup pipeline add Mediator to the dependancy injection container using the extension method AddMediatR. Passing the assembly Startup tells mediator to use refection to include our classes above in the dependancy injection container (as they are in the same assembly as Startup). It will be looking for the IRequestHandler interface.

The order is important, any Repository being used needs to be added to the container first as this will be resolved/injected into the handlers constructor.

1
2
services.AddSingleton<Repository>();
services.AddMediatR(typeof(Startup).Assembly);

Jonathan did a sweet video covering the code above.

Hand rolling

MediatR is great but I think its best to introduce a library when it solves a problem, you can just as easily hand roll CQRS into your application. Always group though by feature (what it actually does).

  1. Setup your folder structure, here my feature is “Files”
1
2
src/MySweetApp/Commands/Files
src/MySweetApp/Queries/Files

The steps going forward are the same for Commands (ie: go do something, could be creating a resource) and Queries (ie: go fetch something), so I will just focus on a Command

  1. Create your class files in src/MySweetApp/Commands/Files
1
2
3
DeleteFileCommand.cs
DeleteFileCommandHandler.cs
IDeleteFileCommandHandler.cs
  1. The command encapsulates the data of the request and can be extended with properties, here its just a simple file id. Its best only construct the object though its constructor, so the public FileId only has a get.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System;

namespace MySweetApp.Commands.Files
{
public class DeleteFileCommand
{
public DeleteFileCommand(Guid fileId)
{
FileId = fileId;
}

public Guid FileId { get; }
}
}
  1. Define the interface, if you have many you could create an Interfaces folder, you can make HandleAsync more descriptive if you want, however the command should do only ONE thing and if you start adding more functionality you are not really thinking about Single Responsibility Principle (SRP).
1
2
3
4
5
6
7
8
9
using System.Threading.Tasks;

namespace MySweetApp.Commands.Files
{
public interface IDeleteFileCommandHandler
{
Task HandleAsync(DeleteFileCommand command);
}
}
  1. Define the command itself
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
namespace MySweetApp.Commands.Files
{
public class DeleteFileCommandHandler : IDeleteFileCommandHandler
{
private readonly IFileRepository _fileRepository;
private readonly IRequestContext _requestContext;
private readonly IBlobRepository _blobRepository;

public DeleteFileCommandHandler(IFileRepository fileRepository, IRequestContext requestContext, IBlobRepository blobRepository)
{
_fileRepository = fileRepository;
_requestContext = requestContext;
_blobRepository = blobRepository;
}

public async Task HandleAsync(DeleteFileCommand command)
{
// find the file in fileRepository based on
// command.FileId
// _requestContext.Identifier ~ this is something the application knows about the user/app making the request, normally a tenant UUID

// delete
// in _blobRepository
// in _fileRepository ~ some systems set a soft delete flag
}
}
}
  1. So then to consume inject IDeleteFileCommandHandler into your calling code, for me it was in the controller, create the DeleteFileCommand instance and call HandleAsync. This is then easily unit tested because you dont need to mock heaps of things.

  2. You just need to wire up the DI resolution of DeleteFileCommandHandler to IDeleteFileCommandHandler, MediatR would have done that for you as a benifit.

The DeleteFileCommand above returned NoContent(); but you could have a CreateFileCommand where it has more properties and returns the created file object, this would be a public API model (DTO)

Tests to enforce naming see Enforce command handler naming

References