ProjectLatest — Include Pending Events 8.x
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 (IEventStoreOperations)
ValueTask<T?> ProjectLatest<T>(Guid id, CancellationToken cancellation = default);
ValueTask<T?> ProjectLatest<T>(string id, CancellationToken cancellation = default);Behavior by Projection Lifecycle
Live Projections
- Fetches all committed events from the database and builds the aggregate
- Finds any pending (uncommitted) events for that stream in the current session
- Applies the pending events on top of the committed state
- Returns the result (no storage — live projections are ephemeral)
Inline Projections
- Loads the pre-projected document from the database
- Finds any pending events for that stream in the current session
- Applies the pending events on top using the aggregate's Apply/Create methods
- Stores the updated document in the session so it will be persisted on the next
SaveChangesAsync() - Returns the result
Async Projections
Same behavior as inline: loads the stored document, applies pending events, stores the updated document in the session.
Example
public record ReportCreated(string Title);
public record SectionAdded(string SectionName);
public record ReportPublished;
public class Report
{
public Guid Id { get; set; }
public string Title { get; set; } = "";
public int SectionCount { get; set; }
public bool IsPublished { get; set; }
public static Report Create(ReportCreated e) => new Report { Title = e.Title };
public void Apply(SectionAdded e) => SectionCount++;
public void Apply(ReportPublished e) => IsPublished = true;
}
// In a command handler:
await using var session = store.LightweightSession();
session.Events.StartStream(streamId,
new ReportCreated("Q1 Report"),
new SectionAdded("Revenue"),
new SectionAdded("Costs")
);
// Get the projected state WITHOUT saving first
var report = await session.Events.ProjectLatest<Report>(streamId);
// report.Title == "Q1 Report"
// report.SectionCount == 2
// report.IsPublished == false
// Save happens later — the inline document is already queued for storage
await session.SaveChangesAsync();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.
Limitations
- Natural key projections:
ProjectLatestwith a natural key ID falls back toFetchLatestbecause the natural key mapping may not exist yet for uncommitted streams. - Read-only sessions:
ProjectLatestis only available onIDocumentSession.Events(notIQuerySession.Events) because it may store the updated document for inline projections.

