Using Explicit Code for Marten Projections

A very important part of any event sourcing architecture is actually being able to interpret the raw events representing the current (or past) state of the system. That’s where Marten’s “Projection” subsystem comes into play as a way to compound a stream of events into a stateful object representing the whole state.

Most of the examples you’ll find of Marten projections will show you one of the aggregation recipes that heavily lean on conventional method signatures with Marten doing some “magic” around those method names, like this simple “self-aggregating” document type:

public record TodoCreated(Guid TodoId, string Description);
public record TodoUpdated(Guid TodoId, string Description);

public class Todo
{
    public Guid Id { get; set; }

    public string Description { get; set; } = null!;

    public static Todo Create(TodoCreated @event) => new()
    {
        Id = @event.TodoId,
        Description = @event.Description,
    };

    public void Apply(TodoUpdated @event)
    {
        Description = @event.Description;
    }
}

Notice the Apply() and Create() methods in the Todo class above. Those are following a naming convention that Marten uses to “know” how to update a Todo document with new information from events.

I (and by “I” I’m clearly taking responsibility for any problems with this approach) went down this path with Marten V4 as a way to make some performance optimizations at runtime. This approach goes okay if you stay well within the well lit path (create, update, maybe delete the aggregate document), but can break down when folks get “fancy” with things like soft deletes. Or all too frequently, this approach can confuse users when the problem domain gets more complex.

There’s an escape hatch though. We can toss aside all the conventional magic and the corresponding runtime magic that Marten does for these projections and just write some explicit code.

Using Marten’s “CustomProjection” recipe — which is just a way to use explicit code to do aggregations of event data — we can write the same functionality as above with this equivalent:

public record TodoCreated(Guid TodoId, string Description);
public record TodoUpdated(Guid TodoId, string Description);

public class Todo
{
    public Guid Id { get; set; }
    public string Description { get; set; } = null!;
}

// Need to inherit from CustomProjection 
public class TodoProjection: CustomProjection<Todo, Guid>
{
    public TodoProjection()
    {
        // This is kinda meh to me, but this tells
        // Marten how to do the grouping of events to
        // aggregated Todo documents by the stream id
        Slicer = new ByStreamId<Todo>();


        // The code below is only valuable as an optimization
        // if this projection is running in Marten's async
        // daemon to help the daemon filter candidate events faster
        IncludeType<TodoCreated>();
        IncludeType<TodoUpdated>();
    }

    public override ValueTask ApplyChangesAsync(DocumentSessionBase session, EventSlice<Todo, Guid> slice, CancellationToken cancellation,
        ProjectionLifecycle lifecycle = ProjectionLifecycle.Inline)
    {
        var aggregate = slice.Aggregate;
        foreach (var e in slice.AllData())
        {
            switch (e)
            {
                case TodoCreated created:
                    aggregate ??= new Todo { Id = slice.Id, Description = created.Description };
                    break;
                case TodoUpdated updated:
                    aggregate ??= new Todo { Id = slice.Id };
                    aggregate.Description = updated.Description;
                    break;
            }
        }
        
        // This is an "upsert", so no silly EF Core "is this new or an existing document?"
        // if/then logic here
        session.Store(aggregate);

        return new ValueTask();
    }
}

Putting aside the admitted clumsiness of the “slicing” junk, our projection code is just a switch statement. In hindsight, the newer C# switch expression syntax was just barely coming out when I designed the conventional approach. If I had it to do again, I think I would have focused harder on promoting the explicit logic and bypassed the whole conventions + runtime code generation thing for aggregations. Oh well.

For right now though, just know that you’ve got an escape hatch with Marten projections to “just write some code” any time the conventional approach causes you the slightest bit of grief.

3 thoughts on “Using Explicit Code for Marten Projections

  1. Would this work for a live aggregate as well and prevent code generation for it all together for that aggregate?

Leave a comment