Eventuous
Published in

Eventuous

Eventuous 0.10: Aggregate identity and historical events

Eventuous is an OpenSource Event Sourcing library for .NET. Read mode at https://eventuous.dev

All the versions of Eventuous before 0.10 had the aggregate identity as part of the aggregate state by default, you weren’t able to avoid having it there. As a developer, you were also required to set the identity property of the aggregate state when applying the initial event:

public record BookingState : AggregateState<BookingState, BookingId> {
public BookingState() {
On<RoomBooked>((state, booked) => state with {
Id = new BookingId(booked.BookingId),
Price = booked.Price }
);
// more handlers here
}
}

The reason for that was that Eventuous AggregateStore uses the identity property of the aggregate state when persisting new events. Also, the application service command handler registrations like OnNew didn’t need a function to get the aggregate identity from the command, as Eventuous expected you to set the identity property of the aggregate state:

OnNew<BookRoom>(
(booking, cmd) => booking.BookRoom(
new BookingId(cmd.BookingId),
cmd.RoomId,
new StayPeriod(cmd.CheckIn, cmd.CheckOut),
cmd.Price)
);
OnExisting<RecordPayment>(
cmd => new BookingId(cmd.BookingId),
(booking, cmd) => booking.RecordPayment(
cmd.PaymentId, cmd.Amount)
);

You can see the difference between OnNew and OnExisting in the snippet above. Naturally, when handling a command on an aggregate instance that already exists, we need to load it first, so we need an id.

As a consequence, you’d need to pass the new instance of the aggregate identity to the aggregate method that initiates the state, like BookRoom in the example above. The BookRoom method also needs to propagate the id to the event it emits:

public void BookRoom(
BookingId id,
string roomId,
StayPeriod period,
decimal price,
string guestId) {
EnsureDoesntExist();
Apply(new RoomBooked(
id, roomId, period.CheckIn, period.CheckOut,
price, guestId)
);
}

Apart from the need to know the aggregate id when appending new events to the aggregate stream in AggregateStore , having the id in events is useful for projections, as you’d use it as a document or row id in the read model.

Everything works, but I observed some friction with this approach. Here are the concerns:

  • All the events are expected to have the aggregate id for projections, although the stream name can be translated to such an id.
  • You mustn’t forget to set the Id property of the aggregate state in the initial event handler of the aggregate state; otherwise, the aggregate store would crash.
  • Conceptually, the aggregate identity has no meaning for the aggregate logic and therefore is redundant.

After a substantial refactoring, I released Eventuous 0.10, which has all those issues resolved, or I tend to think so.

Let’s have a look at what changed.

First, the identity property of the aggregate state is gone. Therefore, a declaration like this

public record BookingState : AggregateState<BookingState, BookingId>

is not necessary anymore and should be replaced by

public record BookingState : AggregateState<BookingState>

I kept the application service aware of the identity type as it needs to work with strongly-typed IDs. The only difference is that OnNew and OnNewAsync also need an argument of a function that gets the aggregate identity from the command, like for OnExisting or OnAny :

OnNew<BookRoom>(
cmd => new BookingId(cmd.BookingId),
(booking, cmd) => booking.BookRoom(
cmd.RoomId,
new StayPeriod(cmd.CheckIn, cmd.CheckOut),
cmd.Price
)
);

You no longer need to pass the identity object to the aggregate, propagate it to events, or set the state Id property in event handlers. All that code can still be used, but it’s not required. Also, if you want to stick to the previous behaviour, you’d need to add an identity property to the aggregate state record yourself.

Overall, Eventuous code got simpler as the number of generic constraints decreased. The only breaking change introduced in the new version is the requirement to add the identity instantiation function to OnNew handler registration in the application service.

You might wonder what happens with projections (Eventuous only has built-in support for MongoDB projections), as events might not have the aggregate id anymore. The solution is to pass the consume context to event handlers instead of just passing the event. It is useful for various reasons; one of those is that you get access to the stream name that can be used to calculate the identity of the aggregate if needed.

The StreamName record got a helper function called GetId that uses the default convention of stream names (AggregateType-AggregateId) and calculates the id from the stream name:

On<RoomBooked>(b => b
.InsertOne
.Document(ctx =>
new BookingDocument(ctx.Stream.GetId()) {
BookingPrice = ctx.Message.Price,
Outstanding = ctx.Message.Price
}
)
);

For updates, you can use the new DefaultId function to simplify the code even more:

On<BookingPaymentRegistered>(b => b
.UpdateOne
.DefaultId()
.Update((evt, update)
=> update.Set(x => x.PaidAmount, evt.AmountPaid)
)
);

Of course, you can use your custom logic to calculate the document id from any of the properties available in the consume context. For that purpose, you need to use the Id(...) function instead of DefaultId.

Another significant change is more subtle from the API surface, as you might not even notice it.

Previously, the Aggregate<TState, TId> class had two properties for the version: OriginalVersion that tells you what aggregate version was loaded from the store, and CurrentVersion that increases as you apply events. For example, if you loaded an aggregate instance of version 3 and applied one event, the original version would be 3, and the current version would be 4. These properties are used by Eventuous to enable optimistic concurrency checks when persisting new events. Another property of the Aggregate base class is the collection of new events called Changes. The AggregateStore enumerates the content of the changes collection and persists them in the aggregate stream.

However, you’d never know the content of the stream when you load the aggregate unless you create a separate collection inside the aggregate state and add new entries there in When handlers. It seems strange as these events get loaded by AggregateStore anyway, so it is trivial to expose them. So, in Eventuous 0.10, you get these events in the Original property of the aggregate, which is a read-only collection of events.

The question you might have is, why would you need to look at those events? Sometimes, you don’t even need to project events to the state to check if something has happened before. At the end of the day, having a full history of state mutations as events is the essence of Event Sourcing, isn’t it?

For example, the Bookingaggregate in the test Domain project is checking if the payment was already registered before by using the state property

ImmutableList<PaymentRecord> PaymentRecords { get; init; }

It gets populated by the When handler:

On<BookingPaymentRegistered>((state, paid) 
=> state with {
PaymentRecords = state.PaymentRecords.Add(
new PaymentRecord(paid.PaymentId, paid.AmountPaid)),
AmountPaid = state.AmountPaid + paid.AmountPaid
}
);

The immutable list is then used to check if the payment has already been registered:

public bool HasPaymentRecord(string paymentId) 
=> PaymentRecords.Any(x => x.PaymentId == paymentId);

With the Original collection exposed, most of this code is redundant as you can query the collection of known events to find out exactly the same information:

bool HasPaymentRecord(string paymentId)
=> Current.OfType<BookingPaymentRegistered>()
.Any(x => x.PaymentId == paymentId);

The function moves from the aggregate state to the aggregate itself (where it belongs), and all the code related to the list of payments can be removed from the BookingState record.

Of course, you need to be aware that if you introduce another event that serves the same purpose as PaymentRegisteredyou’d need to include it in the query. Maybe at some point, you decide to bring back the payments list as the element of the state. But this happens typically at the later stage of the system lifetime when you get more events or new versions of events. Before that happens, you can just query previous events to find out what happened.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Alexey Zimarev

Alexey Zimarev

Alexey is the Event Sourcing and Domain-Driven Design enthusiast and promoter. He works as a Developer Advocate at Event Store and Chief Architect at ABAX.