Skip to content
Deedboxlatest

Add a projection

This guide shows you how to build a read model from events with a projection.

Register one handler per event type in the constructor with On<T>. Deedbox skips a projection when an append holds none of its event types.

// EF Core flavour: your DbContext, enlisted in the append's transaction. Deedbox calls SaveChanges.
public sealed class CartSummaryProjection : Projection<ShopDb>
{
public CartSummaryProjection()
{
On<ItemAdded>(async (e, ctx) =>
{
var row = await ctx.Db.CartSummaries.FindAsync([ctx.StreamId], ctx.CancellationToken)
?? ctx.Db.CartSummaries.Add(new CartSummaryRow(ctx.StreamId)).Entity;
row.ItemCount += e.Qty;
});
On<CheckedOut>(async (_, ctx) =>
{
var row = await ctx.Db.CartSummaries.FindAsync([ctx.StreamId], ctx.CancellationToken);
row!.CheckedOut = true;
});
On<StreamDeleted>(async (_, ctx) =>
await ctx.Db.CartSummaries.Where(r => r.Id == ctx.StreamId).ExecuteDeleteAsync(ctx.CancellationToken));
}
// A rebuild calls ResetAsync, then replays every event.
protected override Task ResetAsync(WriteContext<ShopDb> context) =>
context.Db.CartSummaries.ExecuteDeleteAsync(context.CancellationToken);
}

Override ResetAsync to delete what the projection wrote. A rebuild needs it.

builder.Services.AddDeedbox(es => es
.UsePostgres(connStr)
.Stream<Cart>(s => s.Events<ItemAdded, CheckedOut>())
.Projection<CartSummaryProjection>("cart_summary", Run.Inline)
.Projection<CartTotals>("cart_totals", Run.Async)
.Subscription<SendReceipt>("receipt_email"));
  • The name, such as cart_summary, keys the projection’s checkpoint. Renaming the class keeps its progress. Renaming the projection starts it again from the first event.
  • Run.Inline applies the projection in the append’s transaction. The read model is never behind, and a failing handler fails the append.
  • Run.Async applies the projection in the background runner. The checkpoint commits in the same transaction as the projection’s writes, so each event applies exactly once.

A projection has one run mode. Registering one class twice fails at start-up, because two registrations apply each event twice.

A batch projection receives each batch of its events in one call. It runs async only.

// A batch projection receives each batch of its events in one call, for bulk writes. It runs async only.
public sealed class CartArchive : BatchProjection
{
public CartArchive() => Handles<CheckedOut>();
protected override Task ApplyAsync(IReadOnlyList<EventEnvelope> events, WriteContext context)
{
// One bulk insert for the whole batch, through context.Connection and context.Transaction.
return Task.CompletedTask;
}
}
Property Inline Async
EventId, StreamId, Version, Metadata yes yes
GlobalPosition null yes
Services the append’s scope a scope per batch

Handle the built-in events StreamDeleted and SubjectErased to remove deleted and erased data from your read model.