
Marten has very rich support for projecting events into read, write, or query models. While there are other capabilities as well, the most common usage is probably to aggregate related events into a singular view. Marten projections can be executed Live, meaning that Marten does the creation of the view by loading the target events into memory and building the view on the fly. Projections can also be executed Inline, meaning that the projected views are persisted as part of the same transaction that captures the events that apply to that projection. For this post though, I’m mostly talking about projections running asynchronously in the background as events are captured into the database (think eventual consistency).
Aggregate Projections in Marten combine some sort of grouping of events and process them to create a single aggregated document representing the state of those events. These projections come in two flavors:
Single Stream Projections create a rolled up view of all or a segment of the events within a single event stream. These projections are done either by using the SingleStreamProjection<TDoc, TId> base type or by creating a “self aggregating” Snapshot approach with conventional Create/Apply/ShouldDelete methods that mutate or evolve the snapshot based on new events.
Multi Stream Projections create a rolled up view of a user-defined grouping of events across streams. These projections are done by sub-classing the MultiStreamProjection<TDoc, TId> class and is further described in Multi-Stream Projections. An example of a multi-stream projection might be a “query model” within an accounting system of some sort that rolls up the value of all unpaid invoices by active client.
You can also use a MultiStreamProjection to create views that are a segment of a single stream over time or version. Imagine that you have a system that models the activity of a bank account with event sourcing. You could use a MultiStreamProjection to create a view that summarizes the activity of a single bank account within a calendar month.
The ability to use explicit code to define projections was hugely improved in the Marten 8.0 release.
Within your aggregation projection, you can express the logic about how Marten combines events into a view through either conventional methods (original, old school Marten) or through completely explicit code.
Within an aggregation, you have advanced options to:
- Use event metadata
- Enrich event data with other Marten or external data (newly improved!)
- Append all new events or send messages in response to projection updates with side effects
Simple Example
The most common usage is to create a “write model” that projects the current state for a single stream, so on that note, let’s jump into a simple example.
I’m huge into epic fantasy book series, hence the silly original problem domain in the very oldest code samples. Hilariously, Marten has fielded and accepted pull requests that corrected our modeling of the timeline of the Lord of the Rings in sample code.

Let’s say that we’re building a system to track the progress of a traveling party on a quest within an epic fantasy series like “The Lord of the Rings” or the “Wheel of Time” and we’re using event sourcing to capture state changes when the “quest party” adds or subtracts members. We might very well need a “write model” for the current state of the quest for our command handlers like this one:
public sealed record QuestParty(Guid Id, List<string> Members){ // These methods take in events and update the QuestParty public static QuestParty Create(QuestStarted started) => new(started.QuestId, []); public static QuestParty Apply(MembersJoined joined, QuestParty party) => party with { Members = party.Members.Union(joined.Members).ToList() }; public static QuestParty Apply(MembersDeparted departed, QuestParty party) => party with { Members = party.Members.Where(x => !departed.Members.Contains(x)).ToList() }; public static QuestParty Apply(MembersEscaped escaped, QuestParty party) => party with { Members = party.Members.Where(x => !escaped.Members.Contains(x)).ToList() };}
For a little more context, the QuestParty above might be consumed in a command handler like this:
public record AddMembers(Guid Id, int Day, string Location, string[] Members);public static class AddMembersHandler{ public static async Task HandleAsync(AddMembers command, IDocumentSession session) { // Fetch the current state of the quest var quest = await session.Events.FetchForWriting<QuestParty>(command.Id); if (quest.Aggregate == null) { // Bad quest id, do nothing in this sample case } var newMembers = command.Members.Where(x => !quest.Aggregate.Members.Contains(x)).ToArray(); if (!newMembers.Any()) { return; } quest.AppendOne(new MembersJoined(command.Id, command.Day, command.Location, newMembers)); await session.SaveChangesAsync(); }}
How Aggregation Works
Just to understand a little bit more about the capabilities of Marten’s aggregation projections, let’s look at the diagram below that tries to visualize the runtime workflow of aggregation projections inside of the Async Daemon background process:

- The Daemon is constantly pushing a range of events at a time to an aggregation projection. For example,
Events 1,000 to 2,000 by sequence number - The aggregation “slices” the incoming range of events into a group of
EventSliceobjects that establishes a relationship between the identity of an aggregated document and the events that should be applied during this batch of updates for that identity. To be more concrete, a single stream projection forQuestPartywould be creating anEventSlicefor each quest id it sees in the current range of events. Multi-stream projections will have some kind of custom “slicing” or grouping. For example, maybe in ourQuesttracking system we have a multi-stream projection that tries to track how many monsters of each type are defeated. That projection might “slice” by looking for allMonsterDefeatedevents across all streams and group or slice incoming events by the type of monster. The “slicing” logic is automatic for single stream projections, but will require explicit configuration or explicitly written logic for multi stream projections. - Once the projection has a known list of all the aggregate documents that will be updated by the current range of events, the projection will fetch each persisted document, first from any active aggregate cache in memory, then by making a single batched request to the Marten document storage for any missing documents and adding these to any active cache (see Optimizing Performance for more information about the potential caching).
- The projection will execute any event enrichment against the now known group of
EventSlice. This process gives you a hook to efficiently “enrich” the raw event data with extra data lookups from Marten document storage or even other sources. - Most of the work as a developer is in the application or “Evolve” step of the diagram above. After the “slicing”, the aggregation has turned the range of raw event data into
EventSliceobjects that contain the current snapshot of a projected document by its identity (if one exists), the identity itself, and the events from within that original range that should be applied on top of the current snapshot to “evolve” it to reflect those events. This can be coded either with the conventional Apply/Create/ShouldDelete methods or using explicit code — which is almost inevitably means aswitchstatement. Using theQuestPartyexample again, the aggregation projection would get anEventSlicethat contains the identity of an active quest, the snapshot of the currentQuestPartydocument that is persisted by Marten, and the newMembersJoinedet al events that should be applied to the existingQuestPartyobject to derive the new version ofQuestParty. - Just before Marten persists all the changes from the application / evolve step, you have the
RaiseSideEffects()hook to potentially raise “side effects” like appending additional events based on the now updated state of the projected aggregates or publishing the new state of an aggregate through messaging (Wolverine has first class support for Marten projection side effects through its Marten integration into the full “Critter Stack”) - For the current event range and event slices, Marten will send all aggregate document updates or deletions, new event appending operations, and even outboxed, outgoing messages sent via side effects (if you’re using the Wolverine integration) in batches to the underlying PostgreSQL database. I’m calling this out because we’ve constantly found in Marten development that command batching to PostgreSQL is a huge factor in system performance and the async daemon has been designed to try to minimize the number of network round trips between your application and PostgreSQL at every turn.
- Assuming the transaction succeeds for the current event range and the operation batch in the previous step, Marten will call “after commit” observers. This notification for example will release any messages raised as a side effect and actually send those messages via whatever is doing the actual publishing (probably Wolverine).
Marten happily supports immutable data types for the aggregate documents produced by projections, but also happily supports mutable types as well. The usage of the application code is a little different though.
Starting with Marten 8.0, we’ve tried somewhat to conform to the terminology used by the Functional Event Sourcing Decider paper by Jeremie Chassaing. To that end, the API now refers to a “snapshot” that really just means a version of the projection and “evolve” as the step of applying new events to an existing “snapshot” to calculate a new “snapshot.”










