
JasperFx Software is around and ready to assist you with getting the best possible results using the Critter Stack.
The projections model in Marten and now Polecat has evolved quite a bit over the past decade. Consider this simple aggregated projection of data for our QuestParty in our tests:
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(", ")}"; }}
That type is mutable, but the projection library underneath Marten and Polecat happily supports projecting to immutable types as well.
Some people actually like the conventional method approach up above with the Apply, Create, and ShouldDelete methods. From the perspective of Marten’s or Polecat’s internals, it’s always been helpful because the projection subsystem “knows” in this case that the QuestParty is only applicable to the specific event types referenced in those methods, and when you call this code:
var party = await query .Events .AggregateStreamAsync<QuestParty>(streamId);
Marten and Polecat are able to quietly use extra SQL filters to limit the events fetched from the database to only the types utilized by the projected QuestParty aggregate.
Great, right? Except that some folks don’t like the naming conventions, just prefer explicit code, or do some clever things with subclasses on events that can confuse Marten or Polecat about the precedence of the event type handlers. To that end, Marten 8.0 introduced more options for explicit code. We can rewrite the projection part of the QuestParty above to a completely different class where you can add explicit code:
public class QuestPartyProjection: SingleStreamProjection<QuestParty, Guid>{ public QuestPartyProjection() { // This is *no longer necessary* in // the very most recent versions of Marten, // but used to be just to limit Marten's // querying of event types when doing live // or async projections IncludeType<MembersJoined>(); IncludeType<MembersDeparted>(); IncludeType<QuestStarted>(); } public override QuestParty Evolve(QuestParty snapshot, Guid id, IEvent e) { snapshot ??= new QuestParty{ Id = id }; switch (e.Data) { case MembersJoined j: // Small helper in JasperFx that prevents // double values snapshot.Members.Fill(j.Members); break; case MembersDeparted departed: snapshot.Members.RemoveAll(x => departed.Members.Contains(x)); break; } return snapshot; }}
There are several more items in that SingleStreamProjection base type like versioning or fine grained control over asynchronous projection behavior that might be valuable later, but for now, let’s look at a new feature in Marten and Polecat that let’s you use explicit code right in the single aggregate type:
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; } public void Evolve(IEvent e) { switch (e.Data) { case QuestStarted _: // Little goofy, but this let's Marten know that // the projection cares about that event type break; case MembersJoined j: // Small helper in JasperFx that prevents // double values Members.Fill(j.Members); break; case MembersDeparted departed: Members.RemoveAll(x => departed.Members.Contains(x)); break; } } public override string ToString() { return $"Quest party '{Name}' is {Members.Join(", ")}"; }}
This is admittedly yet another convention method in terms of the method name and the possible arguments, but hopefully the switch statement approach is much more explicit for folks who prefer that. As an additional bonus, Marten is able to automatically register the event types via a source generator that the version of QuestParty just above is using automatically so that we get all the benefits of the event filtering without making users do extra explicit configuration.
Projecting to Immutable Views
Just for completeness, let’s look at alternative versions of QuestParty just to see what it looks like if you make the aggregate an immutable type. First up is the conventional method approach:
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() };}
And with the Evolve approach:
public sealed record QuestParty(Guid Id, List<string> Members){ public static QuestParty Evolve(QuestParty? party, IEvent e) { switch (e.Data) { case QuestStarted s: return new(s.QuestId, []); case MembersJoined joined: return party with { Members = party.Members.Union(joined.Members).ToList() }; case MembersDeparted departed: return party with { Members = party.Members.Where(x => !departed.Members.Contains(x)).ToList() }; case MembersEscaped escaped: return party with { Members = party.Members.Where(x => !escaped.Members.Contains(x)).ToList() }; } return party; }
Summary
What do I recommend? Honestly, just whatever you prefer. This is a case where I’d like everyone to be happy with one of the available options. And yes, it’s not always good that there is more than one way to do the same thing in a framework, but I think we’re going to just keep all these options in the long run. It wasn’t shown here at all, but I think we’ll kill off the early options to define projections through a ton of inline Lambda functions within a fluent interface. That stuff can just die.
In the medium and longer term, we’re going to be utilizing more source generators across the entire Critter Stack as a way of both eliminating some explicit configuration requirements and to optimize our cold start times. I’m looking forward to getting much more into that work.