Sponsor: Using RabbitMQ or Azure Service Bus in your .NET systems? Well, you could just use their SDKs and roll your own serialization, routing, outbox, retries, and telemetry. I mean, seriously, how hard could it be?

public class ShoppingCart { public Guid Id { get; set; } public IList<ShoppingCartLineItem> LineItems { get; set; } public bool IsComplete { get; set; } public DateTime CompleteDate { get; set; } public ShoppingCart(Guid id) { Id = id; LineItems = new List<ShoppingCartLineItem>(); } public void AddProduct(string productSku, int quantity) { if (string.IsNullOrEmpty(productSku)) throw new ArgumentException("productSku"); if (quantity <= 0) throw new ArgumentException("quantity"); LineItems.Add(new ShoppingCartLineItem(productSku, quantity)); } public void Complete() { if (IsComplete) throw new InvalidOperationException("Already completed."); if (LineItems.Any() == false) throw new InvalidOperationException("Cannot complete with no products."); IsComplete = true; CompleteDate = DateTime.UtcNow; } }There are a few things to point out in the above code. Take note of the public properties which represent our current state. These are the pieces of data that we be persisted to data storage. Notice we have a IList<ShoppingCartLineItem> which is our collection of products that are in our shopping cart. The ShoppingCartLineItem class contains the Product SKU and the quantity ordered. One invariant in this example to mention is in the Complete() method. There must be at least one product added to the shopping cart in order to mark it as complete.
if (LineItems.Any() == false) throw new InvalidOperationException("Cannot complete with no products.");Next, let’s implement the above with event sourcing.
public class ShoppingCart : AggregateRoot { private Guid _id; private bool _completed; private DateTime _completedDate; private readonly IList _lineItems; public override Guid Id { get { return _id; } } private void Apply(ShoppingCartCreated e) { _id = e.Id; } private void Apply(ProductAdded e) { _lineItems.Add(new ShoppingCartLineItem(e.ProductSku, e.Quantity)); } private void Apply(Completed e) { _completed = true; _completedDate = e.DateCompleted; } public ShoppingCart(Guid id) { _lineItems = new List(); ApplyChange(new ShoppingCartCreated(id)); } public void AddProduct(string productSku, int quantity) { if (string.IsNullOrEmpty(productSku)) throw new ArgumentException("productSku"); if (quantity <= 0) throw new ArgumentException("quantity"); ApplyChange(new ProductAdded(_id, productSku, quantity)); } public void Complete() { if (_completed) throw new InvalidOperationException("Already completed."); if (_lineItems.Any() == false) throw new InvalidOperationException("Cannot complete with no products."); ApplyChange(new Completed(_id, DateTime.UtcNow)); } }In our event sourced implementation above we still are holding current state in the same fashion as our non event sourced version. I’ve changed them from public properties to private fields since they are not being persisted to data storage. Instead our events raised through ApplyChange() are what are persisted to data storage. The example above is typical of a first implementation of event sourcing. We are taking the concept we know and use of persisting current state along with this new idea of using events as state transitions. There is actually a lot of code that can be removed from our event sourced example above. ShoppingCartLineItem class is no longer needed because the data it encapsulates is now encapsulated in our ProductAdded event. So if we delete that class, then we have the issue with:
private readonly IList<ShoppingCartLineItem> _lineItems;This private member will no longer be valid since we just deleted the ShoppingCartLineItem class. So we can remove this private member as well. Now let’s go back to that invariant that we have:
if (_lineItems.Any() == false) throw new InvalidOperationException("Cannot complete with no products.");How can we maintain this invariant since we are no longer keep track of the line items? You only need the data applicable to an invariant Because of this, we don’t really care what products or quantities are on the shopping cart, just that there is at least one. Any data that is not used by an invariant does not need to be in your aggregate.
private DateTime _completedDate;We also no longer need the date the shopping cart was completed because it is not used by an invariant and it is also contained in the Completed event. Now let’s take a look at our shopping cart with these ideas applied:
public class ShoppingCart : AggregateRoot { private Guid _id; private bool _containsProducts; private bool _completed; public override Guid Id { get { return _id; } } private void Apply(ShoppingCartCreated e) { _id = e.Id; } private void Apply(ProductAdded e) { _containsProducts = true; } private void Apply(Completed e) { _completed = true; } public ShoppingCart(Guid id) { ApplyChange(new ShoppingCartCreated(id)); } public void AddProduct(string productSku, int quantity) { if (string.IsNullOrEmpty(productSku)) throw new ArgumentException("productSku"); if (quantity <= 0) throw new ArgumentException("quantity"); ApplyChange(new ProductAdded(_id, productSku, quantity)); } public void Complete() { if (_completed) throw new InvalidOperationException("Already completed."); if (_containsProducts == false) throw new InvalidOperationException("Cannot complete with no products."); ApplyChange(new Completed(_id, DateTime.UtcNow)); } }The invariant is now based on a boolean set when a product has been added. The aggregate does not care about what products were added or their quantity. It doesn’t even need to have them in a collection. With the implementation of event sourcing in our aggregate, we get a much clearer view of what data drives business logic and is important in our domain model.