Skip to content

Custom Projections

To build your own Marten projection, you just need a class that implements the Marten.Events.Projections.IProjection interface shown below:

cs
/// <summary>
///     Interface for all event projections
/// </summary>
public interface IProjection
{
    /// <summary>
    ///     Apply inline projections during synchronous operations
    /// </summary>
    /// <param name="operations"></param>
    /// <param name="streams"></param>
    void Apply(IDocumentOperations operations, IReadOnlyList<StreamAction> streams);

    /// <summary>
    ///     Apply inline projections during asynchronous operations
    /// </summary>
    /// <param name="operations"></param>
    /// <param name="streams"></param>
    /// <param name="cancellation"></param>
    /// <returns></returns>
    Task ApplyAsync(IDocumentOperations operations, IReadOnlyList<StreamAction> streams,
        CancellationToken cancellation);
}

snippet source | anchor

The StreamAction aggregates outstanding events by the event stream, which is how Marten tracks events inside of an IDocumentSession that has yet to be committed. The IDocumentOperations interface will give you access to a large subset of the IDocumentSession API to make document changes or deletions. Here's a sample custom projection from our tests:

cs
public class QuestPatchTestProjection: IProjection
{
    public Guid Id { get; set; }

    public string Name { get; set; }

    public void Apply(IDocumentOperations operations, IReadOnlyList<StreamAction> streams)
    {
        var questEvents = streams.SelectMany(x => x.Events).OrderBy(s => s.Sequence).Select(s => s.Data);

        foreach (var @event in questEvents)
        {
            if (@event is Quest quest)
            {
                operations.Store(new QuestPatchTestProjection { Id = quest.Id });
            }
            else if (@event is QuestStarted started)
            {
                operations.Patch<QuestPatchTestProjection>(started.Id).Set(x => x.Name, "New Name");
            }
        }
    }

    public Task ApplyAsync(IDocumentOperations operations, IReadOnlyList<StreamAction> streams,
        CancellationToken cancellation)
    {
        Apply(operations, streams);
        return Task.CompletedTask;
    }
}

snippet source | anchor

And the custom projection can be registered in your Marten DocumentStore like this:

cs
var store = DocumentStore.For(opts =>
{
    opts.Connection("some connection string");

    // Use inline lifecycle
    opts.Projections.Add(new QuestPatchTestProjection(), ProjectionLifecycle.Inline);

    // Or use this as an asychronous projection
    opts.Projections.Add(new QuestPatchTestProjection(), ProjectionLifecycle.Async);
});

snippet source | anchor

Released under the MIT License.