Skip to content

Instantly share code, notes, and snippets.

@ToJans
Created September 1, 2010 10:16
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ToJans/560498 to your computer and use it in GitHub Desktop.
Save ToJans/560498 to your computer and use it in GitHub Desktop.
combining CQRS, Event sourcing and BDD
using System;
using System.Collections.Generic;
using System.Linq;
namespace SimpleCQRS2.Framework.Services
{
public class AggregateRootStore : IAggregateRootStore
{
public IAggregateRoot GetAggregateRoot(Type aggregateRootType, Guid aggregateRootId)
{
return GetStoredAggregateRoot(aggregateRootType, aggregateRootId).Instance;
}
public int GetAggregateRootVersion(Type aggregateRootType, Guid aggregateRootId)
{
return GetStoredAggregateRoot(aggregateRootType, aggregateRootId).Version;
}
public void SaveOrUpdate(IAggregateRoot ar,int version)
{
var x = GetStoredAggregateRoot(ar.GetType(),ar.AggregateRootId);
x.Instance = ar;
x.Version = version;
}
class StoredAggregateRoot
{
public IAggregateRoot Instance { get; set; }
public int Version { get; set; }
public Type Type {
get { return Instance.GetType(); }
}
}
Dictionary<Type, Dictionary<Guid, StoredAggregateRoot>> ArStore = new Dictionary<Type, Dictionary<Guid, StoredAggregateRoot>>();
private StoredAggregateRoot GetStoredAggregateRoot(Type aggregateRootType, Guid aggregateRootId)
{
if (!ArStore.ContainsKey(aggregateRootType)) {
ArStore.Add(aggregateRootType, new Dictionary<Guid, StoredAggregateRoot>());
}
if (!ArStore[aggregateRootType].ContainsKey(aggregateRootId)) {
IAggregateRoot instance = (IAggregateRoot)Activator.CreateInstance(aggregateRootType);
instance.AggregateRootId = aggregateRootId;
ArStore[aggregateRootType].Add(aggregateRootId, new StoredAggregateRoot {
Instance = instance,
Version = 0
});
}
return ArStore[aggregateRootType][aggregateRootId];
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using SimpleCQRS2.Example.Domain.Handlers;
using SimpleCQRS2.Example.Domain.Model;
using SimpleCQRS2.Example.Views;
using Aubergine.Model;
namespace SimpleCQRS2.Example.BDD
{
public class BDDAppContext : SimpleCQRS2.Framework.BDD.CQRSFixture
{
Dictionary<string, Guid> nameGuids = new Dictionary<string, Guid>();
public BDDAppContext(){}
protected override IEnumerable<object> MessageHandlers {
get {
yield return new PositiveNumberHandler();
yield return new SomeReadModelUpdater();
}
}
[DSL(@"(?<name>nr\d+)")]
Guid GetGuid(string name)
{
if (!nameGuids.ContainsKey(name)) nameGuids.Add(name,Guid.NewGuid());
return nameGuids[name];
}
[DSL(@"a positive number named (?<guid>nr\d+) was created")]
void NumberCreated(Guid guid)
{
When(new PositiveNumber.Created() { AggregateRootId = guid});
}
[DSL(@"(?<guid>nr\d+) was incremented")]
void WasIncremented(Guid guid)
{
When(new PositiveNumber.WasIncremented() { AggregateRootId = guid });
}
[DSL(@"the value of (?<guid>nr\d+) was changed")]
void WasChanged(Guid guid)
{
When(new PositiveNumber.ValueHasChanged() { AggregateRootId = guid });
}
[DSL(@"(?<guid>nr\d+) was decremented")]
void WasDecremented(Guid guid)
{
When(new PositiveNumber.WasDecremented() { AggregateRootId = guid });
}
[DSL(@"I decrement (?<guid>nr\d+)")]
void Decrement(Guid guid)
{
When(new PositiveNumber.Decrement() { AggregateRootId = guid });
}
[DSL(@"I increment (?<guid>nr\d+)")]
void Increment(Guid guid)
{
When(new PositiveNumber.Increment() { AggregateRootId = guid });
}
[DSL(@"it should have incremented (?<amount>\d+) times?")]
bool ShouldIncrement(int amount)
{
return MyEventStore.All<PositiveNumber.WasIncremented>().Count() == amount;
}
[DSL(@"it should have decremented (?<amount>\d+) times?")]
bool ShouldDecrement(int amount)
{
return MyEventStore.All<PositiveNumber.WasDecremented>().Count() == amount;
}
[DSL(@"it should have changed the value (?<amount>\d+) times?")]
bool ShouldChange(int amount)
{
return MyEventStore.All<PositiveNumber.ValueHasChanged>().Count() == amount;
}
[DSL(@"the value of (?<guid>nr\d+) should be (?<value>\d+)")]
bool ValueShouldBe(Guid guid,int value)
{
var ar = (PositiveNumber)MyArStore.GetAggregateRoot(typeof(PositiveNumber),guid);
return ar.Value == value;
}
[DSL(@"the screen value for (?<guid>nr\d+) should be (?<value>\d+)")]
bool ScreenValueShouldBe(Guid guid,int value)
{
var mu = MyIOC.Resolve<SomeReadModelUpdater>().First();
return mu.Values[guid] == value ;
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
namespace SimpleCQRS2.Framework.Services
{
public class CQRSCommandBus : ICommandBus
{
public CQRSCommandBus(IAnIOC IOC,IEventStore evs,IAggregateRootStore agrs)
{
this.IOC = IOC;
this.agrs = agrs;
this.evs = evs;
}
IAnIOC IOC = null;
IEventStore evs = null;
IAggregateRootStore agrs=null;
public static IEnumerable<Type> GetMessageHandlerInterfaces(Type T)
{
foreach (var i in T.GetInterfaces().Where(t => t.IsGenericType)) {
if (i.GetGenericTypeDefinition() == typeof(IMessageHandler<,>))
yield return i;
}
}
public void HandleCommand(ICommand command)
{
HandleMessage(command, false);
}
public void RunEvents(IAggregateRoot aggregateRoot)
{
while (true) {
var version = agrs.GetAggregateRootVersion(aggregateRoot.GetType(),aggregateRoot.AggregateRootId);
var e = evs.NextEvent(aggregateRoot.GetType(),aggregateRoot.AggregateRootId,version);
if (e == null)
break;
HandleMessage(e, true);
}
}
void HandleMessage(IMessage message, bool isreplay)
{
var gt = typeof(IMessageHandler<,>).MakeGenericType(new Type[] { message.AggregateRootType,message.GetType() });
var handlers = IOC.Resolve(gt);
if (! isreplay && !handlers.Any())
throw new Exception("Type unknown; no handlers available:" + message.GetType().Name);
List<IEvent> events = new List<IEvent>();
var aggregateRoot = agrs.GetAggregateRoot(message.AggregateRootType, message.AggregateRootId);
if (!isreplay)
RunEvents(aggregateRoot);
var version = agrs.GetAggregateRootVersion(message.AggregateRootType, message.AggregateRootId);
foreach (var handler in handlers)
{
var mi = handler.GetType().GetMethod("Handle", new Type[] {
message.GetType(),
message.AggregateRootType
});
var result = mi.Invoke(handler, new object[] {
message,
aggregateRoot
});
// makes sure ienumerable gets consumed
events.AddRange((IEnumerable<IEvent>)result);
}
var evt = message as IEvent;
if (evt!=null)
{
if ( !isreplay)
evs.Save(evt);
agrs.SaveOrUpdate(aggregateRoot,version+1);
}
if (!isreplay)
{
foreach (var e in events)
{
HandleMessage(e, isreplay);
}
}
}
public void HandleEvent(IEvent e)
{
HandleMessage(e,true);
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using SimpleCQRS2.Framework.Services;
namespace SimpleCQRS2.Framework.BDD
{
public abstract class CQRSFixture
{
protected IAnIOC MyIOC;
protected ICommandBus MyMessageBus;
protected IEventStore MyEventStore;
protected IAggregateRootStore MyArStore;
protected abstract IEnumerable<object> MessageHandlers {get;}
protected CQRSFixture()
{
MyIOC = new IOCStub();
MyArStore = new AggregateRootStore();
MyEventStore = new EventStore();
MyMessageBus = new CQRSCommandBus(MyIOC,MyEventStore,MyArStore);
foreach (var inst in MessageHandlers)
{
var t = inst.GetType();
MyIOC.Register(t, inst);
foreach (var it in CQRSCommandBus.GetMessageHandlerInterfaces(t))
MyIOC.Register(it, inst);
}
}
public void Given(IEvent Anevent)
{
MyMessageBus.HandleEvent(Anevent);
}
public void When(ICommand c)
{
MyMessageBus.HandleCommand(c);
}
public void When(IEvent e)
{
MyMessageBus.HandleEvent(e);
}
public CQRSFixture ThenEvents<T>(Predicate<IEnumerable<T>> pred) where T:IEvent
{
Assert.True(pred(MyEventStore.All<T>()));
return this;
}
public CQRSFixture ThenAggregateRoot<T>(Guid aggregateRootId,Predicate<T> pred) where T:IAggregateRoot
{
var ar = (T)MyArStore.GetAggregateRoot(typeof(T),aggregateRootId);
MyMessageBus.RunEvents(ar);
Assert.True(pred(ar));
return this;
}
public CQRSFixture ThenServices<T>(Predicate<IEnumerable<T>> pred)
{
Assert.True(pred(MyIOC.Resolve<T>()));
return this ;
}
public CQRSFixture ThenService<T>(Predicate<T> pred)
{
Assert.True(pred(MyIOC.Resolve<T>().First()));
return this ;
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
namespace SimpleCQRS2.Framework.Services
{
public class EventStore : IEventStore
{
public void Save(IEvent Event)
{
var lastevent = Store.Where(x=>x.Event.AggregateRootId == Event.AggregateRootId).LastOrDefault();
var version = lastevent == null ? 0 : lastevent.Version+1;
Store.Add(new StoredEvent {
Event = Event,
Version = version
});
}
public IEvent NextEvent(Type T,Guid aggregateRootId, int version)
{
return Store
.Where(x =>
x.Event.AggregateRootType == T &&
x.Event.AggregateRootId == aggregateRootId &&
x.Version ==version )
.Select(x => x.Event)
.FirstOrDefault();
}
public IEnumerable<T> All<T>() where T : IEvent
{
return Store.Select(x => x.Event).OfType<T>();
}
List<StoredEvent> Store = new List<StoredEvent>();
class StoredEvent
{
public IEvent Event { get; set; }
public int Version { get; set; }
}
}
}
// BDD fixtures when using CQRS/Eventsourcing = win
//
// ToJans@twitter
//
// To run
// - open a command window
// - go to the build folder (bin/debug)
// - type "consolerunner example\*.txt" for normal output
// - type "consolerunner example\*.txt -html > output.html" for output to a file named output.html
// - type "consolerunner -h" for help
//
// check out the BDDAppContext source, it is practicly a 1 to 1 conversion
Define the appcontext
using SimpleCQRS2.Example.BDD.BDDAppContext
from SimpleCQRS2.DLL
Story This is the proof of concept that BDD works perfectly with CQRS/eventsourcing
is about the appcontext
As a developer
I want to be able to use CQRS/eventsourcing for this example of a positive number
In order to show how easy it is to set up BDD contexts
Scenario [Integration]Decrement command does not work/raise events when the value is zero
Given a positive number named nr1 was created
When I decrement nr1
And I increment nr1
And I increment nr1
And I decrement nr1
Then it should have incremented 2 times
And it should have decremented 1 time
And it should have changed the value 3 times
Scenario [AggregateRoot]When raising an increment or decrement event on a positive number, it should modify the value
When a positive number named nr1 was created
And a positive number named nr2 was created
And nr1 was incremented
And nr1 was incremented
And nr2 was incremented
And nr2 was incremented
And nr2 was decremented
Then the value of nr1 should be 2
And the value of nr2 should be 1
Scenario [Readmodel]When raising a valuechanged event, it should modify the readmodel's matching values
Given a positive number named nr1 was created
And a positive number named nr2 was created
And nr1 was incremented
And nr1 was incremented
And nr2 was incremented
When the value of nr1 was changed
And the value of nr2 was changed
Then the screen value for nr1 should be 2
And the screen value for nr2 should be 1
using System;
using System.Collections.Generic;
using System.Linq;
namespace SimpleCQRS2.Framework
{
// interfaces
public interface IHasAggregateRootId
{
Guid AggregateRootId { get;set;}
}
public interface IAggregateRoot : IHasAggregateRootId
{
}
public interface IMessage:IHasAggregateRootId
{
Type AggregateRootType { get; set; }
}
public interface IMessage<TAggregate>
: IMessage where TAggregate : IAggregateRoot
{
}
public interface IMessageHandler<TAggregate,TMessage>
where TAggregate:IAggregateRoot
where TMessage : IMessage<TAggregate>
{
IEnumerable<IEvent> Handle(TMessage message, TAggregate aggregateRoot);
}
public interface IEvent : IMessage {}
public interface ICommand : IMessage {}
public interface ICommandBus
{
void HandleCommand(ICommand command);
void RunEvents(IAggregateRoot aggregateRoot);
void HandleEvent(IEvent e);
}
public interface IAggregateRootStore
{
IAggregateRoot GetAggregateRoot(Type aggregateRootType, Guid aggregateRootId);
int GetAggregateRootVersion(Type aggregateRootType, Guid aggregateRootId);
void SaveOrUpdate(IAggregateRoot ar,int version);
}
public interface IEventStore
{
// save should set the version as well
void Save(IEvent Event);
IEvent NextEvent(Type t,Guid AggregateRootId,int version);
IEnumerable<T> All<T>() where T:IEvent;
}
public interface IAnIOC
{
void Register(Type T, object instance);
IEnumerable<object> Resolve(Type T);
IEnumerable<T> Resolve<T>();
}
}
using System;
using System.Collections.Generic;
using System.Linq;
namespace SimpleCQRS2.Framework.Services
{
public class IOCStub : IAnIOC
{
public IEnumerable<object> Resolve(Type T)
{
return instances[T];
}
public void Register(Type T, object instanceGetter)
{
if (!instances.ContainsKey(T))
instances.Add(T, new List<object>());
instances[T].Add(instanceGetter);
}
public IEnumerable<T> Resolve<T>()
{
foreach (var i in Resolve(typeof(T)))
yield return (T)i;
}
Dictionary<Type, List<object>> instances = new Dictionary<Type, List<object>>();
}
}
// simple CQRS
// attempt to understand CQRS the way it is supposed to work,
// and set it up for easy BDD testing
// tojans@twitter
// this is a running test
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using SimpleCQRS2.Example.Domain.Handlers;
using SimpleCQRS2.Example.Domain.Model;
using SimpleCQRS2.Example.Views;
using SimpleCQRS2.Framework.BDD;
namespace SimpleCQRS2.Example.BDD
{
[TestFixture()]
public class NUnitTest : CQRSFixture
{
protected override IEnumerable<object> MessageHandlers {
get {
yield return new PositiveNumberHandler();
yield return new SomeReadModelUpdater();
}
}
[Test()]
public void IntegrationTest()
{
var anotherguid = Guid.NewGuid();
var aguid = Guid.NewGuid();
Given(new PositiveNumber.Created() {AggregateRootId = aguid });
Given(new PositiveNumber.Created() {AggregateRootId = anotherguid });
Given(new PositiveNumber.WasIncremented() {AggregateRootId = aguid });
Given(new PositiveNumber.WasIncremented() {AggregateRootId = aguid });
When(new PositiveNumber.Decrement(){ AggregateRootId = anotherguid });
When(new PositiveNumber.Increment() { AggregateRootId = anotherguid });
When(new PositiveNumber.Increment() { AggregateRootId = anotherguid });
When(new PositiveNumber.Decrement(){ AggregateRootId = anotherguid });
ThenEvents<PositiveNumber.WasIncremented>(x=>x.Count()==4);
ThenEvents<PositiveNumber.WasDecremented>(x=>x.Count() == 1);
ThenEvents<PositiveNumber.ValueHasChanged>(x=>x.Count() == 3);
ThenAggregateRoot<PositiveNumber>(aguid,pn=>pn.Value == 2);
ThenAggregateRoot<PositiveNumber>(anotherguid,pn=>pn.Value == 1);
ThenService<SomeReadModelUpdater>( s=>s.Values[anotherguid] == 1);
}
}
}
Aubergine Console Runner - Core bvba - Tom Janssens 2009
Parsing files
Processing file(s) : Example\Integration.txt
Parsing file : C:\Tom Hotouch\CQRS\gist-560498\bin\Debug\Example\Integration.txt
Running Tests
Story This is the proof of concept that BDD works perfectly with CQRS/eventsourcing =>OK
Context the appcontext =>OK
Scenario [Integration]Decrement command does not work/raise events when the value is zero =>OK
Given a positive number named nr1 was created =>OK
When I decrement nr1 =>OK
When I increment nr1 =>OK
When I increment nr1 =>OK
When I decrement nr1 =>OK
Then it should have incremented 2 times =>OK
Then it should have decremented 1 time =>OK
Then it should have changed the value 3 times =>OK
Scenario [AggregateRoot]When raising an increment or decrement event on a positive number, it should modify the value =>OK
When a positive number named nr1 was created =>OK
When a positive number named nr2 was created =>OK
When nr1 was incremented =>OK
When nr1 was incremented =>OK
When nr2 was incremented =>OK
When nr2 was incremented =>OK
When nr2 was decremented =>OK
Then the value of nr1 should be 2 =>OK
Then the value of nr2 should be 1 =>OK
Scenario [Readmodel]When raising a valuechanged event, it should modify the readmodel's matching values =>OK
Given a positive number named nr1 was created =>OK
Given a positive number named nr2 was created =>OK
Given nr1 was incremented =>OK
Given nr1 was incremented =>OK
Given nr2 was incremented =>OK
When the value of nr1 was changed =>OK
When the value of nr2 was changed =>OK
Then the screen value for nr1 should be 2 =>OK
Then the screen value for nr2 should be 1 =>OK
using System;
using System.Linq;
using SimpleCQRS2.Framework;
using SimpleCQRS2.Example.Domain.Model;
namespace SimpleCQRS2.Example.Domain.Model
{ // domain model
public partial class PositiveNumber : IAggregateRoot
{
public int Value = 0;
public Guid AggregateRootId { get; set; }
}
// AR messages
public partial class PositiveNumber
{
public abstract class Message : IMessage<PositiveNumber>
{
public Guid AggregateRootId { get; set; }
public Type AggregateRootType { get; set; }
public Message()
{
AggregateRootType = typeof(PositiveNumber);
}
}
public class Create: Message, ICommand {};
public class Created : Message, IEvent {};
public class Increment : Message, ICommand {}
public class Decrement : Message, ICommand {}
public class WasIncremented : Message, IEvent {}
public class WasDecremented : Message, IEvent {}
public class ValueHasChanged : Message, IEvent {}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Messaging;
using SimpleCQRS2.Framework;
using SimpleCQRS2.Example.Domain.Model;
namespace SimpleCQRS2.Example.Domain.Handlers
{ // domain services / handlers
public class PositiveNumberHandler
: IMessageHandler<PositiveNumber,PositiveNumber.Increment>
, IMessageHandler<PositiveNumber,PositiveNumber.WasIncremented>
, IMessageHandler<PositiveNumber,PositiveNumber.Decrement>
, IMessageHandler<PositiveNumber,PositiveNumber.WasDecremented>
, IMessageHandler<PositiveNumber,PositiveNumber.Create>
, IMessageHandler<PositiveNumber,PositiveNumber.Created>
{
public IEnumerable<IEvent> Handle(PositiveNumber.Increment command, PositiveNumber ar)
{
yield return new PositiveNumber.WasIncremented { AggregateRootId = ar.AggregateRootId };
}
public IEnumerable<IEvent> Handle(PositiveNumber.Decrement command, PositiveNumber ar)
{
if (ar.Value > 0) {
yield return new PositiveNumber.WasDecremented { AggregateRootId = ar.AggregateRootId };
}
}
public IEnumerable<IEvent> Handle(PositiveNumber.WasIncremented evt, PositiveNumber ar)
{
ar.Value++;
yield return new PositiveNumber.ValueHasChanged { AggregateRootId = ar.AggregateRootId };
}
public IEnumerable<IEvent> Handle(PositiveNumber.WasDecremented evt, PositiveNumber ar)
{
ar.Value--;
yield return new PositiveNumber.ValueHasChanged { AggregateRootId = ar.AggregateRootId };
}
public IEnumerable<IEvent> Handle(PositiveNumber.Create message, PositiveNumber aggregateRoot)
{
yield return new PositiveNumber.Created() {AggregateRootId = Guid.NewGuid() };
}
public IEnumerable<IEvent> Handle(PositiveNumber.Created message, PositiveNumber aggregateRoot)
{
aggregateRoot.Value = 0;
yield return new PositiveNumber.ValueHasChanged() { AggregateRootId = aggregateRoot.AggregateRootId };
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using SimpleCQRS2.Framework;
using SimpleCQRS2.Example.Domain.Model;
namespace SimpleCQRS2.Example.Views
{
public class SomeReadModelUpdater
: IMessageHandler<PositiveNumber,PositiveNumber.ValueHasChanged>
{
public Dictionary<Guid, int> Values = new Dictionary<Guid, int>();
public IEnumerable<IEvent> Handle(PositiveNumber.ValueHasChanged evt, PositiveNumber aggregateRoot)
{
Values[evt.AggregateRootId] = (aggregateRoot as PositiveNumber).Value;
yield break;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment