Fork me on GitHub

Custom Projections Edit on GitHub


Multistream Projections using ViewProjection

The ViewProjection class is an implementation of the IProjection that can handle building a projection from multiple streams.

This can be setup from configuration like:


StoreOptions(_ =>
{
    _.AutoCreateSchemaObjects = AutoCreate.All;
    _.Events.TenancyStyle = tenancyStyle;
    _.Events.InlineProjections.AggregateStreamsWith<QuestParty>();
    _.Events.ProjectView<PersistedView, Guid>()
        .ProjectEventAsync<QuestStarted>((view, @event) => { view.Events.Add(@event); return Task.CompletedTask; })
        .ProjectEventAsync<MembersJoined>(e => e.QuestId, (view, @event) => { view.Events.Add(@event); return Task.CompletedTask; })
        .ProjectEventAsync<ProjectionEvent<MonsterSlayed>>(e => e.Data.QuestId, (view, @event) => { view.Events.Add(@event.Data); return Task.CompletedTask; })
        .DeleteEvent<QuestEnded>()
        .DeleteEvent<MembersDeparted>(e => e.QuestId)
        .DeleteEvent<MonsterDestroyed>((session, e) => session.Load<QuestParty>(e.QuestId).Id);
});
or through a class like:

public class PersistAsyncViewProjection : ViewProjection<PersistedView, Guid>
{
    public PersistAsyncViewProjection()
    {
        ProjectEventAsync<QuestStarted>(PersistAsync);
        ProjectEventAsync<MembersJoined>(e => e.QuestId, PersistAsync);
        ProjectEventAsync<MonsterSlayed>((session, e) => session.Load<QuestParty>(e.QuestId).Id, PersistAsync);
        DeleteEvent<QuestEnded>();
        DeleteEvent<MembersDeparted>(e => e.QuestId);
        DeleteEvent<MonsterDestroyed>((session, e) => session.Load<QuestParty>(e.QuestId).Id);
    }

    private Task PersistAsync<T>(PersistedView view, T @event)
    {
        view.Events.Add(@event);
        return Task.CompletedTask;
    }
}

`ProjectEvent` and `DeleteEvent` can operate on events that need a single or multiple Ids operated on. With `ProjectEvent` if a `List` is passed, the handler method will be called for each Id in the collection. With `DeleteEvent` if a `List` is passed, then each document tied to the Id in the collection will be removed. Each of these methods take various overloads that allow selecting the Id field implicitly, through a property or through two different Funcs `Func` and `Func`. If additional Marten event details are needed, then events can use the `ProjectionEvent<>` generic when setting them up with `ProjectEvent`. `ProjectionEvent` exposes the Marten Id, Version, Timestamp and Data.

Projections are created during the DocumentStore creation by default. Marten gives also possible to register them with factory method. With such registration projections are created on runtime during the events application. Thanks to that it's possible to setup custom creation logic or event connect dependency injection mechanism.


StoreOptions(_ =>
{
    _.AutoCreateSchemaObjects = AutoCreate.All;
    _.Events.TenancyStyle = tenancyStyle;
    _.Events.InlineProjections.AggregateStreamsWith<QuestParty>();
    _.Events.InlineProjections.Add(() => new PersistViewProjectionWithInjection(logger));
});

By convention it's needed to provide the default constructor with projections definition and other with code injection (that calls the default constructor).


public class PersistViewProjectionWithInjection : PersistViewProjection
{
    private readonly Logger logger;

    public PersistViewProjectionWithInjection() : base()
    {
        ProjectEvent<QuestPaused>(@event => @event.QuestId, LogAndPersist);
    }

    public PersistViewProjectionWithInjection(Logger logger) : this()
    {
        this.logger = logger;
    }

    private void LogAndPersist<T>(PersistedView view, T @event)
    {
        logger.Log($"Handled {typeof(T).Name} event: {@event.ToString()}");
        view.Events.Add(@event);
    }
}