MessageHandler

How to implement Aggregate Roots

A

Aggregate roots are responsible for maintaining the integrity of a cluster of objects, the aggregate.

Their primary responsibility is deciding if a command can be executed on the aggregate or not and record that decission.

Defining an Aggregate Root

To define an Aggregate Root, create a class which inherits from EventSourced. Which is part of the MessageHandler.EventSourcing nuget package.

PM> Install-Package MessageHandler.EventSourcing

EventSourced can be found in the MessageHandler.EventSourcing.DomainModel namespace.

using MessageHandler.EventSourcing.DomainModel

And requires an id to be passed in. This id field is used to retrieve the correct stream from the event store.

public class OrderBooking : EventSourced
{   
    public OrderBooking(string id) : base(id)
    {
    }
}

Naming Aggregate Roots

We advice to name aggregate roots after process steps and not regular entities.

For example: call your aggregate root OrderBooking, instead of Order.

As the booking step is the point in an e-commerce process where a PurchaseOrder (what the buyer asks) gets converted into a SalesOrder (what the seller promises to delivery).

Modeling your domain model along these axis will result in more maintainable code in the long run.

Where is the aggregate?

The entities PurchaseOrder and SalesOrder are part of the aggregate that is being protected by this root.

At least conceptually... they may or may not explicitly exist as classes, in the private model of the root class.

That said, their data must be embedded in the events, so that at any time in the future, when the root has a need for that data to validate the invariants of the aggregate, code can be added to do so.

Sourcing from events

In order to protect the invariants of the aggregate, the root usually needs access to data from prior decissions it took.

Therefore the root must be able to restore its internal model by reapplying persisted events.

To restore internal state, implement the IApply<TEvent> interface for every event type emitted from the instance before.

In the implementation of the Apply method, copy over any state that may be embedded on the event into internal fields.

Only copy information that is actually needed to validate the business rules required, and nothing more to reduce future refactoring effort.

public class OrderBooking : EventSourced,
    IApply<BookingStarted>
{   
    private bool _allreadyStarted;

    public OrderBooking(string id) : base(id)
    {
    }

    public void Apply(BookingStarted msg)
    {
        _allreadyStarted = true;
    }
}

Before you can invoke a command on the aggregate instance, you need to provide it its history first.

Manually sourcing events

This can be done manually, by passing in an IEnumerable<SourcedEvent> to the RestoreFrom method.

var history = new List<SourcedEvent>();
var booking = new OrderBooking();
booking.RestoreFrom(history);

Using the aggregate repository

Sourcing events can also be taken care of for you, using the Get<TEventSourced> operation on an instance of IEventSourcedRepository.

IEventSourcedRepository repository = runtime.CreateAggregateRepository();
var booking = await repository.Get<OrderBooking>(bookingId);

This Get operation will load the events from the underlying eventstore, using TEventSourced as a stream type and the passed in id as stream id.

All events will be deserialized, sorted, and passed into the RestoreFrom operation, of a new instance, prior to returning the instance.

The restored instance is now ready to handle new commands.

Implementing commands

Implementing commands is as straightforward as adding a new method to the class, e.g. PlacePurchaseOrder.

The method needs to do two things:

  • Validate if the command can be executed.
  • Record the decission by passing an event representing the decission to the Emit method on the base class.
public class OrderBooking : EventSourced,
        IApply<BookingStarted>
{
    private bool _allreadyStarted;

    public OrderBooking(string id) : base(id)
    {
    }

    public void PlacePurchaseOrder(PurchaseOrder purchaseOrder)
    {
        if(_allreadyStarted) return;

        Emit(new BookingStarted(){
            PurchaseOrder = purchaseOrder
        });
    }    

    public void Apply(BookingStarted msg)
    {
        _allreadyStarted = true;
    }
}

Passing an event to the Emit method, will also call the corresponding Apply method, so that you can keep any internal state changes in a single place.

This way you can guarantee that the internal state of an instance is the same regardless if it got restored from the eventstore, or built up by invoking commands.

Persisting changes

Emitted events, are not yet persisted though, they exist in memory only.

The most common scenario is to leave this responsibility to the aggregate repository, the one used to obtain the instance, by calling Flush.

await repository.Flush());

Unittesting

While unittesting it might be desireable to manually commit the changes, by calling Commit on the root instance, instead.

[Fact]
public void GivenNewBookingProcess_WhenPlacingValidPurchaseOrder_ThenPurchaseOrderIsCarriedOnEvent()
{
    // given
    var history = new List<SourcedEvent>();
    var booking = new OrderBooking(bookingId);
    booking.RestoreFrom(history);

    //when
    var purchaseOrder = new PurchaseOrder();
    booking.PlacePurchaseOrder(purchaseOrder);

    //then
    var pendingEvents = booking.Commit();
    var bookingStarted = pendingEvents.FirstOrDefault(e => typeof(BookingStarted).IsAssignableFrom(e.GetType()));

    Assert.NotNull(bookingStarted.PurchaseOrder);
}

This allows you to inspect the messages that would otherwise be stored.

Resolving repository from the IOC container

The EventSourcedRepository is also available for resolution from the IOC container, if the service collection was passed into the handler runtime configuration.

The EventSourcedRepository is registered with its interface IInvokeProjections and can be injected like this:

public class HomeController
{
    public HomeController(IEventSourcedRepository repository)
    {
        
    }
}

Sign up to our newsletter to get notified about new content and releases

You can unsubscribe at any time by clicking the link in the footer of your emails. I use Mailchimp as my marketing platform. By clicking subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp's privacy practices here.