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
.
- In a
Command
folder create the static classAddTodo
- Add the record
Command
, classHandler
and recordResponse
1 | namespace CQRSTest.Commands |
- In a
Queries
folder create the static classGetTodoById
. - Add the record
Query
,Handler
and recordResponse
1 | namespace CQRSTest.Queries |
- In the controller use constructor injection to inject
IMediator mediator
, then usemediator.Send(requestObject)
to infer the relativeCommand
orQuery
.
1 | namespace CQRSTest.Controllers |
- In the application startup pipeline add Mediator to the dependancy injection container using the extension method
AddMediatR
. Passing the assemblyStartup
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 theIRequestHandler
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 | services.AddSingleton<Repository>(); |
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).
- Setup your folder structure, here my feature is “Files”
1 | src/MySweetApp/Commands/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
- Create your class files in
src/MySweetApp/Commands/Files
1 | DeleteFileCommand.cs |
- 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 | using System; |
- Define the interface, if you have many you could create an
Interfaces
folder, you can makeHandleAsync
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 | using System.Threading.Tasks; |
- Define the command itself
1 | namespace MySweetApp.Commands.Files |
So then to consume inject
IDeleteFileCommandHandler
into your calling code, for me it was in the controller, create theDeleteFileCommand
instance and callHandleAsync
. This is then easily unit tested because you dont need to mock heaps of things.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