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 class QuestParty
{
public List<string> Members { get; set; } = new();
public IList<string> Slayed { get; } = new List<string>();
public string Key { get; set; }
public string Name { get; set; }
// In this particular case, this is also the stream id for the quest events
public Guid Id { get; set; }
// These methods take in events and update the QuestParty
public void Apply(MembersJoined joined) => Members.Fill(joined.Members);
public void Apply(MembersDeparted departed) => Members.RemoveAll(x => departed.Members.Contains(x));
public void Apply(QuestStarted started) => Name = started.Name;
public override string ToString()
{
return $"Quest party '{Name}' is {Members.Join(", ")}";
}
}
Marten provides the ability to use IEvent<T>
metadata within your projections, assuming that you're not trying to run the aggregations inline.
The syntax using the built in aggregation technique is to take in IEvent<T>
as the argument to your Apply(event)
methods, where T
is the event type you're interested is covered in Single Stream Projections.
public class QuestPartyWithEvents
{
private readonly IList<string> _members = new List<string>();
public string[] Members
{
get
{
return _members.ToArray();
}
set
{
_members.Clear();
_members.AddRange(value);
}
}
public IList<string> Slayed { get; } = new List<string>();
public void Apply(MembersJoined joined)
{
_members.Fill(joined.Members);
}
public void Apply(MembersDeparted departed)
{
_members.RemoveAll(x => departed.Members.Contains(x));
}
public void Apply(QuestStarted started)
{
Name = started.Name;
}
public string Name { get; set; }
public Guid Id { get; set; }
public override string ToString()
{
return $"Quest party '{Name}' is {Members.Join(", ")}";
}
}
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 session = store.LightweightSession())
{
// questId is the id of the stream
var party = session.Events.AggregateStream<QuestParty>(questId);
Console.WriteLine(party);
var party_at_version_3 = await session.Events
.AggregateStreamAsync<QuestParty>(questId, 3);
var party_yesterday = await session.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 event metadata (e.g. stream version and sequence number) are not available during the execution of inline projections. If you need to use event metadata in your projections, please use asynchronous or live projections.
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.