Skip to content

Projecting

Replaying builds state on demand, one entity at a time. A projection is the complement: a read model that EventSourcingKit keeps up to date continuously, in the background, as events are stored. Projections are how you build queryable views – a catalog of all books, say – without replaying them on every request.

How Projecting Works

EventSourcingKit runs a background observer that watches the event store. Whenever a new event is stored, the observer feeds it to your projections, which update their read models through reducers. The observer remembers its position with a checkpoint, so it resumes exactly where it left off after a restart – all of this is set up for you.

Defining the Projection

A projection derives from Projection<TId> and holds the shape you want to query:

using EventSourcingKit.Projecting;

public record Book : Projection<Guid>
{
    public string Title { get; init; } = "";
    public string Author { get; init; } = "";
    public bool IsCheckedOut { get; init; }
}

Its reducers look just like the ones for state, but live in the EventSourcingKit.Projecting namespace. An update reducer additionally tells the projection which entity an event belongs to, through GetId:

using EventSourcingKit;
using EventSourcingKit.Projecting;

public class BookRegisteredReducer
    : ICreateReducer<Book, Guid, BookRegistered>
{
    public Book Create(Event<BookRegistered> @event) =>
        new()
        {
            Id = @event.Data.BookId,
            Title = @event.Data.Title,
            Author = @event.Data.Author
        };
}

public class BookCheckedOutReducer
    : IUpdateReducer<Book, Guid, BookCheckedOut>
{
    public Guid GetId(Event<BookCheckedOut> @event) => @event.Data.BookId;

    public Book Apply(Book projection, Event<BookCheckedOut> @event) =>
        projection with { IsCheckedOut = true };
}

Registering the Projection

A projection needs a data store that persists its read model. To get started, use the built-in InMemoryProjectionRepository, registered alongside EventSourcingKit:

using EventSourcingKit;
using EventSourcingKit.Defaults;
using EventSourcingKit.Projecting;

builder.Services.AddEventSourcingKit(
        builder.Configuration,
        [typeof(BookRegistered).Assembly]
    )
    .AddProjection<
        Book,
        Guid,
        InMemoryProjectionRepository<Book, Guid>,
        IProjectionRepository<Book, Guid>
    >();

You can now inject IProjectionRepository<Book, Guid> anywhere in your application to query the projection through GetById, GetManyById and GetAll. That interface is the fourth type argument above, and Persisting Projections shows it in full.

Moving beyond in-memory

The in-memory store is perfect for getting started, but its data is lost on restart. For durable read models backed by PostgreSQL or MongoDB, see Persisting Projections.

For More Information