Skip to content

Instantly share code, notes, and snippets.

@luizdamim
Last active December 29, 2015 13:39
Show Gist options
  • Save luizdamim/7679115 to your computer and use it in GitHub Desktop.
Save luizdamim/7679115 to your computer and use it in GitHub Desktop.
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;
}
}
}
using System;
namespace commondomain
{
[Serializable]
public class AccountClosedEvent : IDomainEvent
{
}
}
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;
}
}
}
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;
}
}
}
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; }
}
}
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;
}
}
}
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;
}
}
}
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;
}
}
}
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());
}
}
}
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());
}
}
}
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);
}
}
}
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);
}
}
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);
}
}
namespace commondomain
{
// A marker interface I have for my own sanity. Useful for convention-based
// analysis and verification
public interface IDomainEvent
{
}
}
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);
}
}
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!");
}
}
}
using System;
namespace commondomain
{
public class OmgSadnessNotifier : IEventHandler<AccountClosedEvent>
{
public void Handle(AccountClosedEvent e)
{
Console.WriteLine("Dude, we lost a customer... start the layoffs :(");
}
}
}
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();
}
}
}
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