Last active
December 29, 2015 13:39
-
-
Save luizdamim/7679115 to your computer and use it in GitHub Desktop.
Example of CommonDomain and NEventStore. Original:
- http://stackoverflow.com/questions/6948338/examples-of-testing-the-domain-using-joliver-commondomain-eventstore
- http://pastebin.com/upZS72W0
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using CommonDomain.Core; | |
namespace commondomain | |
{ | |
// This is my old buddy Account. It inherits from AggregateBase, which comes from CommonDomain. | |
// There's no real need to bring CommonDomain in if you don't want. It provides a couple simple mechanisms for me. | |
// First, it gives me the IRepository wrapper around EventStore which I use above in my CommandHandlers | |
// Second, it gives me a base that tracks all of my uncommitted changes for me. | |
// Third, it wires up, by convention, my event handlers (the private void Apply(SomeEvent @event) methods | |
public class Account : AggregateBase | |
{ | |
public string Name { get; private set; } | |
public string Twitter { get; private set; } | |
public bool IsActive { get; private set; } | |
public Account(Guid id, string name, string twitter) | |
{ | |
Id = id; | |
RaiseEvent(new AccountCreatedEvent(Id, name, twitter, true)); | |
} | |
// Aggregate should have only one public constructor | |
private Account(Guid id) | |
{ | |
Id = id; | |
} | |
public void Close() | |
{ | |
RaiseEvent(new AccountClosedEvent()); | |
} | |
private void Apply(AccountCreatedEvent @event) | |
{ | |
Id = @event.Id; | |
Name = @event.Name; | |
Twitter = @event.Twitter; | |
} | |
private void Apply(AccountClosedEvent e) | |
{ | |
IsActive = false; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
namespace commondomain | |
{ | |
[Serializable] | |
public class AccountClosedEvent : IDomainEvent | |
{ | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
namespace commondomain | |
{ | |
// This is going to seem a bit conflated so bear with me. When we create a new Account, | |
// we raise an AccountCreatedEvent. We then apply that AccountCreatedEvent to ourselves. | |
// Once we save our uncommitted events to EventStore, then that AccountCreatedEvent is also | |
// sent out to our bus for other interested parties. | |
[Serializable] | |
public class AccountCreatedEvent : IDomainEvent | |
{ | |
public Guid Id { get; private set; } | |
public string Name { get; private set; } | |
public string Twitter { get; private set; } | |
public bool IsActive { get; private set; } | |
public AccountCreatedEvent(Guid id, string name, string twitter, bool isActive) | |
{ | |
Id = id; | |
Name = name; | |
Twitter = twitter; | |
IsActive = isActive; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace commondomain | |
{ | |
// Normally this class would do something awesome like update Raven | |
// There's no reason for this to be a single denormalizer | |
// However, there's no reason for this to be multiple denormalizers. Design decision! | |
// Our use-case in production is that our denormalizers will update our flattened models in RavenDB | |
// although, honestly, it could be SQL Server, Mongo, Raik, whatever. | |
public class AccountDenormalizer : IEventHandler<AccountCreatedEvent>, IEventHandler<AccountClosedEvent> | |
{ | |
public string AccountName { get; private set; } | |
public bool IsActive { get; private set; } | |
public void Handle(AccountCreatedEvent e) | |
{ | |
AccountName = e.Name; | |
} | |
public void Handle(AccountClosedEvent e) | |
{ | |
IsActive = false; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace commondomain | |
{ | |
// On to the concrete part of the spike... we have accounts, accounts are an aggregate in my domain. | |
// For this spike, accounts have a name, are active or inactive (in the real world, they're deactivated for many reasons, but not here) | |
// and an account has an address (in the real world, they actually have a couple addresses. Again, not germane to this spike) | |
// | |
// | |
//This is just a value object in DDD parlance. It has no identity itself because it's always owned by an entity object. | |
public class Address | |
{ | |
public Address(string line1, string city, string state) | |
{ | |
this.Line1 = line1; | |
this.City = city; | |
this.State = state; | |
} | |
public string Line1 { get; private set; } | |
public string City { get; private set; } | |
public string State { get; private set; } | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Reflection; | |
using CommonDomain; | |
using CommonDomain.Persistence; | |
namespace commondomain | |
{ | |
// By convention, I want to provide two means for creating domain objects. To the public, I want | |
// to provide an always-valid constructor. This explicitly shows what needs to be provided to the domain | |
// to create a valid instance of that object (eg, Person needs a twitter handle to be valid if I were doing twitter stream analysis) | |
// Internally, to EventStore, I want it to be able to create my object via a private ctor and I'm going to pass in the | |
// objects id. | |
// This method is pretty simplistic but my current domain suits it just fine. | |
public class AggregateFactory : IConstructAggregates | |
{ | |
public IAggregate Build(Type type, Guid id, IMemento snapshot) | |
{ | |
ConstructorInfo constructor = type.GetConstructor( | |
BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeof(Guid) }, null); | |
return constructor.Invoke(new object[] { id }) as IAggregate; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
namespace commondomain | |
{ | |
//A command doesn't need to carry state if you don't want it to... Here, we're just telling it the account id to close. | |
public class CloseAccountCommand | |
{ | |
public Guid AccountId { get; private set; } | |
public CloseAccountCommand(Guid accountId) | |
{ | |
AccountId = accountId; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
namespace commondomain | |
{ | |
//Commands to do things are sent to your domain | |
//For a great discussion on validation with commands, check out http://ingebrigtsen.info/2012/07/28/cqrs-in-asp-net-mvc-with-bifrost/ | |
public class CreateAccountCommand | |
{ | |
public Guid Id { get; private set; } | |
public string Name { get; private set; } | |
public string Twitter { get; private set; } | |
public CreateAccountCommand(Guid accountId, string name, string twitter) | |
{ | |
Id = accountId; | |
Name = name; | |
Twitter = twitter; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using CommonDomain.Persistence; | |
namespace commondomain | |
{ | |
//This is the handler that will apply commands to my domain. I could choose | |
//another round of some sort of non-business rule validation here. I could | |
//log stuff. Whatever. There's also no reason that you need one CommandHandler | |
//per command. I'm just doing this because I think this is how our real impl will shape out. | |
//IRepository comes from CommonDomain and is a facade over EventStore (both by Jonathan Oliver) | |
public class CreateAccountCommandHandler : ICommandHandler<CreateAccountCommand> | |
{ | |
private readonly IRepository _repository; | |
public CreateAccountCommandHandler(IRepository repository) | |
{ | |
_repository = repository; | |
} | |
public void Handle(CreateAccountCommand command) | |
{ | |
var account = new Account(command.Id, command.Name, command.Twitter); | |
_repository.Save(account, Guid.NewGuid()); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using CommonDomain.Persistence; | |
namespace commondomain | |
{ | |
// This may _look_ like a normal "load this by id and then mutate state and then save it" repository | |
// However, it's actually loading the entire event stream for that object and re-applying the events to | |
// it. Don't worry, though, it's not re-publishing events to the bus.. it's just raising events | |
// internally to the object. Your domain object, at the end, will be the culmination of all of those | |
// applied events. This would be much simpler in F# of we thought about domain state as a left fold | |
// of immutable events causing state change. | |
// | |
// One neat thing about EventStore and, by extension, CommonDomain, is that you can load versions of your | |
// object. Check out the overloads on _repository.GetById some time. | |
public class DeactivateAccountCommandHandler : ICommandHandler<CloseAccountCommand> | |
{ | |
private readonly IRepository _repository; | |
public DeactivateAccountCommandHandler(IRepository repository) | |
{ | |
_repository = repository; | |
} | |
public void Handle(CloseAccountCommand command) | |
{ | |
var account = _repository.GetById<Account>(command.AccountId); | |
account.Close(); | |
_repository.Save(account, Guid.NewGuid()); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using MemBus; | |
using NEventStore; | |
namespace commondomain | |
{ | |
// Ok, so this is where things get a little.... interesting | |
// EventStore is sort of my coordinator for everything | |
// When I create a new domain object, I issue commands to it. It, in turn, raises events to change its internal state. | |
// | |
// Again, thing of the current state of a domain object as what you get after all events that built it are applied. | |
// new Person("@benhyr") might raise a PersonCreatedEvent. Then person.UpdateTwitterId("@hyrmn") raises a PersonUpdatedEvent | |
// When I load that Person from the EventStore, rather than getting Person.TwitterId from a db field, I'm getting PersonCreatedEvent | |
// (which sets the TwitterId initially) and then PersonUpdatedEvent (which updates the TwitterId to the new value) | |
// | |
// Now, back to this class. When I raise events, they're uncommitted until I persist them back to the EventStore. | |
// By default, we assume others might be interested in these uncommitted events. Of course, it's EventStore not EventStoreAndMessageBus | |
// (although EventStore could do some basic stuff for us). So, we're telling EventStore to publish to our MemBus bus... at some point, | |
// we might put NSB or MassTransit or EasyNetQ or whatever in place. | |
public static class DelegateDispatcher | |
{ | |
public static void DispatchCommit(IPublisher bus, Commit commit) | |
{ | |
// This is where we'd hook into our messaging infrastructure, such as NServiceBus, | |
// MassTransit, WCF, or some other communications infrastructure. | |
// This can be a class as well--just implement IDispatchCommits. | |
foreach (var @event in commit.Events) | |
bus.Publish(@event.Body); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class EndToEndTest | |
{ | |
private readonly SomeAwesomeUI _client; | |
private readonly IBus _bus; | |
// Here, I'm wiring up my MemBus instance and telling it how to resolve my subscribers | |
// MemBus also has an awesome way to resolve subscribers from an IoC container. In prod, | |
// I'll wire my subscribers into StructureMap and have MemBus resolve them from there. | |
// I'm also initializing my awesome test client UI which, if you'll recall from way back at the start | |
// simply publishes commands to my MemBus instance (and, again, it could be whatever) | |
public EndToEndTest() | |
{ | |
_bus = BusSetup.StartWith<Conservative>() | |
.Apply<FlexibleSubscribeAdapter>(a => | |
{ | |
a.ByInterface(typeof(IHandleEvent<>)); | |
a.ByInterface(typeof(IHandleCommand<>)); | |
}) | |
.Construct(); | |
_client = new SomeAwesomeUI(_bus); | |
} | |
[Fact] | |
public void CanPublishCreateAccountCommand() | |
{ | |
Should.NotThrow(() => _client.CreateNewAccount()); | |
} | |
[Fact] | |
public void CanReceiveCreateAccountCommand() | |
{ | |
var store = Wireup.Init().UsingInMemoryPersistence().Build(); | |
var handler = new CreateAccountCommandHandler(new EventStoreRepository(store, new AggregateFactory(), new ConflictDetector())); | |
_bus.Subscribe(handler); | |
Should.NotThrow(() => _client.CreateNewAccount()); | |
} | |
[Fact] | |
public void CreateAccountEventIsStored() | |
{ | |
var store = Wireup.Init().UsingInMemoryPersistence().Build(); | |
var repository = new EventStoreRepository(store, new AggregateFactory(), new ConflictDetector()); | |
var handler = new CreateAccountCommandHandler(repository); | |
_bus.Subscribe(handler); | |
var accountId = _client.CreateNewAccount(); | |
store.OpenStream(accountId, 0, int.MaxValue).CommittedEvents.Count.ShouldBeGreaterThan(0); | |
} | |
[Fact] | |
public void CanLoadAccountFromEventStore() | |
{ | |
var store = Wireup.Init().UsingInMemoryPersistence().Build(); | |
var repository = new EventStoreRepository(store, new AggregateFactory(), new ConflictDetector()); | |
var handler = new CreateAccountCommandHandler(repository); | |
_bus.Subscribe(handler); | |
var accountId = _client.CreateNewAccount(); | |
var account = repository.GetById<Account>(accountId); | |
account.ShouldNotBe(null); | |
account.Name.ShouldBe("Testy"); | |
} | |
[Fact] | |
public void CreateAccountEventIsPublishedToBus() | |
{ | |
var store = Wireup.Init().UsingInMemoryPersistence() | |
.UsingSynchronousDispatchScheduler() | |
.DispatchTo(new DelegateMessageDispatcher(c => DelegateDispatcher.DispatchCommit(_bus, c))) | |
.Build(); | |
var handler = new CreateAccountCommandHandler(new EventStoreRepository(store, new AggregateFactory(), new ConflictDetector())); | |
var denormalizer = new AccountDenormalizer(); | |
_bus.Subscribe(handler); | |
_bus.Subscribe(denormalizer); | |
_client.CreateNewAccount(); | |
denormalizer.AccountName.ShouldBe("Testy"); | |
} | |
[Fact] | |
public void DeactivingAccountDoesntRetriggerInitialCreate() | |
{ | |
var store = Wireup.Init().UsingInMemoryPersistence() | |
.UsingSynchronousDispatchScheduler() | |
.DispatchTo(new DelegateMessageDispatcher(c => DelegateDispatcher.DispatchCommit(_bus, c))) | |
.Build(); | |
var createHandler = new CreateAccountCommandHandler(new EventStoreRepository(store, new AggregateFactory(), new ConflictDetector())); | |
var deactivateHandler = new DeactivateAccountCommandHandler(new EventStoreRepository(store, new AggregateFactory(), new ConflictDetector())); | |
var denormalizer = new AccountDenormalizer(); | |
_bus.Subscribe(createHandler); | |
_bus.Subscribe(deactivateHandler); | |
_bus.Subscribe(denormalizer); | |
var accountId = _client.CreateNewAccount(); | |
_client.CloseAccount(accountId); | |
denormalizer.AccountName.ShouldBe("Testy"); | |
denormalizer.IsActive.ShouldBe(false); | |
store.OpenStream(accountId, 0, int.MaxValue).CommittedEvents.Count.ShouldBe(2); | |
} | |
//For fun, run this with the Debugger (eg, if using TDD.NET then right click on this method and select Test With -> Debugger. | |
//Put break points in various spots of the code above and see what happens. | |
[Fact] | |
public void TyingtTogether() | |
{ | |
var store = Wireup.Init().UsingInMemoryPersistence() | |
.UsingSynchronousDispatchScheduler() | |
.DispatchTo(new DelegateMessageDispatcher(c => DelegateDispatcher.DispatchCommit(_bus, c))) | |
.Build(); | |
var createHandler = new CreateAccountCommandHandler(new EventStoreRepository(store, new AggregateFactory(), new ConflictDetector())); | |
var deactivateHandler = new DeactivateAccountCommandHandler(new EventStoreRepository(store, new AggregateFactory(), new ConflictDetector())); | |
var denormalizer = new AccountDenormalizer(); | |
_bus.Subscribe(createHandler); | |
_bus.Subscribe(deactivateHandler); | |
_bus.Subscribe(denormalizer); | |
_bus.Subscribe(new KaChingNotifier()); | |
_bus.Subscribe(new OmgSadnessNotifier()); | |
var accountId = _client.CreateNewAccount(); | |
_client.CloseAccount(accountId); | |
denormalizer.AccountName.ShouldBe("Testy"); | |
denormalizer.IsActive.ShouldBe(false); | |
store.OpenStream(accountId, 0, int.MaxValue).CommittedEvents.Count.ShouldBe(2); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace commondomain | |
{ | |
//By convention, I mark my command handlers with this interface. It's partially to handle wiring and partially | |
//so I can toss around things like contravariant | |
public interface ICommandHandler<in TCommand> | |
{ | |
void Handle(TCommand command); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace commondomain | |
{ | |
// A marker interface I have for my own sanity. Useful for convention-based | |
// analysis and verification | |
public interface IDomainEvent | |
{ | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace commondomain | |
{ | |
// Again, another convention interface so I can tell my bus how to resolve my handlers. | |
// Any party that wants to know about a particular event will mark itself as such. | |
public interface IEventHandler<in TEvent> | |
{ | |
void Handle(TEvent e); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
namespace commondomain | |
{ | |
// And, to show multiple event handlers in action, here's a handler that might | |
// do something like send an email welcoming the person that just registered | |
// or maybe a cool SignalR tie-in that goes to the sales dashboard | |
// or a web service endpoint that has a Netduino polling it and ringing a gong when | |
// someone signs up. You know, whatever. | |
public class KaChingNotifier : IEventHandler<AccountCreatedEvent> | |
{ | |
public void Handle(AccountCreatedEvent e) | |
{ | |
Console.WriteLine("Dude, we got a customer, we're gonna be rich!"); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
namespace commondomain | |
{ | |
public class OmgSadnessNotifier : IEventHandler<AccountClosedEvent> | |
{ | |
public void Handle(AccountClosedEvent e) | |
{ | |
Console.WriteLine("Dude, we lost a customer... start the layoffs :("); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using CommonDomain.Core; | |
using CommonDomain.Persistence.EventStore; | |
using MemBus; | |
using MemBus.Configurators; | |
using MemBus.Subscribing; | |
using NEventStore; | |
using NEventStore.Dispatcher; | |
namespace commondomain | |
{ | |
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
var bus = BusSetup.StartWith<Conservative>() | |
.Apply<FlexibleSubscribeAdapter>(a => | |
{ | |
a.ByInterface(typeof(IEventHandler<>)); | |
a.ByInterface(typeof(ICommandHandler<>)); | |
}) | |
.Construct(); | |
var someAwesomeUi = new SomeAwesomeUi(bus); | |
var store = Wireup.Init() | |
.UsingInMemoryPersistence() | |
.UsingSynchronousDispatchScheduler() | |
.DispatchTo(new DelegateMessageDispatcher(c => DelegateDispatcher.DispatchCommit(bus, c))) | |
.Build(); | |
var handler = | |
new CreateAccountCommandHandler(new EventStoreRepository(store, new AggregateFactory(), | |
new ConflictDetector())); | |
bus.Subscribe(handler); | |
bus.Subscribe(new KaChingNotifier()); | |
someAwesomeUi.CreateNewAccount(Guid.NewGuid(), "Luiz", "@luizvd"); | |
Console.ReadLine(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using MemBus; | |
namespace commondomain | |
{ | |
class SomeAwesomeUi | |
{ | |
private readonly IBus bus; | |
public SomeAwesomeUi(IBus bus) | |
{ | |
this.bus = bus; | |
} | |
public void CreateNewAccount(Guid accountId, string name, string twitter) | |
{ | |
var createCommand = new CreateAccountCommand(accountId, name, twitter); | |
bus.Publish(createCommand); | |
} | |
public void CloseAccount(Guid accountId) | |
{ | |
var closeCommand = new CloseAccountCommand(accountId); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment