项目作者: stevewgh

项目描述 :
Tiny DDD Aggregate
高级语言: C#
项目地址: git://github.com/stevewgh/TinyAggregate.git
创建时间: 2017-10-16T14:53:49Z
项目社区:https://github.com/stevewgh/TinyAggregate

开源协议:MIT License

下载


TinyAggregate

A Tiny Domain Driven Design (DDD) Aggregate base class and interfaces which are designed to simplify development when using an event sourcing pattern.

Highlights

  • No magic! Events are applied to the aggregate using the Visitor pattern
  • Auto wiring of event handling by the Aggregate<TVisitor> class
  • Consistent event handling (applying and replaying events)
  • Uncommited events allow unit testing of the aggregate by asserting the events produced by the aggregate
  • Version numbers allows easy concurrency checks when integrating with Event stores

Getting started

  1. Install-Package TinyAggregate
  1. Define an interface that acts as the Visitor between the Events and Aggregate. Create a method for all domain events that your aggregate handles.

    1. interface IVehicleVisitor
    2. {
    3. void Visit(EngineStarted engineStarted);
    4. }
  2. Create your domain events, implement the IAcceptVisitors<TVisitor> interface and supply the visitor interface you created as the generic parameter. Provide the implementation and call the visitor with the object instance (see example).

    1. class EngineStarted : IAcceptVisitors<IVehicleVisitor>
    2. {
    3. public void Accept(IVehicleVisitor visitor) {
    4. visitor.Visit(this);
    5. }
    6. }
  3. Create your aggregate, inherit from the Aggregate<TVisitor> class and supply the visitor interface you created as the generic parameter. Provide your domain specific operations and any domain events generated should be passed to the ApplyEvent() method.

    1. class Vehicle : Aggregate<IVehicleVisitor>
    2. {
    3. public void StartTheEngine() {
    4. ApplyEvent(new EngineStarted());
    5. }
    6. }
  4. Testing that it works is really easy. Using xUnit we could do something like this (notice the cast to the base interface to expose the UncommitedEvents collection):

    1. [Fact]
    2. public void Vehicle_Should_Have_Started_When_StartTheEngine_Is_Called()
    3. {
    4. // arrange
    5. var car = new Vehicle();
    6. // act
    7. car.StartTheEngine();
    8. // assert
    9. ((IAggregate<IVehicleVisitor>)car).UncommitedEvents.OfType<EngineStarted>().Count().Should().Be(1);
    10. }

    A simpler way of accessing the IAggregate interface is to use the ToAggregate() extension method. Using the
    extension method the assert would look like this:

    1. // assert
    2. car.ToAggregate().UncommitedEvents.OfType<EngineStarted>().Count().Should().Be(1);

Doing something with the events

So now you’ve got an aggregate, it’s tested and working great, but what now? Well this is where your domain will dictate what should happen. Our vehicle aggregate supports starting the engine, but nothing tells us if the engine is running, this is where the visitor interface comes into play again.

It’s important to keep the domain action (StartTheEngine()) separate to the application of the events which the action creates. This is so that the events can be applied and replayed without the initial domain action being called again. The way we do that in TinyAggregate is by calling the ApplyEvent() method.

Free event handling wireup

A nice side effect of implementing the visitor interface on the aggregate is that we are notified when the event should be applied. This wiring up is done in the Aggregate class so we don’t need to concern ourselves with it. One thing to note, in our Vehicle class we have explicitly implemented the IVehicleVisitor interface to keep the public interface of the Vehicle class clean.

  1. class Vehicle : Aggregate<IVehicleVisitor>, IVehicleVisitor
  2. {
  3. public bool EngineIsRunning { get; private set; }
  4. public void StartTheEngine() {
  5. ApplyEvent(new EngineStarted());
  6. }
  7. // this method will be called when Apply() or Replay() are called with an EngineStarted event
  8. void IVehicleVisitor.Visit(EngineStarted engineStarted) {
  9. EngineIsRunning = true;
  10. }
  11. }

Putting it all together:

  1. static void Main(string[] args)
  2. {
  3. var car = new Vehicle();
  4. Console.WriteLine($"Engine running: {car.EngineIsRunning}");
  5. car.StartTheEngine();
  6. Console.WriteLine($"Engine running: {car.EngineIsRunning}");
  7. Console.ReadLine();
  8. }

Saving events:

How you choose to save the events is up to you, but getting them is really easy.

  1. var car = new Vehicle();
  2. car.StartTheEngine();
  3. // save the events somewhere
  4. var events = car.ToAggregate().UncommitedEvents;

Replaying events, e.g. loading the aggregate:

Retrieve the events from memory or storage, then call the Replay method. You are responsible for telling the aggregate which is the current version, the reason for this is in case the event store has allowed a snapshot to be taken and therefore the number of events being replayed and version number will no longer match.

  1. var events = GetEventsFromStore();
  2. var car = new Vehicle();
  3. car.ToAggregate().Replay(events.Count, events);

Using an injected visitor

If you prefer to keep the visitor and aggregate completely separate then you need to override the Visitor property and return your own visitor instance. However, as the handling of events and storage of any state will be done in different classes, you will need a mechanism of updating one from the other.

  1. class Vehicle : Aggregate<IVehicleVisitor>
  2. {
  3. private readonly IVehicleVisitor visitor;
  4. protected override IVehicleVisitor Visitor { get; }
  5. // set is now internal to allow the injected visitor to set it
  6. public bool EngineIsRunning { get; internal set; }
  7. public Vehicle(IVehicleVisitor visitor) {
  8. this.visitor = visitor;
  9. }
  10. public void StartTheEngine() {
  11. ApplyEvent(new EngineStarted());
  12. }
  13. }