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 works nicely with C# 7 pattern matching to react to each distinct outcome.
case Success _:
case FirstTimeLogin _:
case InvalidCredentials _:
Behind the Scenes
We also added some simple middleware to automatically register handlers, manage database transactions for commands, and log and track the details and results of the commands while redacting sensitive information. This means that you get implicit logging, tracking, and analytics for any command that’s handled by the system. We also ship logged commands to Graylog, so it’s easy to do a quick query of how frequently users log in, failed login attempts, causes of errors, and more. I’ll cover the mechanics of this more in a future post as we generalize the framework a little bit more.
Handlers represent core business logic, so given a set of requirements, they’re easy to read and write.
public async Task<Outcome> Handle(CreateUserCommand command)
if (await Database.Exists(command.Email))
return new UserAlreadyExistsOutcome();
} var user = await Database.CreateUser(
command.Email, command.Name); var token = await Database.CreateVerificationToken(); await EmailService.SendWelcomeMessage(user.Email, token); return new SuccessOutcome();
Since commands essentially define a scenario, and handlers define business logic, they are easy to write high value unit tests for. We can test a scenario for each user persona and assert that the outcome is what we expected.
var handler = new CreateUserHandler(MockUserDb, MockEmailService);var command = new CreateUserCommand
Name = "Sample",
Email = "firstname.lastname@example.org"
}var outcome = await handler.Handle(command);Assert.OfType<Success>(outcome);Assert.True(MockEmailService.MessageSent);Assert.True(MockUserDb.TokenInserted)
Separating commands and queries is a great approach to explore when it feels like your code might not have the right levels of abstraction. Making clear commands which represent the scenarios in which users interact with your domain can make for readable, maintainable, and easily testable code.