Skip to content

Fix InMemory stream Version not advancing on append#275

Merged
MPapst merged 2 commits intoPapstIO:mainfrom
artnim:fix-inmemory-version-bump
May 6, 2026
Merged

Fix InMemory stream Version not advancing on append#275
MPapst merged 2 commits intoPapstIO:mainfrom
artnim:fix-inmemory-version-bump

Conversation

@artnim
Copy link
Copy Markdown
Contributor

@artnim artnim commented May 5, 2026

Summary

InMemoryEventStream.Version was a get-only property set in the constructor and never updated. AppendAsync writes each event with Version + 1, but since Version never moved off its initial value, every direct append produced an event at version 1 and the stream's reported tip stayed at 0.

This breaks any caller that relies on stream.Version reflecting the actual tip — most visibly the incremental aggregation overload AggregateAsync(stream, target, ct), which reads stream.Version as its targetVersion. With a stale tip, the loop's target.Version == targetVersion break can fire before any newly-appended events are applied, so incremental aggregation silently no-ops after the first append.

The Cosmos implementation already keeps Version in sync via the PatchItemAsync response on each append; the issue was specific to the in-memory store.

Fix

Derive Version from the events list, mirroring how LatestSnapshotVersion already works in the same class:

public ulong Version => _events.Count == 0 ? _initialVersion : _events.Max(e => e.Version);

The constructor-supplied version is kept as _initialVersion so an empty stream still reports the seed value. AppendAsync and AppendSnapshotAsync keep their Version = Version + 1 lines unchanged — they now read the live tip. InMemoryTransactionalBatch is unaffected.

Test plan

  • New tests in InMemoryEventStreamTests pin the contract:
    • AppendAsync produces events at versions 1, 2, 3, …
    • AppendAsync advances stream.Version
    • AppendSnapshotAsync advances stream.Version
    • ListAsync(2, …) returns the second event after two appends
  • All existing Papst test projects green (InMemory 7, EventStore 54, CodeGeneration 20, MongoDB 10, AzureCosmos 9)
  • Verified against AltensoSCM's integration suite — incremental aggregation now correctly applies post-first-append events

artnim added 2 commits May 5, 2026 14:37
The InMemoryEventStream's Version is set in the constructor and never
updated, so each AppendAsync writes events with the same (initial)
Version + 1 and the stream tip never advances. This breaks incremental
aggregation, which uses stream.Version as the target version.

Pin the contract:
- Direct AppendAsync produces events at versions 1, 2, 3, ...
- AppendAsync advances stream.Version
- AppendSnapshotAsync advances stream.Version
- ListAsync(2, ...) returns the second event after two appends

The Cosmos implementation already satisfies all of these.
Version was a get-only property set in the constructor and never
updated, so AppendAsync wrote each event with `Version + 1` against the
unchanging initial value — every direct append produced version 1, and
the stream tip stayed at 0. Incremental aggregation
(`AggregateAsync(stream, target)`) reads stream.Version as its target
version and skips empty event sets when stream.Version doesn't reflect
recent appends.

Derive Version from the events list (matching how LatestSnapshotVersion
works in the same class), falling back to the constructor-supplied
initial version when the stream is empty. Per-event Version assignment
in AppendAsync / AppendSnapshotAsync now reads the live tip, and the
transactional batch path stays self-contained.

The Cosmos implementation already advances Version via the patch
response on each append, so this only affected the in-memory store.
Copy link
Copy Markdown
Collaborator

@MPapst MPapst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@MPapst MPapst merged commit 6cd0c56 into PapstIO:main May 6, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants