CQRS is the concept of separation of concerns between reading data and writing data. At a system level, this can mean that you can independently scale and optimize for reads and writes. At a code level, this generally means focusing on modeling user interactions within your domain, and modeling commands and queries rather than just exposing a data model.
This approach fits the bill when working on a major refactor of DealerOn’s “Ignition” OpenID Connect service. The application handles sign-in and sign-out, but also provides an API for creating and managing users. Different user roles have permission to see a different information and have varying levels of access to the API. In the first few iterations of the refactor, I worked with a series of services which exposed read/write operations on a user, but no matter how much I cleaned up and pared down the code, it never quite felt right. Change tracking was awkward, and the variety of distinct scenarios for managing users lead to a rapidly ballooning mash up of services which handled specific flows, but with no real consistency between them.
This is where CQRS comes in. Modeling user interactions as commands and queries, rather than as reads/writes meant that commands could follow a uniform shape and leaves queries free to be optimized for each read model. This ended up providing a wide range of benefits, including implicit and consistent change tracking, easy testing, and improved consistency in the code. This post will focus mainly of the command side of CQRS and the patterns we used.
We start with an abstract
Command class. Each distinct user interaction is defined as a class that extends from
abstract class Command
IHandler<TCommand> interface defines how we’ll handle each command.
interface IHandler<TCommand> where TCommand : Command
Task<Outcome> Handle(TCommand command);
Finally, an abstract
Outcome class defines the outcome of a command. This allows you to define descriptive outcomes beyond just a success or an exception, and…