CQRS – Event Versioning
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.


