NOTE: This is Part 2 of a five-part series in which I detail how a real-world ASP.NET Web API app using the Command-Query Responsibility Segregation and Event Sourcing (CQRS/ES) patterns and the Redis database might look. Here's Part 1 of this series. The corresponding repository is over on GitHub.

Now that we've discussed why we want to use CQRS (Command-Query Responsibility Segregation) and ES (Event Sourcing) for this system, we can now start going into detail about how we can implement such an architecture. In this part of our Real-World CQRS/ES with ASP.NET and Redis series, we will:

  • Gather the requirements for our Write Model.
  • Learn about the order of operations for a CQRS application.
  • Build the commands and events necessary to implement our requirements AND
  • Build the command handlers and aggregate roots.

Let's get started!

Application Requirements

Let's imagine we run a network of restaurants. At each restaurant, we have employees who are employed to work at that location. A single location may have many employees, but each employee can only work at one location at a time. However, employees may switch locations (due to moves, staffing shortages, etc.). How can we model a CQRS/ES system which abides by these parameters?

Here are the business rules for this system:

  1. If an employee exists, s/he must be assigned to a location.
  2. Employees may switch locations, but they may not be assigned to more than one location at a time.
  3. We must be able to add new employees and new locations at any time.
  4. Each Employee and Location must have a simple-to-remember ID.

A Note On Proper Tense

When using CQRS terminology, it is important to use proper grammatical tenses when naming commands and events.

  • Commands are things that will happen in the future, so we name them using future tense and imperative (e.g. "X must be done").
  • Events are things that have already happened, so we name them using past tense (e.g. "X has been done").

Events

In designing any system which uses CQRS, we want to first establish the kinds of changes that can occur within the system. These changes are represented by our events, and (due to us using the Event Sourcing pattern) the events are what get stored in our Write Model data store. Here are our possible events for this project:

  • Employee Created
  • Location Created
  • Employee Assigned to Location
  • Employee Removed from Location

In a more-fleshed-out system, you might also have events for Employee Quit, Location Closed, Employee Promoted, or any of a host of other events.

Commands

Here are the possible commands for our very simple system (it just so happens that, in our system, a single command corresponds to a single event):

  • Create an Employee
  • Create a Location
  • Assign Employee to Location
  • Remove Employee from Location

With the commands and events defined, we are finally ready to start building our system!

Building the Write Model

The first part of the app that we are going to build is the domain of the app. In many CQRS systems, the domain is the collection of objects which comprise both the Write Model and the Read Model (remember that CQRS's definition says that these are to be two separate models).

Generally speaking, if you want to define a domain, you should start by picking an Aggregate Root object. Remember that an Aggregate Root is an object through which all modifications are done.

In addition to the Aggregate Root, the Write Model for a given objects consists of the following items:

  • Commands
  • Command Handlers
  • Events
  • Event Handlers

The Read Model for any given object consists of:

  • Event Handlers
  • Read Model Access

But wait, you say, why the frick are Event Handlers listed twice? Because the Event Handlers are often what change the Read Data Store to represent what the Write Data Store is storing. The Event Handlers exist in a sort of grey are between the Write and Read Models. In this series, we are going to treat the Event Handlers as though they are part of the Read Model, but in reality they're somewhere in between.

It turns out that building the Write Model is often simpler than building the Read Model, so that's what we're going to do first.

Order of Operations

When any change (create a location, assign an employee, etc) is requested by the end user, the following events occur:

  1. A command for the request is issued.
  2. The command handler processes the command and changes the correct Aggregate Root.
  3. The Aggregate Root creates an Event and sends it to the bus.
  4. The Event Store saves the new event.
  5. The Event Handlers process the new event.

Keep this order in mind as we progress through the next several sections.

BaseCommand and BaseEvent

Because we'll be building several commands and events, our system will define two classes which make that a bit simpler: BaseCommand and BaseEvent, both of which are in turn implement interfaces defined by SimpleCQRS:

public class BaseCommand : ICommand
{
    /// <summary>
    /// The Aggregate ID of the Aggregate Root being changed
    /// </summary>
    public Guid Id { get; set; }

    /// <summary>
    /// The Expected Version which the Aggregate will become.
    /// </summary>
    public int ExpectedVersion { get; set; }
}

public class BaseEvent : IEvent
{
    /// <summary>
    /// The ID of the Aggregate being affected by this event
    /// </summary>
    public Guid Id { get; set; }

    /// <summary>
    /// The Version of the Aggregate which results from this event
    /// </summary>
    public int Version { get; set; }

    /// <summary>
    /// The UTC time when this event occurred.
    /// </summary>
    public DateTimeOffset TimeStamp { get; set; }
}

We'll be using these base classes quite a lot in the next few sections. For now, let's begin to define our actual process by defining the commands, events, and command handlers necessary to create a new employee

Process 1: Creating an Employee

Let's begin by defining an Aggregate Root object for the Employee:

public class Employee : AggregateRoot
{
    private int _employeeID;
    private string _firstName;
    private string _lastName;
    private DateTime _dateOfBirth;
    private string _jobTitle;

    private Employee() { }

    public Employee(Guid id, int employeeID, string firstName, string lastName, DateTime dateOfBirth, string jobTitle)
    {
        Id = id;
        _employeeID = employeeID;
        _firstName = firstName;
        _lastName = lastName;
        _dateOfBirth = dateOfBirth;
        _jobTitle = jobTitle;

        //TODO: Apply Events
    }
}

NOTE: The AggregateRoot class is provided by CQRSLite.

The next thing we need are any commands which deal with an employee. At the moment, we only have one: the creation of that employee. Said command looks like this:

public class CreateEmployeeCommand : BaseCommand
{
    public readonly int EmployeeID;
    public readonly string FirstName;
    public readonly string LastName;
    public readonly DateTime DateOfBirth;
    public readonly string JobTitle;

    public CreateEmployeeCommand(Guid id, int employeeID, string firstName, string lastName, DateTime dateOfBirth, string jobTitle)
    {
        Id = id;
        EmployeeID = employeeID;
        FirstName = firstName;
        LastName = lastName;
        DateOfBirth = dateOfBirth;
        JobTitle = jobTitle;
    }
}

That command needs to be processed by a command handler, which will need to instantiate a new Employee aggregate root.

public class EmployeeCommandHandler : ICommandHandler<CreateEmployeeCommand>
{
    private readonly ISession _session;

    public EmployeeCommandHandler(ISession session)
    {
        _session = session;
    }

    public void Handle(CreateEmployeeCommand command)
    {
        Employee employee = new Employee(command.Id, command.EmployeeID, command.FirstName, command.LastName, command.DateOfBirth, command.JobTitle);
        _session.Add(employee);
        _session.Commit();
    }
}

NOTE: The ISession object is provided by CQRSLite and acts as a gateway into the data loaded into our Event Store. It is similar to Entity Framework's DataContext class, and so we use it in a similar manner.

We now need an event which will be kicked off by this command, though not directly; rather, the Aggregate Root Employee will create the event and place it on the event bus. Here's our event:

public class EmployeeCreatedEvent : BaseEvent
{
    public readonly int EmployeeID;
    public readonly string FirstName;
    public readonly string LastName;
    public readonly DateTime DateOfBirth;
    public readonly string JobTitle;

    public EmployeeCreatedEvent(Guid id, int employeeID, string firstName, string lastName, DateTime dateOfBirth, string jobTitle)
    {
        Id = id;
        EmployeeID = employeeID;
        FirstName = firstName;
        LastName = lastName;
        DateOfBirth = dateOfBirth;
        JobTitle = jobTitle;
    }
}

We must also modify our Employee to kick off this event:

public class Employee : AggregateRoot
{
    ...

    public Employee(Guid id, int employeeID, string firstName, string lastName, DateTime dateOfBirth, string jobTitle)
    {
        Id = id;
        _employeeID = employeeID;
        _firstName = firstName;
        _lastName = lastName;
        _dateOfBirth = dateOfBirth;
        _jobTitle = jobTitle;

        ApplyChange(new EmployeeCreatedEvent(id, employeeID, firstName, lastName, dateOfBirth, jobTitle));
    }
}

We also need an Event Handler for this event, but we will write those up during Part 3 of this series, since our Event Handlers only exist to modify the Read Model's database.

Process 2: Creating a Location

Following a similar effort as creating an employee, here are the command, command handler, aggregate root, and event needed to create a location:

public class CreateLocationCommand : BaseCommand
{
    public readonly int LocationID;
    public readonly string StreetAddress;
    public readonly string City;
    public readonly string State;
    public readonly string PostalCode;

    public CreateLocationCommand(Guid id, int locationID, string streetAddress, string city, string state, string postalCode)
    {
        Id = id;
        LocationID = locationID;
        StreetAddress = streetAddress;
        City = city;
        State = state;
        PostalCode = postalCode;
    }
}

public class LocationCommandHandler : ICommandHandler<CreateLocationCommand>
{
    private readonly ISession _session;

    public LocationCommandHandler(ISession session)
    {
        _session = session;
    }

    public void Handle(CreateLocationCommand command)
    {
        var location = new Location(command.Id, command.LocationID, command.StreetAddress, command.City, command.State, command.PostalCode);
        _session.Add(location);
        _session.Commit();
    }
}

public class Location : AggregateRoot
{
    private int _locationID;
    private string _streetAddress;
    private string _city;
    private string _state;
    private string _postalCode;
    private List<int> _employees;

    private Location() { }

    public Location(Guid id, int locationID, string streetAddress, string city, string state, string postalCode)
    {
        Id = id;
        _locationID = locationID;
        _streetAddress = streetAddress;
        _city = city;
        _state = state;
        _postalCode = postalCode;
        _employees = new List<int>();

        ApplyChange(new LocationCreatedEvent(id, locationID, streetAddress, city, state, postalCode));
    }
}

public class LocationCreatedEvent : BaseEvent
{
    public readonly int LocationID;
    public readonly string StreetAddress;
    public readonly string City;
    public readonly string State;
    public readonly string PostalCode;

    public LocationCreatedEvent(Guid id, int locationID, string streetAddress, string city, string state, string postalCode)
    {
        Id = id;
        LocationID = locationID;
        StreetAddress = streetAddress;
        City = city;
        State = state;
        PostalCode = postalCode;
    }
}

Process 3: Assigning an Employee to a Location

Now this process gets a little more interesting. Due to our business rules, we cannot assign an Employee to more than one Location. This means that whenever we need to assign an Employee, we must first remove him from any Location he's currently assigned to.

We've got a problem, now: how do we figure out what location (if any) the specified employee is currently assigned to?

When using CQRS, we must assume that any command which has reached the command handler is already valid and must be executed. That means any validation must be done before the command is issued. Consequently, business rules involving things like validation must happen at an earlier time, sometime before the command handler receives a command. We will handle the necessary validation in Part 4 of this series.

We can modify the Location Aggregate Root to have two new methods: one for removing an employee and one for adding an employee:

public class Location : AggregateRoot
{
    public void AddEmployee(int employeeID)
    {
        _employees.Add(employeeID);
        ApplyChange(new EmployeeAssignedToLocationEvent(Id, _locationID, employeeID));
    }

    public void RemoveEmployee(int employeeID)
    {
        _employees.Remove(employeeID);
        ApplyChange(new EmployeeRemovedFromLocationEvent(Id, _locationID, employeeID));
    }
}

Let's also create the corresponding events:

public class EmployeeAssignedToLocationEvent : BaseEvent
{
    public readonly int NewLocationID;
    public readonly int EmployeeID;

    public EmployeeAssignedToLocationEvent(Guid id, int newLocationID, int employeeID)
    {
        Id = id;
        NewLocationID = newLocationID;
        EmployeeID = employeeID;
    }
}

public class EmployeeRemovedFromLocationEvent : BaseEvent
{
    public readonly int OldLocationID;
    public readonly int EmployeeID;

    public EmployeeRemovedFromLocationEvent(Guid id, int oldLocationID, int employeeID)
    {
        Id = id;
        OldLocationID = oldLocationID;
        EmployeeID = employeeID;
    }
}

Finally, we need to create two commands: one to remove the employee from his current location, and one to add an employee to a new location. We also need our LocationCommandHandler to handle these two new commands.

public class AssignEmployeeToLocationCommand : BaseCommand
{
    public readonly int EmployeeID;
    public readonly int LocationID;

    public AssignEmployeeToLocationCommand(Guid id, int locationID, int employeeID)
    {
        Id = id;
        EmployeeID = employeeID;
        LocationID = locationID;
    }
}

public class RemoveEmployeeFromLocationCommand : BaseCommand
{
    public readonly int EmployeeID;
    public readonly int LocationID;

    public RemoveEmployeeFromLocationCommand(Guid id, int locationID, int employeeID)
    {
        Id = id;
        EmployeeID = employeeID;
        LocationID = locationID;
    }
}

public class LocationCommandHandler : ICommandHandler<CreateLocationCommand>,
                                      ICommandHandler<AssignEmployeeToLocationCommand>,
                                      ICommandHandler<RemoveEmployeeFromLocationCommand>
{
    ...

    public void Handle(AssignEmployeeToLocationCommand command)
    {
        Location location = _session.Get<Location>(command.Id);
        location.AddEmployee(command.EmployeeID);
        _session.Commit();
    }

    public void Handle(RemoveEmployeeFromLocationCommand command)
    {
        Location location = _session.Get<Location>(command.Id);
        location.RemoveEmployee(command.EmployeeID);
        _session.Commit();
    }
}

With these classes developed, we have complete our real-world CQRS/ES application's Write Model!

Summary

In a CQRS/ES application, the Write Model consists of commands, events, aggregate roots, and command handlers. A command is issued, which is processed by a command handler; the handler acquires the aggregate root object and changes it accordingly; and the aggregate root kicks off any necessary events.

In short:

  • We learned about the order of operations for a CQRS application (command -> command handler -> aggregate root -> event store -> event handler)
  • We built four possible command/event paths (create employee, create location, assign employee, remove employee)

The Write Model is only half of the CQRS puzzle; we still need a Read Model against which we can query for data. We will build said Read Model during Part 3 of Real-World CQRS/ES with ASP.NET and Redis!

Happy Coding!