Projections Overview
A projection builds a read model from events. Fisher supports every shape the Critter Stack defines, across every lifecycle — and because the abstractions are JasperFx's, a projection written for Marten or Polecat runs here unaltered.
The shapes
| Shape | Builds |
|---|---|
| Single stream | One document per stream |
| Multi stream | One document from events across many streams |
| Event projection | Arbitrary writes per event |
| Flat table | Rows in a plain relational table |
| Composite | Several projections as ordered stages under one shard |
The lifecycles
| Lifecycle | When | Consistency |
|---|---|---|
| Live | Folded on demand | Always current; nothing stored |
| Inline | The append's own transaction | Strong |
| Async | A background daemon | Eventual |
Registering
opts.Projections.Snapshot<Order>(SnapshotLifecycle.Inline);
opts.Projections.Add<OrdersByCustomer>(ProjectionLifecycle.Async);
opts.Projections.CompositeProjectionFor("reporting", c => { … });
opts.Projections.Subscribe(new NotifyOnShipment());Add<T>(lifecycle) constructs the projection for you and is the spelling Marten and Polecat use, so a registration block reads identically against all three stores. Pass an instance instead when the projection needs constructor arguments:
opts.Projections.Add(new OrdersByCustomer(connectionString), ProjectionLifecycle.Async);Both forms take an optional AsyncOptions lambda for rebuild and batching behaviour:
opts.Projections.Add<OrdersByCustomer>(ProjectionLifecycle.Async, o => o.BatchSize = 1000);Conventional methods are source-generated
public class Order
{
public Guid Id { get; set; }
public bool Shipped { get; set; }
public static Order Create(OrderPlaced e) => new() { … };
public void Apply(OrderShipped e) => Shipped = true;
public bool ShouldDelete(OrderCancelled e) => true;
}WARNING
The dispatcher is emitted by JasperFx.Events.SourceGenerator, and there is no runtime fallback.
- The Fisher package carries the generator, so referencing Fisher is enough — no analyzer reference of your own. The generator runs in the assembly that defines the aggregate or projection, so that assembly is the one that has to reference Fisher.
- A conventional-method projection class must be declared
partial. - The aggregate needs an identity member, because the generator keys the dispatcher on
(TDoc, TId). TIdis the aggregate's own id type — a strong-typed id is a wrapper struct, and the generated dispatcher is keyed on the wrapper.
Rebuilds
var daemon = await store.BuildProjectionDaemonAsync();
await daemon.RebuildProjectionAsync("Order", CancellationToken.None);A rebuild tears down the projection's existing state and replays from the beginning of the event store.
DANGER
Teardown is where projection bugs hide. A replay rewrites every row it can still produce, so a surviving row is invisible except where the replay cannot recreate it — a row whose backing events are gone, archived or compacted.
That means an ordinary rebuild test passes even when teardown is broken. Every place Fisher's teardown had to learn something — flat tables, composite members, EF Core-backed documents — is pinned with a row the replay cannot recreate, and any projection you write with unusual storage should be too.
Both halves of teardown — the progression rows and the documents — run in one transaction, because clearing progress without clearing documents replays a projection on top of rows it already wrote.
TIP
Teardown checks for the table in C#, not in SQL. SQLite resolves a table name when it prepares a statement, so a where exists (select 1 from sqlite_master …) guard on the delete fails before the guard could run. Names come back from sqlite_master first, and missing tables are skipped.
Errors
opts.Projections.Errors.SkipApplyErrors = true;A skipped poison event is quarantined into fi_dead_letters rather than stopping its shard.
Side effects
A projection can publish messages through an outbox seam — and the default outbox drops every message, which is the end state rather than a placeholder. Fisher ships no delivery mechanism.
Testing
await store.Advanced.EventProjectionScenarioAsync(scenario =>
{
scenario.Append(streamId, new OrderPlaced(…));
scenario.DocumentShouldExist<Order>(streamId);
});See Integration Testing.

JasperFx provides formal support for Fisher and other Critter Stack libraries. Please check our