Simple CQRS in ASP.NET Core

Eric Damtoft
Feb 22, 2019 · 3 min read

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.

Photo by Hannah Joshua on Unsplash

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.

Building Blocks

Photo by Esther Jiao on Unsplash

We start with an abstract Command class. Each distinct user interaction is defined as a class that extends from Command.

abstract class Command
{
}

An 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.

switch (loginOutcome)
{
case Success _:
return Redirect(returnUrl);
case FirstTimeLogin _:
return RedirectToAction("AccountUpdate");
case InvalidCredentials _:
ModelState.AddModelError("Invalid Credentials");
return View(model);
// ...
}

Behind the Scenes

Command analytics and logging

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.

Handling Commands

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();
}

Testing

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);

Takeaways

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.

DealerOn Dev

Syndicated from https://dev.to/dealeron | On the DealerOn Dev Team, we strive to be the industry leader in code quality, innovation, and culture. The author’s views expressed in this publication are endorsed by DealerOn. The author’s views elsewhere may not be.

Thanks to sohjsolwin

Eric Damtoft

Written by

Software Architect at DealerOn

DealerOn Dev

Syndicated from https://dev.to/dealeron | On the DealerOn Dev Team, we strive to be the industry leader in code quality, innovation, and culture. The author’s views expressed in this publication are endorsed by DealerOn. The author’s views elsewhere may not be.

More From Medium

More from DealerOn Dev

More from DealerOn Dev

How to Write Readable Code

More from DealerOn Dev

More from DealerOn Dev

Let Your Code Do the Talking

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade