Snapshots
A snapshot is an aggregate stored as a document and kept current from its stream.
opts.Projections.Snapshot<Order>(SnapshotLifecycle.Inline);
opts.Projections.Snapshot<Order>(SnapshotLifecycle.Async);
opts.Projections.Snapshot<Order>(SnapshotLifecycle.Live);| Lifecycle | When it is updated | Read with |
|---|---|---|
Inline | The same transaction as the append | LoadAsync<Order>(id) |
Async | A background daemon, shortly after | LoadAsync<Order>(id) |
Live | Never stored — folded on demand | AggregateStreamAsync<Order>(id) |
What Snapshot<T> does
It closes a single-stream projection over the aggregate's own identity type — the same rule live aggregation follows, for the same source-generator reason — and registers the document mapping, so the snapshot's table is created with the schema.
public class Order
{
public Guid Id { get; set; }
public bool Shipped { get; set; }
public void Apply(OrderShipped e) => Shipped = true;
}WARNING
The Apply dispatcher is source-generated with no runtime fallback, keyed on (TDoc, TId). The aggregate needs an identity member, and the generator runs in the assembly that defines it — so that assembly is the one that has to reference Fisher. The package carries JasperFx.Events.SourceGenerator inside it; there is no analyzer reference to add yourself.
Inline snapshots
Applied in SaveChangesAsync before the batch is taken, because applying a projection queues further operations — the snapshot writes — that have to commit alongside the events that caused them.
session.Events.Append(orderId, new OrderShipped(DateTimeOffset.UtcNow));
await session.SaveChangesAsync();
var order = await session.LoadAsync<Order>(orderId); // already currentSee Inline Projections.
Async snapshots
builder.Services.AddFisher(opts =>
{
opts.Connection("Data Source=app.db");
opts.Projections.Snapshot<Order>(SnapshotLifecycle.Async);
})
.ApplyAllDatabaseChangesOnStartup()
.AddAsyncDaemon(DaemonMode.Solo);Eventually consistent. To wait:
var orders = await session.Query<Order>()
.QueryForNonStaleData(TimeSpan.FromSeconds(5))
.ToListAsync();See the async daemon.
Live
Nothing is stored and no table is created. Every read folds the stream:
var order = await session.Events.AggregateStreamAsync<Order>(orderId);FetchForWriting reads whichever exists
var stream = await session.Events.FetchForWriting<Order>(orderId);It uses a stored snapshot if there is one, and live aggregation otherwise — so switching a lifecycle does not change your command handlers.
Compacting a long stream
If replay cost is the problem rather than read frequency, stream compacting replaces a stream's events with a single snapshot event, and every reader inherits the fast start with no code change — JasperFx's aggregator fast-forwards from it before folding.
WARNING
Compacting is one-way. A projection rebuilt afterwards rebuilds from the snapshot rather than from the history that produced it.
Snapshots and masking
WARNING
Event data masking does not reach a snapshot already written. The daemon's high-water mark is a sequence and masking does not move it, so a projection that already folded the unmasked body keeps what it derived — a snapshot holding protected information still holds it until that projection is rebuilt.
Marten behaves the same way, and it is why masking is a data-at-rest operation rather than a correction.

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