ProjectLatest — Include Pending Events
ProjectLatest<T>() returns the projected state of an aggregate including any events that have been appended in the current session but not yet committed. This eliminates the need for a forced SaveChangesAsync() + FetchLatest() round-trip when you need the projected result immediately after appending events.
Motivation
A common pattern in command handlers looks like this:
// Today's pattern: forced flush + re-read
session.Events.StartStream<Report>(id, new ReportCreated("Q1"));
await session.SaveChangesAsync(ct); // forced flush
var report = await session.Events.FetchLatest<Report>(id, ct); // re-read
return report;With ProjectLatest, this becomes:
// Better: project locally including pending events
session.Events.StartStream<Report>(id, new ReportCreated("Q1"));
var report = await session.Events.ProjectLatest<Report>(id, ct);
// SaveChangesAsync happens later (e.g., Wolverine AutoApplyTransactions)
return report;API
// On IDocumentSession.Events (IEventOperations)
ValueTask<T?> ProjectLatest<T>(Guid id, CancellationToken cancellation = default);
ValueTask<T?> ProjectLatest<T>(string key, CancellationToken cancellation = default);Behavior
- Fetches the current committed aggregate state from the database via
FetchLatest<T>() - Finds any pending (uncommitted) events for that stream in the current session
- Applies the pending events on top using the aggregate's Apply/Create methods
- Returns the result
When No Pending Events Exist
If there are no uncommitted events for the given stream in the session, ProjectLatest behaves identically to FetchLatest — it returns the current committed state.
Example
[Fact]
public async Task includes_pending_events_from_start_stream()
{
var streamId = Guid.NewGuid();
await using var session = theStore.LightweightSession();
// Append events without committing
session.Events.StartStream(streamId,
new ReportCreated("Q1 Report"),
new SectionAdded("Revenue"),
new SectionAdded("Costs")
);
// ProjectLatest includes the pending events above
var report = await session.Events.ProjectLatest<Report>(streamId);
report.ShouldNotBeNull();
report.Title.ShouldBe("Q1 Report");
report.SectionCount.ShouldBe(2);
// SaveChangesAsync can happen later
await session.SaveChangesAsync();
}Merging Committed and Pending Events
ProjectLatest also works when the stream has previously committed events and new events are appended in the current session:
[Fact]
public async Task includes_pending_events_after_committed_events()
{
var streamId = Guid.NewGuid();
// First, commit some events
await using (var session = theStore.LightweightSession())
{
session.Events.StartStream(streamId,
new ReportCreated("Q1 Report"),
new SectionAdded("Revenue")
);
await session.SaveChangesAsync();
}
// In a new session, append more events without committing
await using (var session = theStore.LightweightSession())
{
session.Events.Append(streamId,
new SectionAdded("Costs"),
new SectionAdded("Outlook"),
new ReportPublished()
);
// ProjectLatest merges the committed state with pending events
var report = await session.Events.ProjectLatest<Report>(streamId);
report.ShouldNotBeNull();
report.Title.ShouldBe("Q1 Report");
report.SectionCount.ShouldBe(3); // 1 committed + 2 pending
report.IsPublished.ShouldBeTrue(); // from pending ReportPublished
}
}String-Keyed Streams
ProjectLatest supports string-keyed streams as well:
// For stores configured with StreamIdentity.AsString
session.Events.StartStream("report-123",
new ReportCreated("Annual Report"),
new SectionAdded("Overview")
);
var report = await session.Events.ProjectLatest<Report>("report-123");Limitations
- Read-only sessions:
ProjectLatestis only available onIDocumentSession.Events(notIQuerySession.Events) because it needs access to the session's pending work tracker.

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