CQRS – Event Versioning

February 9th, 2010

When using Event Sourcing you store your events in an Event Store. This Event Store can only insert new events and read historical events, nothing more nothing less. So when you change your domain logic and also the events belonging to this behavior, then you cannot go back into the Event Store and do a one time convert of all the historical events belonging to the same behavior. The Event Store needs to stay intact, that is one of its powers.

So you make a new version of the original event, this new version carries more or less information then the original one. Lets take a look at a very simple example:

    1 namespace Fohjin.DDD.Events.Account
    2 {
    3     [Serializable]
    4     public class CashWithdrawnEvent : DomainEvent
    5     {
    6         public decimal Balance { get; private set; }
    7         public decimal Amount { get; private set; }
    8 
    9         public CashWithdrawnEvent(decimal balance, decimal amount)
   10         {
   11             Balance = balance;
   12             Amount = amount;
   13         }
   14     }
   15 
   16     [Serializable]
   17     public class CashWithdrawnEvent_v2 : DomainEvent
   18     {
   19         public decimal Balance { get; private set; }
   20         public decimal Amount { get; private set; }
   21         public Guid AtmId { get; private set; }
   22 
   23         public CashWithdrawnEvent_v2(decimal balance, decimal amount, Guid atmId)
   24         {
   25             Balance = balance;
   26             Amount = amount;
   27             AtmId = atmId;
   28         }
   29     }
   30 }

This to me looks like a natural evolution for this type of event, so how do you deal with this. Because after having used the system, before adding this extension there have been many cash withdrawals. So all these events are in the Event Store, they cannot be altered, and when you retrieve an Aggregate Root from the Event Store all these historical events need to be processed in order to restore the internal state.

Now what you don’t want is to maintain code in the Aggregate Root that knows how to handle these old event versions, sure one version is ok, but what about one hundred different versions? Also we are not just talking about just in the Aggregate Root, also the different event handlers need to be kept and maintained.

The better approach is to have a mechanism that you can hook-up with different event convertors. Then when an event is retrieved from the Event Store it first goes through this pipeline of convertors to be converted to the latest event version.

Now I wanted to do this properly and write some actual code for this, and then blog about it, but someone kept nagging me about it, so here is a very rough spike instead, first some tests:

    1 namespace Test.Fohjin.DDD.Spike
    2 {
    3     public class Spike_test_1 : BaseTestFixture
    4     {
    5         private object ConvertedEvent;
    6 
    7         protected override void When()
    8         {
    9             ConvertedEvent = new EventConvertor().Convert(new CashWithdrawnEvent(10.0M, 20.0M));
   10         }
   11 
   12         [Then]
   13         public void The_converted_event_is_the_latest_version()
   14         {
   15             ConvertedEvent.WillBeOfType<CashWithdrawnEvent_v4>();
   16         }
   17 
   18         [Then]
   19         public void The_converted_event_wil_contain_the_correct_data()
   20         {
   21             ConvertedEvent.As<CashWithdrawnEvent_v4>().Balance.WillBe(10.0M);
   22             ConvertedEvent.As<CashWithdrawnEvent_v4>().Amount.WillBe(20.0M);
   23             ConvertedEvent.As<CashWithdrawnEvent_v4>().AtmId.WillBe(string.Empty);
   24         }
   25     }
   26 
   27     public class Spike_test_2 : BaseTestFixture
   28     {
   29         private object ConvertedEvent;
   30 
   31         protected override void When()
   32         {
   33             ConvertedEvent = new EventConvertor().Convert(new CashWithdrawnEvent_v2(10.0M, 20.0M, "12345"));
   34         }
   35 
   36         [Then]
   37         public void The_converted_event_is_the_latest_version()
   38         {
   39             ConvertedEvent.WillBeOfType<CashWithdrawnEvent_v4>();
   40         }
   41 
   42         [Then]
   43         public void The_converted_event_wil_contain_the_correct_data()
   44         {
   45             ConvertedEvent.As<CashWithdrawnEvent_v4>().Balance.WillBe(10.0M);
   46             ConvertedEvent.As<CashWithdrawnEvent_v4>().Amount.WillBe(20.0M);
   47             ConvertedEvent.As<CashWithdrawnEvent_v4>().AtmId.WillBe("12345");
   48         }
   49     }
   50 
   51     public class Spike_test_3 : BaseTestFixture
   52     {
   53         private object ConvertedEvent;
   54 
   55         protected override void When()
   56         {
   57             ConvertedEvent = new EventConvertor().Convert(new CashWithdrawnEvent_v3(10.0M, 20.0M, "12345"));
   58         }
   59 
   60         [Then]
   61         public void The_converted_event_is_the_latest_version()
   62         {
   63             ConvertedEvent.WillBeOfType<CashWithdrawnEvent_v4>();
   64         }
   65 
   66         [Then]
   67         public void The_converted_event_wil_contain_the_correct_data()
   68         {
   69             ConvertedEvent.As<CashWithdrawnEvent_v4>().Balance.WillBe(10.0M);
   70             ConvertedEvent.As<CashWithdrawnEvent_v4>().Amount.WillBe(20.0M);
   71             ConvertedEvent.As<CashWithdrawnEvent_v4>().AtmId.WillBe("12345");
   72         }
   73     }
   74 
   75     public class Spike_test_4 : BaseTestFixture
   76     {
   77         private object ConvertedEvent;
   78 
   79         protected override void When()
   80         {
   81             ConvertedEvent = new EventConvertor().Convert(new CashWithdrawnEvent_v4(10.0M, 20.0M, "12345"));
   82         }
   83 
   84         [Then]
   85         public void The_converted_event_is_the_latest_version()
   86         {
   87             ConvertedEvent.WillBeOfType<CashWithdrawnEvent_v4>();
   88         }
   89 
   90         [Then]
   91         public void The_converted_event_wil_contain_the_correct_data()
   92         {
   93             ConvertedEvent.As<CashWithdrawnEvent_v4>().Balance.WillBe(10.0M);
   94             ConvertedEvent.As<CashWithdrawnEvent_v4>().Amount.WillBe(20.0M);
   95             ConvertedEvent.As<CashWithdrawnEvent_v4>().AtmId.WillBe("12345");
   96         }
   97     }
   98 }

So basically some tests to confirm the correct conversion from one event to another event, now below here is the full implementation:

    1 namespace Test.Fohjin.DDD.Spike
    2 {
    3     public class EventConvertor
    4     {
    5         private readonly Dictionary<Type, Func<object, object>> _convertors;
    6 
    7         public EventConvertor()
    8         {
    9             _convertors = new Dictionary<Type, Func<object, object>>();
   10             RegisterEventConvertors();
   11         }
   12 
   13         private void RegisterEventConvertors()
   14         {
   15             _convertors.Add(typeof(CashWithdrawnEvent), x => new CashWithdrawnEventConvertor().Convert((CashWithdrawnEvent)x));
   16             _convertors.Add(typeof(CashWithdrawnEvent_v2), x => new CashWithdrawnEvent_v2Convertor().Convert((CashWithdrawnEvent_v2)x));
   17             _convertors.Add(typeof(CashWithdrawnEvent_v3), x => new CashWithdrawnEvent_v3Convertor().Convert((CashWithdrawnEvent_v3)x));
   18         }
   19 
   20         public object Convert(object soureEvent)
   21         {
   22             Func<object, object> convertor;
   23             return _convertors.TryGetValue(soureEvent.GetType(), out convertor) 
   24                 ? Convert(convertor(soureEvent)) 
   25                 : soureEvent;
   26         }
   27     }
   28 
   29     public interface IEventConvertor<TSourceEvent, TTargetEvent>
   30         where TSourceEvent : IDomainEvent
   31         where TTargetEvent : IDomainEvent
   32     {
   33         TTargetEvent Convert(TSourceEvent sourceEvent);
   34     }
   35 
   36     public class CashWithdrawnEventConvertor : IEventConvertor<CashWithdrawnEvent, CashWithdrawnEvent_v4>
   37     {
   38         public CashWithdrawnEvent_v4 Convert(CashWithdrawnEvent sourceEvent)
   39         {
   40             var theEvent = new CashWithdrawnEvent_v4(sourceEvent.Balance, sourceEvent.Amount, string.Empty)
   41             {
   42                 AggregateId = sourceEvent.AggregateId
   43             };
   44             (theEvent as IDomainEvent).Version = (sourceEvent as IDomainEvent).Version;
   45             return theEvent;
   46         }
   47     }
   48 
   49     public class CashWithdrawnEvent_v2Convertor : IEventConvertor<CashWithdrawnEvent_v2, CashWithdrawnEvent_v3>
   50     {
   51         public CashWithdrawnEvent_v3 Convert(CashWithdrawnEvent_v2 sourceEvent)
   52         {
   53             var theEvent = new CashWithdrawnEvent_v3(sourceEvent.Balance, sourceEvent.Amount, sourceEvent.AtmId)
   54             {
   55                 AggregateId = sourceEvent.AggregateId
   56             };
   57             (theEvent as IDomainEvent).Version = (sourceEvent as IDomainEvent).Version;
   58             return theEvent;
   59         }
   60     }
   61 
   62     public class CashWithdrawnEvent_v3Convertor : IEventConvertor<CashWithdrawnEvent_v3, CashWithdrawnEvent_v4>
   63     {
   64         public CashWithdrawnEvent_v4 Convert(CashWithdrawnEvent_v3 sourceEvent)
   65         {
   66             var theEvent = new CashWithdrawnEvent_v4(sourceEvent.Balance, sourceEvent.Amount, sourceEvent.AtmId)
   67             {
   68                 AggregateId = sourceEvent.AggregateId
   69             };
   70             (theEvent as IDomainEvent).Version = (sourceEvent as IDomainEvent).Version;
   71             return theEvent;
   72         }
   73     }
   74 }

This implementation is definitely not very elegant (so it doesn’t really belong on this blog) but hey it does show you how a possible solution would work. When building this yourself you might want to use conventions to auto register the convertors and chain them together during configuration so there is no need for the recursive functionality.

Also look at the jump from version 1 to version 4, this is an optimization to speed up the conversion. You would do this after a few versions, not for each version.

I’ll be adding a proper solution to the example in the near future, something that you would just plug the convertors in and the system would figure out how to handle them itself.

Mark Nijhof CQRS, DDD, Event Sourcing

  1. February 23rd, 2010 at 08:34 | #1

    @Sebastian
    Concurrency issues are dealt with by verifying the version numbers of the AR in the event store and the AR you have. If they do not match then you have a concurrency valuation. Now because we work with commands and events we can (if you handle this smartly) automatically solve some of these issues. For example if these two different commands result in different state changes then we could allow the version conflict to be ignored. Greg is going to write a post about this as well, and I am planning on creating a test case where this becomes visible.

  2. dokie
    February 25th, 2010 at 11:22 | #2

    @Mark Nijhof
    Do you mean the version number of the reconstructed AR is compared to the version number in the store on point of save? This stops a group of cases where that wondow for concurrency error is (usually) small. How does this help with the scenario where the command received would overwrite the previously written AR which may have also been valis – e.g. If 2 users change the address but to different values (ignore why that might happen for now) then depending on whose Command gets there last, their changes will win since the Command does not carry any version information over from the Query side.

    I may also be confused by your reply though so apologies if so and would appreciate a clearer explanation.

Comment pages
Comments are closed.