Click here to Skip to main content
Click here to Skip to main content
Technical Blog

Project YakShayQRS: Another CQRS evolution

, 21 Mar 2012 CPOL
Rate this:
Please Sign up or sign in to vote.
My efforts to minimize CQRS in several iterations.

Introduction

TL;DR: I managed to minimize the CQRS overhead even further.

Over the years, I have been trying to minimize CQRS in several iterations, even releasing a framework and a lib in doing so. Yet, I have still not been satisfied by the approach I reached. This time I once again am pretty satisfied with the way things turned out, and they actually seem to require even way less overhead...

How it works

The concept is quite simple: I use a generic message class to implement messaging. Next to this I use the virtual keyword:

  • A class can contain virtual properties; these properties define the unique key of the instance. (e.g., an "Account" class has a "protected virtual string AccountId {get;set;}").
  • When invoking a message on a class type, an instance is loaded where the unique key is loaded based on the match between the message parameters and the classes' virtual properties. A message only gets invoked if it contains all the virtual properties from the class.
  • In order to alter state, one should use a virtual method.
  • One can only send messages targeting non-virtual methods to a class instance.
  • Non-virtual methods should never alter state, but instead call a virtual method that alters the state...
  • When rebuilding state based on past events, only the messages targeting the virtual methods are invoked to rebuild the state; no messages are emitted.

The advantage: all the wiring is convention-based; and once you get it it is quite easy: altering state should only happen through virtual methods, but these virtual methods should never contain any logic and just alter state or do nothing. The intercepted call to the virtual method gets emitted and gets processed by any non-virtual methods in other classes (if the message matches the classes' key).

Maybe an example can make it more clear

public class Account
{
    // virtual props define the unique id, are used as a message filter
    // and initialized upon loading the instance
    protected virtual string AccountId { get; set; }
    bool IsRegistered = false;
    private Decimal Balance = 0;

    // public non-virtual methods define the external interface (i.e. the commands);
    // these methods are **NEVER** invoked when rebuilding current state from past messages
    public void RegisterAccount(string OwnerName)
    {
        if (IsRegistered)
            return;
        OnAccountRegistered(OwnerName);
    }

    public void DepositAmount(decimal Amount)
    {
        if (Amount <= 0)
            OnTransactionCancelled("Deposit", "Your amount has to be positive");
        OnAmountDeposited(Amount);
    }

    public void WithdrawAmount(decimal Amount)
    {
        if (Amount <= 0)
            OnTransactionCancelled("Withdraw", "Your amount has to be positive");
        if (Amount > Balance)
            OnTransactionCancelled("Withdraw", 
                "Your amount has to be smaller then the balance");
        OnAmountWithdrawn(Amount);
    }

    public void Transfer(string TargetAccountId, decimal Amount)
    {
        if (Amount <= 0)
            OnTransactionCancelled("Transfer", "Your amount has to be positive");
        if (Amount > Balance)
            OnTransactionCancelled("Transfer", 
                  "Your amount has to be smaller then the balance");
        OnTransferProcessedOnSource(TargetAccountId, Amount);
    }

    public void ProcessTransferOnTarget(string SourceAccountId, decimal Amount)
    {
        if (IsRegistered)
            OnTransferProcessedOnTarget(SourceAccountId, Amount);
        else
            OnTransferFailedOnTarget(SourceAccountId, Amount);
    }

    public void CancelTransferOnSource(string TargetAccountId, decimal Amount)
    {
        OnTransferCancelledOnSource(TargetAccountId, Amount);
    }

    // virtual methods emit messages before they get called &
    // the virtual props are added to the emitted messages.
    // When building the current object's state based on past messages,
    // only the virtual methods get invoked.
    protected virtual void OnAccountRegistered(string OwnerName) { IsRegistered = true; }
    protected virtual void OnAmountDeposited(decimal Amount) { Balance += Amount; }
    protected virtual void OnAmountWithdrawn(decimal Amount) { Balance -= Amount; }
    protected virtual void OnTransferProcessedOnSource(
              string TargetAccountId, decimal Amount) { Balance -= Amount; }
    protected virtual void OnTransferCancelledOnSource(
              string TargetAccountId, decimal Amount) { Balance += Amount; }
    protected virtual void OnTransferProcessedOnTarget(
              string SourceAccountId, decimal Amount) { Balance += Amount; }
    protected virtual void OnTransferFailedOnTarget(string SourceAccountId, decimal Amount) { }
    protected virtual void OnTransactionCancelled(string what, string reason) { }
}

public class AccountTransferSaga
{
    // no virtual props = no unique Id
    // Processing these events emits commands (messages)
    public void OnTransferProcessedOnSource(string AccountId, string TargetAccountId, decimal Amount)
    {
        ProcessTransferOnTarget(TargetAccountId, AccountId, Amount);
    }
    public void OnTransferFailedOnTarget(string AccountId, string SourceAccountId, decimal Amount)
    {
        CancelTransferOnSource(SourceAccountId, AccountId, Amount);
    }
    // these commands get emitted
    protected virtual void ProcessTransferOnTarget(string AccountId, 
              string SourceAccountId, decimal Amount) { }
    protected virtual void CancelTransferOnSource(string AccountId, 
              string TargetAccountId, decimal Amount) { }
}

public class AccountBalances
{
    public AccountBalances() { }
    public Dictionary<string, Decimal> Balances = new Dictionary<string, decimal>();
    // no non-virtual commands, as this class only processes messages, it does not emit any
    public virtual void OnAccountRegistered(string AccountId) { Balances.Add(AccountId, 0); }
    public virtual void OnAmountDeposited(string AccountId, decimal Amount) { Balances[AccountId] += Amount; }
    public virtual void OnAmountWithdrawn(string AccountId, decimal Amount) { Balances[AccountId] -= Amount; }
    public virtual void OnTransferProcessedOnTarget(string AccountId, 
                        string SourceAccountId, decimal Amount)
    {
        Balances[AccountId] += Amount;
        Balances[SourceAccountId] -= Amount;
    }
}

[TestMethod]
public void Deposits_and_withdraws_should_not_interfere_with_each_other()
{
    var SUT = new YakShayBus();
    // register all types under test
    SUT.RegisterType<Account>();
    SUT.RegisterType<AccountTransferSaga>();
    SUT.RegisterType<AccountBalances>();
    var ms = new MessageStore();
    SUT.HandleUntilAllConsumed(Message.FromAction(
        x => x.RegisterAccount(AccountId: "account/1", OwnerName: "Tom")), ms.Add, ms.Filter);
    SUT.HandleUntilAllConsumed(Message.FromAction(
        x => x.RegisterAccount(AccountId: "account/2", OwnerName: "Ben")), ms.Add, ms.Filter);
    SUT.HandleUntilAllConsumed(Message.FromAction(
        x => x.DepositAmount(AccountId: "account/1", Amount: 126m)), ms.Add, ms.Filter);
    SUT.HandleUntilAllConsumed(Message.FromAction(
        x => x.DepositAmount(AccountId: "account/2", Amount: 10m)), ms.Add, ms.Filter);
    SUT.HandleUntilAllConsumed(Message.FromAction(
        x => x.Transfer(AccountId: "account/1", TargetAccountId: "account/2", Amount: 26m)), 
        ms.Add, ms.Filter);
    SUT.HandleUntilAllConsumed(Message.FromAction(
        x => x.WithdrawAmount(AccountId: "account/2", Amount: 10m)), ms.Add, ms.Filter);
    var bal = new AccountBalances();
    SUT.ApplyHistory(bal, ms.Filter);
    bal.Balances.Count.ShouldBe(2);
    bal.Balances["account/1"].ShouldBe(100m);
    bal.Balances["account/2"].ShouldBe(26m);
}

As usual, the full source can be found over at github.

Conclusion

This is yet another evolutionary approach to CQRS, and it feels pretty neat (event though the same things applied to the previous versions. Let me know what you think !

License

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

Share

About the Author

Tom Janssens
Founder Core bvba
Belgium Belgium
Tom Janssens, owner of Core, a software and consultancy company.
Father of two sons named Quinten & Matisse, and married to a beautiful woman named Liesbeth.
 
Blog: http://tojans.me
Github: http://github.com/ToJans
Twitter: http://twitter.com/ToJans
LinkedIn: http://www.linkedin.com/in/tomjanssens

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.1411028.1 | Last Updated 21 Mar 2012
Article Copyright 2012 by Tom Janssens
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid