Marten 9.25.0
Fixes
The window-step event loader skipped events and reported the projection as caught up (#5239, #5242)
If you run async projections with an event type filter against a large store, read this one.
Marten's adaptive event loader falls back through three strategies when a fetch times out: Normal → SkipAhead → WindowStep. The final one scans the sequence in fixed 10,000-wide windows, and its SELECT is bounded by the window — but it computed the page ceiling against the full high-water mark. Since the daemon writes that ceiling as durable projection progress (EventRange(floor, page.Ceiling) → last_seq_id), every matching event between the window ceiling and the high-water mark was skipped and would never be loaded.
This was the ordinary path, not an edge case. The window is 10,000 sequence numbers wide, the batch size is 500, and the strategy exists precisely because matching events are sparse — so "returned fewer than BatchSize events", the branch that took the high-water mark, was the expected outcome. It is reached only after two consecutive statement timeouts, i.e. on exactly the large, busy stores the strategy was added for.
The observable result: a projection logged Falling back to WindowStep at Warning, then advanced its progression to the high-water mark having applied only the handful of events in the first window. No exception, no dead-letter row, the shard reporting itself caught up, and a read model permanently missing most of its data.
Two narrower skips in the same loop are fixed alongside it: skipped unknown/undeserializable events were never counted toward the batch (so a window filled partly by skips was misclassified), and the loop advanced on events added to the page rather than rows read, so a window whose rows were all skipped stepped over any matching events the LIMIT had truncated.
If a projection has already been affected, this fix does not backfill it — rebuild the affected projections.
Reported with a traced mechanism and an executed failing test by @arnelirobles, from barakoCMS.
Store-level CompactStreamAsync threw, and then silently did nothing (#5240)
IDocumentStore's store-level CompactStreamAsync overloads resolved their target method by reflection against Marten.Events.IEventStoreOperations. #5153 lifted both overloads onto JasperFx.Events.IEventStoreOperations, and because Type.GetMethod does not walk base interfaces, the lookup returned null and the call threw a NullReferenceException.
Repairing only the lookup would not have been enough: the compaction request merely queues its operations, so the store-level overload — which owns its own session — was disposing without committing. It would have been a silent no-op. It now saves, and the CancellationToken it accepts is no longer discarded.
The surface had no test anywhere in the tree, which is how an interface move broke it with no compile error and no failing test. Two store-level compaction tests now cover it.
Reported by @arnelirobles.
CompactStreamAsync now rejects a stream identity mismatch (#5244, #5245)
Compaction branches on the store's configured StreamIdentity rather than on which overload you called, and never asserted the two agreed. Calling CompactStreamAsync<T>(Guid) against a string-identified store therefore matched no stream and returned successfully having compacted nothing — no exception, and no way to tell a no-op from a completed compaction. The other three combinations failed with messages that named nothing actionable (Nullable object must have a value, a stream not found misdiagnosis, or a raw Npgsql null-parameter error).
All four now throw the message Marten already uses for this class of mistake: This Marten event store is configured to identify streams with Guids (or …with strings).
This is a behaviour change for anyone whose code was reaching the silent no-op. That call was never doing anything, so the throw is surfacing a pre-existing bug in the caller rather than introducing one.
Dependencies
JasperFx 2.49.0 (#5241, #5243)
Raises the JasperFx floor from 2.48.0 to 2.49.0, which adds LoadAsync<T>(object id) to JasperFx.Events.Documents.IDocumentReadOperations so store-agnostic code can load a document keyed by a strong-typed identifier.
No Marten API change: IQuerySession already declared this overload and QuerySession already implemented it, dispatching on the runtime type of the id — Marten is the store the new member was modeled on. Marten code is unaffected; the bump matters only if you also consume the JasperFx document abstraction directly.