ProjectLatest — Include Pending Events
ProjectLatest folds the session's uncommitted events on top of the committed state, so a command handler can see the effect of what it has just appended without saving first.
await using var session = store.LightweightSession();
session.Events.Append(orderId, new OrderLineAdded("SKU-1", 2));
session.Events.Append(orderId, new OrderLineAdded("SKU-2", 1));
// Includes both pending events
var order = await session.Events.ProjectLatest<Order>(orderId);
if (order!.LineCount > 10)
{
session.Events.Append(orderId, new OrderFlaggedForReview());
}
await session.SaveChangesAsync();FetchLatest vs ProjectLatest
| Reads | |
|---|---|
FetchLatest<T>(id) | The committed state — a stored snapshot if there is one, otherwise a fold |
ProjectLatest<T>(id) | The committed state plus this session's pending events |
When it is the right tool
- A handler appending several events that each depend on the state after the previous one.
- A validation that must consider what this unit of work is about to write.
- Returning the resulting state from a command handler without a second round of work.
When FetchForWriting is better
For the ordinary fetch-decide-append shape, FetchForWriting is the tool — it carries the optimistic concurrency guard, and appending through the stream it hands back keeps the version bookkeeping correct:
var stream = await session.Events.FetchForWriting<Order>(orderId);
stream.AppendOne(new OrderShipped(DateTimeOffset.UtcNow));
await session.SaveChangesAsync();ProjectLatest is a read. It does not open a boundary and does not guard anything.
Pending events are tracked per stream
Fisher tracks them in a dictionary keyed by stream identity, so several Append calls for one stream in one session accumulate onto the same action rather than replacing each other.
WARNING
That is also why FetchForWriting reuses an already-tracked action rather than constructing a fresh one — replacing the entry would silently drop events an earlier Append had queued for the same stream.
Tenancy
A tenant scope keeps its own pending-stream map, because the map is keyed by stream id and two tenants may reuse one. So session.ForTenant("globex").Events.ProjectLatest<Order>(id) sees globex's pending events, not acme's.

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