Yes !! I did it !!!
I have been making a few attempts to combine BDD with CQRS/event sourcing, since they seem to make a perfect fit.
After mailing to the DDD/CQRS newsgroup for a few times, I finally managed to make something presentable... this is the BDD part for the domain:
// 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 practically 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
And this is the resulting output after running the console runner:
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
This is the context, notice how setting up the interpreter usually only takes a single line (i.e. register an event, execute a command, or check something).
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 ;
}
}
}
And this is the implementation of the domain class:
using System;
using System.Linq;
using SimpleCQRS2.Framework;
using SimpleCQRS2.Example.Domain.Model;
namespace SimpleCQRS2.Example.Domain.Model
{
public partial class PositiveNumber : IAggregateRoot
{
public int Value = 0;
public Guid AggregateRootId { get; set; }
}
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 {}
}
}
And its domain handler:
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
{
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 };
}
}
}
As well as a view updater (in reality, this view store would be queried by a winform or webform directly).
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;
}
}
}
Please do note that the current Event Store and Aggregate store are not persisted; IRL you would have to persist the event store (forward write-only), and probably some snapshots from your aggregate root as well (maybe using a bigtable); this should be easy to add, I might do this in a later phase....
You can find the full source code here.
Update !!
I uploaded the source code to github at http://github.com/ToJans/CQRSNode.
I also added (currently synchronous) a node.js -like implementation... which is actually a really simple webserver...
This is the code that maps the requests:
using System;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using NetNode.Services;
using SimpleCQRS2.Example.Domain.Handlers;
using SimpleCQRS2.Example.Domain.Model;
using SimpleCQRS2.Example.Views;
using SimpleCQRS2.Framework;
using SimpleCQRS2.Framework.Services;
namespace NetNode
{
class AppConverter: HttpListenerRequestConverter
{
ICommandBus cb;
IAnIOC ioc;
IAggregateRootStore arstore;
IEventStore evstore;
public AppConverter()
{
ioc = new IOCStub();
foreach(var t in new Type[]
{typeof(PositiveNumberHandler),typeof(SomeReadModelUpdater)})
{
var inst = Activator.CreateInstance(t);
ioc.Register(t,inst);
foreach(var t2 in
CQRSCommandBus.GetMessageHandlerInterfaces(t))
ioc.Register(t2,inst);
}
evstore = new EventStore();
arstore = new AggregateRootStore();
cb = new CQRSCommandBus(ioc,evstore,arstore);
Register(req=> true, (req,resp) => GetView(null));
Register(req=> Regex.IsMatch(req.RawUrl,"/create")
, (req,resp) =>
{
cb.HandleCommand(new PositiveNumber.Create()
{ AggregateRootId = Guid.NewGuid()});
return GetView("positive number created");
});
Register(req=> Regex.IsMatch(req.RawUrl,"/increment/.+")
, (req,resp) =>
{
var guid = Guid.Parse(Regex.Match(req.RawUrl,
"/increment/(?<guid>.+)").Groups["guid"].Value);
cb.HandleCommand(new PositiveNumber.Increment()
{ AggregateRootId = guid});
return GetView("Number incremented");
});
Register(req=> Regex.IsMatch(req.RawUrl ,"/decrement/.+")
, (req,resp) =>
{
var guid = Guid.Parse(Regex.Match(req.RawUrl,
"/decrement/(?<guid>.+)").Groups["guid"].Value);
cb.HandleCommand(new PositiveNumber.Decrement()
{ AggregateRootId = guid});
return GetView("Number decremented");
});
}
private string GetView(string status)
{
var sb = new StringBuilder();
sb.Append("<html><body>");
if (!string.IsNullOrEmpty(status))
{
sb.AppendFormat("<h3>{0}</h3>",status);
}
var rm = ioc.Resolve<SomeReadModelUpdater>().First().Values;
sb.Append("<a href='http://www.codeproject.com/create'>
Add a positive number</a>");
sb.Append("<ul>");
foreach(var k in rm.Keys)
{
sb.Append("<li>");
sb.Append("<a href=
'http://www.codeproject.com/increment/"+
k.ToString() + "'>+</a>");
sb.Append(rm[k]);
sb.Append("<a href=
'http://www.codeproject.com/decrement/"+
k.ToString() + "'>-</a>");
sb.Append("</li>");
}
sb.Append("</ul>");
sb.Append("</body></html>");
return sb.ToString();
}
}
}