Projections
Marten has a strong model for user-defined projections of the raw event data. Projections are used within Marten to create read-side views of the raw event data.
Choosing a Projection Type
TIP
Do note that all the various types of aggregated projections inherit from a common base type and have the same core set of conventions. The aggregation conventions are best explained in the Aggregate Projections page.
- Single Stream Projections combine events from a single stream into a single view.
- Multi Stream Projections are a specialized form of projection that allows you to aggregate a view against arbitrary groupings of events across streams.
- Event Projections are a recipe for building projections that create or delete one or more documents for a single event
- Custom Aggregations are a recipe for building aggregate projections that require more logic than can be accomplished by the other aggregation types. Example usages are soft-deleted aggregate documents that maybe be recreated later or if you only apply events to an aggregate if the aggregate document previously existed.
- If one of the built in projection recipes doesn't fit what you want to do, you can happily build your own custom projection
Projection Lifecycles
Marten varies a little bit in that projections can be executed with three different lifecycles:
- Inline Projections are executed at the time of event capture and in the same unit of work to persist the projected documents
- Live Aggregations are executed on demand by loading event data and creating the projected view in memory without persisting the projected documents
- Asynchronous Projections are executed by a background process (eventual consistency)
For other descriptions of the Projections pattern inside of Event Sourcing architectures, see:
Aggregates
Aggregates condense data described by a single stream. Marten only supports aggregation via .Net classes. Aggregates are calculated upon every request by running the event stream through them, as compared to inline projections, which are computed at event commit time and stored as documents.
The out-of-the box convention is to expose public Apply(<EventType>)
methods on your aggregate class to do all incremental updates to an aggregate object.
Sticking with the fantasy theme, the QuestParty
class shown below could be used to aggregate streams of quest data:
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()
};
}
Live Aggregation via .Net
You can always fetch a stream of events and build an aggregate completely live from the current event data by using this syntax:
await using var session2 = store.LightweightSession();
// questId is the id of the stream
var party = await session2.Events.AggregateStreamAsync<QuestParty>(questId);
var party_at_version_3 = await session2.Events
.AggregateStreamAsync<QuestParty>(questId, 3);
var party_yesterday = await session2.Events
.AggregateStreamAsync<QuestParty>(questId, timestamp: DateTime.UtcNow.AddDays(-1));
There is also a matching asynchronous AggregateStreamAsync()
mechanism as well. Additionally, you can do stream aggregations in batch queries with IBatchQuery.Events.AggregateStream<T>(streamId)
.
Inline Projections
First off, be aware that some event metadata (IEvent.Version
and IEvent.Sequence
) is not available during the execution of inline projections when using the "Quick" append mode. If you need to use this metadata in your projections, please use asynchronous or live projections, or use the "Rich" append mode.
If you would prefer that the projected aggregate document be updated inline with the events being appended, you simply need to register the aggregation type in the StoreOptions
upfront when you build up your document store like this:
var store = DocumentStore.For(_ =>
{
_.Connection(ConnectionSource.ConnectionString);
_.Events.TenancyStyle = tenancyStyle;
_.DatabaseSchemaName = "quest_sample";
if (tenancyStyle == TenancyStyle.Conjoined)
{
_.Schema.For<QuestParty>().MultiTenanted();
}
// This is all you need to create the QuestParty projected
// view
_.Projections.Snapshot<QuestParty>(SnapshotLifecycle.Inline);
});
At this point, you would be able to query against QuestParty
as just another document type.
Rebuilding Projections
Projections need to be rebuilt when the code that defines them changes in a way that requires events to be reapplied in order to maintain correct state. Using an IDaemon
this is easy to execute on-demand:
Refer to Rebuilding Projections for more details.
WARNING
Marten by default while creating new object tries to use default constructor. Default constructor doesn't have to be public, might be also private or protected.
If class does not have the default constructor then it creates an uninitialized object (see here for more info)
Because of that, no member initializers will be run so all of them need to be initialized in the event handler methods.