Projections and Projectors¶
A projection is a read model that EventSourcingKit keeps current as events are stored. Where replaying builds state on demand, a projection is maintained continuously in the background, ready to be queried at any time. A projector is the component that applies events to a projection.
Defining a Projection¶
A projection derives from Projection<TId> and holds the shape you want to query. Every projection also carries the EventMetadata of the last event applied to it, which the library uses to stay idempotent:
public record Book : Projection<Guid>
{
public string Title { get; init; } = "";
public bool IsCheckedOut { get; init; }
}
Projection Reducers¶
Projection reducers live in the EventSourcingKit.Projecting namespace and come in more variants than their replaying counterparts, because a projection is written to storage rather than reduced in memory:
ICreateReducercreates a projection from an event.IUpdateReducerupdates an existing projection, and suppliesGetIdso the projector knows which one to load.IUpsertReducercreates or updates in one step.IDeleteReducerremoves a projection.- Each of these has an
IAsync...variant for reducers that need to do asynchronous work.
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 };
}
Persisting Projections¶
A projection is stored through an IProjectionDataStore<TProjection, TId>, the contract the projector uses to read and write your read model:
public interface IProjectionDataStore<TProjection, TId>
where TProjection : Projection<TId>
{
Task<TProjection?> GetByIdIfExisting(
TId id,
CancellationToken cancellationToken
);
Task Create(TProjection projection, CancellationToken cancellationToken);
Task Update(TProjection projection, CancellationToken cancellationToken);
Task DeleteById(TId id, CancellationToken cancellationToken);
}
You register a projection with AddProjection, naming the projection, its id type, the data store, and the interface your application queries it through:
.AddProjection<
Book,
Guid,
InMemoryProjectionRepository<Book, Guid>,
IProjectionRepository<Book, Guid>
>();
The built-in InMemoryProjectionRepository is convenient for getting started; for durable storage you provide your own data store, as shown in Persisting Projections.
How the Projector Stays Correct¶
You rarely write a projector yourself: AddProjection supplies one. It runs the matching reducer for each event, stamps the projection with the event's metadata, and skips events it has already applied by comparing event ids. This makes projecting idempotent, so reprocessing an event never corrupts a read model.
For side effects that are not a stored read model, such as forwarding events elsewhere, you can implement the IProjector interface directly and register it with AddProjector.
For More Information¶
- Projecting is the introductory walkthrough.
- Observing Events and Checkpoints explains how events reach your projectors.
- Publishing Events uses a custom projector to forward events.