Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

CQRS for the lazy *ss

4.00/5 (2 votes)
16 Jan 2012CPOL3 min read 18.6K  
Yet another attempt to remove all the protocol that CQRS requires.

Introduction

In one of my numerous attempts to create a neat approach to CQRS, this is yet another attempt to remove all the protocol that CQRS requires. I love bootstrapping new projects and experimenting with new approaches, and this is another step that improves efficiency. It is a natural evolution to one of my other CQRS playgrounds: Scritchy.

The conventional CQRS approach requires you to write command - and event classes, to wire up your commands and event handlers to the relevant ARs, sagas and viewmodel builders. I removed some of the clutter in that process with Scritchy. Scritchy uses conventions to wire up the commands and events to the relevant components. This makes CQRS a lot less verbose while still offers the same advantages.

Scritchy v2?

While this is a good attempt, it still requires you to write the dreaded event - and command classes. Having pushed some best practices/usages/libs to some enterprise teams in the past, I noticed the following: any change that adds extra steps/work to the process makes acceptance harder by devs, as most people are usually trapped in a certain approach, and they do not see an advantage in having to write more code to do the same thing. If they do not need to write all the "protocol"/extra classes, you have removed yet another step that might slow down acceptance of the practice.

The new approach I am about to tell you about in this blog post completely hides the messages from the dev, i.e., the dev can just use a conventional method call, and messages are created underneath.

Disclaimer

This is a possible approach to the problem, but I tend to think that the majority of the CQRS evangelists and practitioners value explicitness over pragmatism. I love the explicitness and clarity of their examples and implementations, but in the real world, most people start to think: "Wow, that is a lot of code to simply add an item to the stock; are they nuts?".

Having experienced the fallacy of the explicitness in a startup attempt, my brain got tickled to find fast and neat approaches to a pretty established conventional path. I think this one is pretty close.

The full source is available over at GitHub, and the demo app can be seen/tested over at appharbor.

Example

So without further ado, here I will show you ALL THE CODE required to implement a complete CQRS app:

The Domain: Bank Account

Let us first start with some sequence diagrams to introduce you to the problem domain:

Image 1

Image 2

Image 3

Image 4

Domain:Account

C#
using System.Collections.Generic;
using MinimalisticCQRS.Infrastructure;
namespace MinimalisticCQRS.Domain
{
    public class Account : AR
    {
        decimal Balance = 0;
        bool IsEnabed = false;
        public void RegisterAccount(string OwnerName, string AccountNumber)
        {
            if (IsEnabed) return;
            Apply.AccountRegistered(OwnerName, AccountNumber, AccountId: Id);
        }
        public void DepositCash(decimal Amount)
        {
            Guard.Against(!IsEnabed, "You can not deposit into an unregistered account");
            Guard.Against(Amount < 0, "You can not deposit an amount < 0");
            Apply.AmountDeposited(Amount, AccountId: Id);
        }
        public void WithdrawCash(decimal Amount)
        {
            Guard.Against(!IsEnabed, "You can not withdraw from an unregistered account");
            Guard.Against(Amount < 0, "You can not withdraw an amount < 0");
            Guard.Against(Amount > Balance, 
              "You can not withdraw an amount larger then the current balance");
            Apply.AmountWithdrawn(Amount, AccountId: Id);
        }
        public void TransferAmount(decimal Amount, string TargetAccountId)
        {
            Guard.Against(!IsEnabed, "You can not transfer from an unregistered account");
            Guard.Against(Amount < 0, "You can not transfer an amount < 0");
            Guard.Against(Amount > Balance, 
              "You can not transfer an amount larger then the current balance");
            Apply.AmountWithdrawn(Amount, AccountId: Id);
            Apply.TransferProcessedOnSource(Amount, TargetAccountId, AccountId: Id);
        }
        public void ProcessTransferOnTarget(decimal Amount, string SourceAccountId)
        {
            if (IsEnabed)
            {
                Apply.AmountDeposited(Amount, AccountId: Id);
                Apply.TransferCompleted(Amount, SourceAccountId, AccountId: Id);
            }
            else
            {
                Apply.TransferFailedOnTarget(
                    "You can not transfer to an unregistered account", 
                    Amount, SourceAccountId, AccountId: Id);
            }
        }
        public void CancelTransfer(string Reason,decimal Amount,string TransferTargetId)
        {
            Apply.AmountDeposited(Amount, AccountId: Id);
            Apply.TransferCanceled(Reason, Amount, TransferTargetId, AccountId: Id);
        }
        // events
        void OnAccountRegistered(string OwnerName)
        {
            Balance = 0;
            IsEnabed = true;
        }
        void OnAmountDeposited(decimal Amount)
        {
            Balance += Amount;
        }
        void OnAmountWithdrawn(decimal Amount)
        {
            Balance -= Amount;
        }
    }
}

Nothing spectacular here; yes, I do realize it is a boring sample, but we need a well-known domain to explain it IMO; suggestions for an alternative domain are welcome!

Domain:AccountTransferSaga

C#
namespace MinimalisticCQRS.Domain
{
public class AccountTransferSaga
{
    private dynamic bus;
    public AccountTransferSaga(dynamic bus)
    {
        this.bus = bus;
    }
    public void OnTransferProcessedOnSource(decimal Amount, 
           string TargetAccountId, string AccountId)
    {
        bus.ProcessTransferOnTarget(Amount, 
          SourceAccountId: AccountId, AccountId: TargetAccountId);
    }
    public void OnTransferFailedOnTarget(string Reason, decimal Amount, 
                string SourceAccountId, string AccountId)
    {
        bus.CancelTransfer(Reason,Amount, 
          TargetAccountId:AccountId, AccountId: SourceAccountId);
    }
}
}

Orchestrates the transfer between 2 accounts.

Domain:AccountUniquenessSaga

C#
using System.Collections.Generic;
using MinimalisticCQRS.Infrastructure;
namespace MinimalisticCQRS.Domain
{
    public class AccountUniquenessSaga
    {
        List<string> RegisteredAccountNumbers = new List<string>();
        public void CanRegisterAccount(string OwnerName, string AccountNumber, string AccountId)
        {
            Guard.Against(RegisteredAccountNumbers.Contains(AccountNumber), 
               "This account number has already been registered");
        }
        void OnAccountRegistered(string OwnerName, string AccountNumber, string AccountId)
        {
            RegisteredAccountNumbers.Add(AccountNumber);
        }
    }
}

Verifies uniqueness of the account; I do realize on rare cases there might be an error here (when using async processing, one might have eventual consistency), but I consider the effort in fixing the error by hand less than the cost to implement it on the domain.

The code above is all that is needed for the domain, i.e., no events or commands, etc. isn't that neat ?

Hubs

In order to show you what else is needed, I added all the server-side code as well, so you have an idea how much code you need to implement a complete app.

C#
using System;
using System.Collections.Generic;
using System.Linq;using System.Web;
using SignalR.Hubs;using MinimalisticCQRS.Infrastructure;
namespace MinimalisticCQRS.Hubs
{
public class CommandHub : Hub
{
    dynamic bus;
    public CommandHub(dynamic bus)
    {
        this.bus = bus;
    }
    public void RegisterAccount(string OwnerName, string AccountNumber, string AccountId)
    {
        bus.RegisterAccount(OwnerName, AccountNumber, AccountId: AccountId);
    }
    public void DepositCash(decimal Amount, string AccountId)
    {
        bus.DepositCash(Amount, AccountId: AccountId);
    }
    public void WithdrawCash(decimal Amount, string AccountId)
    {
        bus.WithdrawCash(Amount, AccountId: AccountId);
    }
    public void TransferAmount(decimal Amount, string TargetAccountId, string AccountId)
    {
        bus.TransferAmount(Amount, TargetAccountId, AccountId:AccountId);
    }
    // commands don't need to be executed by AR if they are irrelevant to the domain
    public void ShareMessage(string username, string message)
    {
        Guard.Against(string.IsNullOrWhiteSpace(username), "Username can not be empty");
        if (string.IsNullOrWhiteSpace(message))
            message = "ZOMG!!! I have no idea what to say, " + 
               "so I'll just say this stuff has lots of awesomesauce"; 
        bus.OnMessageShared(username, message);
    }
}
}
C#
using System;
using System.Collections.Generic;
using System.Linq;using System.Web;
using SignalR.Hubs;
namespace MinimalisticCQRS.Hubs
{
public class QueryHub : Hub
{
    public class AccountDetails
    {
        public string Id;
        public string OwnerName;
        public Decimal Balance;
        public string AccountNumber;
        public string Description { get { return string.Format("{0} - {1}", 
               AccountNumber, OwnerName); } }
    }
    Dictionary<string, AccountDetails> Details;
    public QueryHub()
    {
        Details = new Dictionary<string, AccountDetails>();
    }
    public AccountDetails[] GetDetails()
    {
        return Details.Values.ToArray();
    }
    void OnAccountRegistered(string OwnerName, string AccountNumber, string AccountId)
    {
        var detail = new AccountDetails { Id = AccountId, 
            AccountNumber = AccountNumber, OwnerName = OwnerName, Balance = 0 };
        Details.Add(AccountId, detail);
        Clients.AddAccountDetails(detail);
    }
    void OnAmountDeposited(decimal Amount, string AccountId)
    {
        Details[AccountId].Balance += Amount;
        Clients.UpdateBalance(Details[AccountId].Balance,AccountId);
    }
    void OnAmountWithdrawn(decimal Amount, string AccountId)
    {
        Details[AccountId].Balance -= Amount;
        Clients.UpdateBalance( Details[AccountId].Balance, AccountId);
    }
    void OnMessageShared(string username, string message)
    {
        Clients.AddChatMessage(username, message);
    }
    void OnTransferCanceled(string Reason, decimal Amount, 
                   string TargetAccountId, string AccountId)
    {
        Caller.Alert(Reason);
    }
}
}

Conclusion

This is an approach to CQRS that removes the need to write message classes; it uses a combination of serializing method invocation on a dynamic together with conventions to build a system where messages are completely hidden from the user. Talk about removing infrastructure from the code!

I assume there will be a lot of opponents to this approach, as this gives room for typos, etc. However, I do believe that this approach combined with TDD/BDD could be for CQRS what rails was for MVC: a pragmatic approach that allows you to just write code at blistering speed without a lot of added protocol.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)