Click here to Skip to main content
15,892,737 members
Articles / Programming Languages / C#

Project YakShayQRS: Another CQRS Evolution

Rate me:
Please Sign up or sign in to vote.
0.00/5 (No votes)
21 Mar 2012CPOL2 min read 6.7K   3  
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

C#
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 (even 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)


Written By
Founder Virtual Sales Lab
Belgium Belgium

Comments and Discussions

 
-- There are no messages in this forum --