Command and Query Responsibility Segregation (CQRS)

WIP

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.

.Net 5 C# 9 & MediatR

All of the examples below are from Jonathan Williams: CQRS using C# and MediatR

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.

.Net 3.1

TODO

References