CQRS in in C# most trivial possible example

Hello everybody,

today I want to make compendium of CQRS for very simple case.

So, at one of the projects I seen following way of implementing code:

public class WriteModel
{
        //common properties
        public int SomeEntity {get;set; } // ID of some entity
}

public class ReadModel : WriteModel
{
        public string AdditionalProperty {get;set; }
}

and that way of implementing it was proudly named CQRS. To be honest I wasn't very agreeable on that part, so I decided to hear opinions of other people, not only mine understanding of what I read in book.

And I'd like to save one of the comments not only at stackoverflow, but also at my web site.

So, what's wrong with this approach?

Liskov Substitution Principal is one problem -- the WriteModel supports mutations (via commands); to inherit from the WriteModel as you do here implies that the ReadModel also supports commands. Which implies that the WriteModel is not the only way to mutate the state of the model. That's very bad. If reverse the hierarchy, then you have a ReadModel supporting queries (good), and then the WriteModel inheriting from it also supporting queries. That's bad, but not to such a degree.  In other words, by doing this, you are losing separation of the two models. When you abandon CQRS like this, you are giving up the benefits the pattern affords. Let's imagine this exercise -- suppose you start with a model representation using Command Query Separation (aka: CQS). A trivial implementation might look like

public class Model {
    private Model.State state;

    // queries go here
    // these never change the model state

    // commands go here
    // these can change the model state
    // they don't usually return data
}

So for an example like a model of a bank account, a naive model might look like:

// CQS
public class Account {
    private Account.State state;

    // commands
    public void deposit(Money m) {...}
    public void withdraw(Money m) {...}

    // queries
    public Money getBalance() {...}

    private class State {...}
}

Superficially, all that CQRS adds here is the refactoring of the commands and queries into two separate implementations (ie: the "write model" and the "read model").

// CQRS
public class Account {
    public class WriteModel {
        private Account.State state;

        // commands
        public void deposit(Money m) {...}
        public void withdraw(Money m) {...}
    }

    public class ReadModel {
        private Account.State state;

        // queries
        public Money getBalance() {...}
    }

    private class State {...}
}

This, by itself, doesn't buy you very much -- two separate interfaces instead of one gives you some assurance that the compiler can enforce that the read only representation of the account can't be used to change it. Greg Young's key insight at this point: because we have separated the two models, we no longer need to use the same representation of the account state in both cases. In the abstract form, it might look more like:

// CQRS
public class Account {
    public class WriteModel {
        private WriteModel.State state;

        // commands
        public void deposit(Money m) {...}
        public void withdraw(Money m) {...}

        private class State {...}
    }

    public class ReadModel {
        private ReadModel.State state;

        // queries
        public Money getBalance() {...}

        private class State {...}
    }
}

This is a really big deal, because WriteModel.State can now be optimized for writes, and ReadModel.State can be optimized for reads. You can have completely different representations, completely different persistence strategies, even different skill levels of programmers working each model (programmers working with the read model may introduce bugs, but they can't possibly corrupt your client's data, because data modifications are restricted to the write model). But all of that goes away if you insist on the two models sharing a representation of state.

No Comments

Add a Comment