
I noticed there’s a new book coming out next month on CQRS & Event Sourcing from a .NET perspective, so I was of course curious to look at their sample code. I was admittedly looking to see if the samples used Marten and the rest of the Critter Stack, but also to just see what approach the author was using. The answers were “something else” for the tooling but what triggered this post is that the samples were all built around the old AggregateRoot pattern that was popular in the early days of Event Sourcing. So, just to summarize the rest of this post:
When you are doing Event Sourcing, I very strongly recommend you express your domain logic using the Decider Pattern and avoid the old AggregateRoot Pattern.
If you see any halfway recent literature on doing Event Sourcing and CQRS with the Critter Stack, you’ll see examples of command handlers that use the “Decider” pattern taken from Functional Programming folks like this one:
public record CategoriseIncident( IncidentCategory Category, Guid CategorisedBy, int Version);public static class CategoriseIncidentEndpoint{ // This is Wolverine's form of "Railway Programming" // Wolverine will execute this before the main endpoint, // and stop all processing if the ProblemDetails is *not* // "NoProblems" public static ProblemDetails Validate(Incident incident) { return incident.Status == IncidentStatus.Closed ? new ProblemDetails { Detail = "Incident is already closed" } // All good, keep going! : WolverineContinue.NoProblems; } // This tells Wolverine that the first "return value" is NOT the response // body [EmptyResponse] [WolverinePost("/api/incidents/{incidentId:guid}/category")] public static IncidentCategorised? Post( // the actual command CategoriseIncident command, // Wolverine is generating code to look up the Incident aggregate // data for the event stream with this id [Aggregate("incidentId")] Incident incident) { if (incident.Category == command.Category) return null; // This is a simple case where we're just appending a single event to // the stream. return new IncidentCategorised(incident.Id, command.Category, command.CategorisedBy); }}
Focus on the Post() method up above. It’s a pure function, meaning that it only deals with inputs and returns outputs. No invocations of infrastructure whatsoever. Its only real responsibility is to “decide” what if any new events should be appended to mark a change of state given the initial state of the current Incident and the incoming CategoriseIncident command. There’s of course plenty of infrastructure around that simple function, but Wolverine is mediating between the Marten persistence and your little domain logic up above.
To level set, there’s an example of using AggregateRoot in a section below just in case you’re not already familiar with that pattern.
From here on out though, here some of the reasons why I heavily recommend using the Decider over AggregateRoot in today’s development work:
- The decider pattern makes the relationship much more clear between incoming commands and the events emitted. In other words, I think the Decider usage will make your system easier to reason about by removing layers and indirection.
- I think unit testing is easier with the pure Decider functions than the aggregates
- From years of supporting users and now JasperFx clients using Marten, people using the AggregateRoot pattern clearly run into more problems troubleshooting and debugging their application to the point where I tell people that I will not support them online if they run into trouble with their AggregateRoot usage (unless they’re a JasperFx customer of course, but all of our customers generally use the Decider in newer work)
- Marten with or without Wolverine has a lot of performance optimizations built in with the assumption that the projected aggregates are not mutated by application code. You can still get *some* of that goodness by using immutable types for aggregates, but that would blow up your aggregate root usage anyway
- I’ll agree with a common opinion that it’s helpful to separate the “evolve” functionality of how the projected aggregate state is calculated away from the “decide” logic. Especially when projections need to change later.
As a bonus, using the Decider pattern in Wolverine handlers 100% makes it a lot easier for our CritterWatch tool to derive an accurate Event Model visualization for your system.
You can find plenty of other articles out there from other people making the same case I am here, but usually with some kind of “Functional Programming is better than Object Oriented Programming” hook. My thinking on that has changed quite a bit over the years and you can clearly see much more FP thinking evident in both Marten and especially Wolverine. As much as I despise getting lectured online by FP enthusiasts and my own history as an OOP guy, I’ll say that “It doesn’t matter if a cat is black or white, as long as it catches mice” and happily use the FP Decider approach over the OOP AggregateRoot in this particular case.
As folks frequently point out, Wolverine especially has veered into FP-centric usage in our approach to command handlers or HTTP endpoints. I’ll simply say that that cat catches mice.
I also see this Decider > AggregateRoot valuation to be mostly orthogonal to the question of using “classical” stream-based event sourcing modeling versus the newer fangled Dynamic Consistency Boundary (DCB) approach because DCB very naturally fits well with Decider functions.
What’s this AggregateRoot thing anyway?
Let’s level set a little bit with a real example taken from some older documentation in Marten (which very much recommends against the approach), the “AggregateRoot” pattern calls for you to generally to model all writes to an event stream through a single aggregate type usually derived from something like this base type from our old docs example:
// Infrastructure to capture modifications to state in eventspublic abstract class AggregateBase{ // For indexing our event streams public string Id { get; protected set; } // For protecting the state, i.e. conflict prevention // The setter is only public for setting up test conditions public long Version { get; set; } // JsonIgnore - for making sure that it won't be stored in inline projection [JsonIgnore] private readonly List<object> _uncommittedEvents = new List<object>(); // Get the deltas, i.e. events that make up the state, not yet persisted public IEnumerable<object> GetUncommittedEvents() { return _uncommittedEvents; } // Mark the deltas as persisted. public void ClearUncommittedEvents() { _uncommittedEvents.Clear(); } protected void AddUncommittedEvent(object ) { // add the event to the uncommitted list _uncommittedEvents.Add(); }}
Then in real usage, it might be something like this:
public sealed class Invoice: AggregateBase{ public Invoice(int invoiceNumber) { if (invoiceNumber <= 0) { throw new ArgumentException("Invoice number needs to be positive", nameof(invoiceNumber)); } // Instantiation creates our initial event, capturing the invoice number var = new InvoiceCreated(invoiceNumber); // Call Apply to mutate state of aggregate based on event Apply(); // Add the event to uncommitted events to use it while persisting the events to Marten events store AddUncommittedEvent(); } // Public parameterless ctor for Marten's event-store replay. Pre-9.0 // Marten could call a private parameterless ctor reflectively; the // source-generated dispatch path in 9.0 emits direct calls instead, // which require public visibility. See the 9.0 migration guide. public Invoice() { } // Enforce any contracts on input, then raise event capturing the data public void AddLine(decimal price, decimal vat, string description) { if (string.IsNullOrEmpty(description)) { throw new ArgumentException("Description cannot be empty", nameof(description)); } var = new LineItemAdded(price, vat, description); // Call Apply to mutate state of aggregate based on event Apply(); // Add the event to uncommitted events to use it while persisting the events to Marten events store AddUncommittedEvent(); } public override string ToString() { var lineItems = string.Join(Environment.NewLine, lines.Select(x => $"{x.Item1}: {x.Item2} ({x.Item3}% VAT)")); return $"{lineItems}{Environment.NewLine}Total: {Total}"; } public decimal Total { get; private set; } private readonly List<Tuple<string, decimal, decimal>> lines = new List<Tuple<string, decimal, decimal>>(); // Apply methods need to be `public` for the source-generated dispatcher // to invoke them — pre-9.0 Marten reflected over private members; the // SG-emitted dispatch path in 9.0 emits direct method calls. See the // 9.0 migration guide. public void Apply(InvoiceCreated ) { Id = .InvoiceNumber.ToString(CultureInfo.InvariantCulture); // Ensure to update version on every Apply method. Version++; } public void Apply(LineItemAdded ) { var price = .Price * (1 + .Vat / 100); Total += price; lines.Add(Tuple.Create(.Description, price, .Vat)); // Ensure to update version on every Apply method. Version++; }}
And you’d maybe wrap the AggregateRoot access in a repository class that deals with some of the grubby technical details:
public sealed class AggregateRepository{ private readonly IDocumentStore store; public AggregateRepository(IDocumentStore store) { this.store = store; } public async Task StoreAsync(AggregateBase aggregate, CancellationToken ct = default) { await using var session = await store.LightweightSerializableSessionAsync(token: ct); // Take non-persisted events, push them to the event stream, indexed by the aggregate ID var events = aggregate.GetUncommittedEvents().ToArray(); session.Events.Append(aggregate.Id, aggregate.Version, events); await session.SaveChangesAsync(ct); // Once successfully persisted, clear events from list of uncommitted events aggregate.ClearUncommittedEvents(); } public async Task<T> LoadAsync<T>( string id, int? version = null, CancellationToken ct = default ) where T : AggregateBase { await using var session = await store.LightweightSerializableSessionAsync(token: ct); var aggregate = await session.Events.AggregateStreamAsync<T>(id, version ?? 0, token: ct); return aggregate ?? throw new InvalidOperationException($"No aggregate by id {id}."); }}
Again, this is a very old code sample from before Wolverine and the “aggregate handler workflow” (our version of the Decider pattern).