Click here to Skip to main content
Click here to Skip to main content
Technical Blog

Tagged as

Winning the game with CQRS/event sourcing and BDD

, 5 Sep 2010 CPOL
Rate this:
Please Sign up or sign in to vote.
Winning the game with CQRS/event sourcing and BDD

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
{	// 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 {}
	}
}

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
{	// 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 };
		}
	}
}

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();
		}
	}
}

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Tom Janssens
Founder Core bvba
Belgium Belgium
Tom Janssens, owner of Core, a software and consultancy company.
Father of two sons named Quinten & Matisse, and married to a beautiful woman named Liesbeth.
 
Blog: http://tojans.me
Github: http://github.com/ToJans
Twitter: http://twitter.com/ToJans
LinkedIn: http://www.linkedin.com/in/tomjanssens

Comments and Discussions

 
QuestionWhat is it? PinmentorSandeep Mewara3-Sep-10 23:48 
AnswerRe: What is it? PinmemberTom Janssens5-Sep-10 9:38 
I now included the github code fragments... I hope this makes it more clear... (it is not an introduction to either BDD or CQRS, so if you are not familiar with both of these concepts, this will not make a lot of sense to you.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.141220.1 | Last Updated 5 Sep 2010
Article Copyright 2010 by Tom Janssens
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid