Fix #4481: FetchForWriting without appending events shouldn't block unrelated inserts#4489
Merged
Merged
Conversation
When an inline aggregate's stream was fetched via FetchForWriting
and no events were subsequently appended, an unrelated document
insert on the same session would fail with
JasperFx.ConcurrencyException on SaveChangesAsync — the empty stream
still flowed through the inline projection's Apply step and queued
a snapshot-update operation against the unchanged aggregate version,
which then failed the optimistic-concurrency check.
The symptom reproduced for projections that override Evolve directly
but not for the conventional Apply/Create method shape. Root cause
is upstream in JasperFx.Events 1.x:
JasperFxAggregationProjectionBase.AppliesTo(eventTypes)
// Have to do this because you don't know if any events catch
if (AllEventTypes.Length == 0) return true;
return eventTypes.Intersect(AllEventTypes).Any() || ...;
For Apply/Create projections AllEventTypes is populated from the
discovered handler methods, so an empty stream's empty eventTypes
collection cleanly evaluates to false and the stream is screened out.
For Evolve-based projections AllEventTypes is empty, the early-out
fires `return true`, and the empty stream slips through.
Fix lives in Marten's two inline-appender entry points
(RichEventAppender, QuickEventAppender): only pass streams that
actually have events to the inline-projection ApplyAsync calls.
Mirrors the same `x.Events.Any()` filter that RichEventAppender's
streamActions branch already uses on the storage side.
Regression test added in
src/EventSourcingTests/FetchForWriting/
fetch_for_writing_and_projection_metadata_for_inline_projections.cs
that fails without the fix (the exact symptom from the issue:
ConcurrencyException on the unchanged VersionedGuy) and passes with it.
Closes #4481.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 19, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #4481.
When an inline aggregate's stream is fetched via
FetchForWriting<TAggregate>and no events are subsequently appended, an unrelated document insert on the same session was failing withJasperFx.ConcurrencyExceptiononSaveChangesAsync— the empty stream still flowed through the inline projection's apply step and queued a snapshot-update operation against the unchanged aggregate version, tripping the optimistic-concurrency check.The symptom reproduced for projections that override
Evolvedirectly but not for the conventionalApply/Createmethod shape.Root cause
Upstream in JasperFx.Events 1.x:
For
Apply/CreateprojectionsAllEventTypesis populated from the discovered handler methods, so an empty stream's emptyeventTypescollection cleanly evaluates tofalseand the stream is screened out by the early intersection check inJasperFxSingleStreamProjectionBase.ApplyAsync. ForEvolve-based projectionsAllEventTypesis empty, theLength == 0early-out firesreturn true, and the empty stream slips through — then the projection writes a snapshot with the old expected version and a concurrency check fires when any other operation commits.Fix
Lives in Marten's two inline-appender entry points (
RichEventAppender,QuickEventAppender): only pass streams that actually have events to the inline-projectionApplyAsynccalls. Mirrors the samex.Events.Any()filter thatRichEventAppender'sstreamActionsbranch already uses on the storage side. Marten-side fix avoids needing to ship a new JasperFx.Events 1.x release.Test plan
fetch_for_writing_without_appending_does_not_block_unrelated_insertsreproduces the exactConcurrencyExceptionfrom the issue without the fix.FullyQualifiedName~FetchForWritingtests pass on net10.0.EventSourcingTestsrun: 5 pre-existing failures on8.0baseline confirmed reproducible without this PR (verified by reverting and re-running the same test set) — no regressions introduced.A companion PR cherry-picks the fix to
master.🤖 Generated with Claude Code