Skip to content

Fix #4481: FetchForWriting without appending events shouldn't block unrelated inserts (master)#4490

Merged
jeremydmiller merged 1 commit into
masterfrom
fix/4481-concurrency-fetch-without-append-master
May 19, 2026
Merged

Fix #4481: FetchForWriting without appending events shouldn't block unrelated inserts (master)#4490
jeremydmiller merged 1 commit into
masterfrom
fix/4481-concurrency-fetch-without-append-master

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Summary

Closes #4481. Master companion to #4489 (the 8.0 PR).

When an inline aggregate's stream was fetched via FetchForWriting<TAggregate> and no events were subsequently appended, an unrelated document insert on the same session was failing 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, tripping the optimistic-concurrency check.

Symptom reproduced only when the projection overrode Evolve directly; the conventional Apply/Create shape wasn't affected.

Root cause

Upstream in JasperFxAggregationProjectionBase.AppliesTo (jasperfx repo, unchanged across the 1.x → 2.x rename):

public bool AppliesTo(IEnumerable<Type> 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 by the intersection check in JasperFxSingleStreamProjectionBase.ApplyAsync. For Evolve-based projections AllEventTypes is empty, the Length == 0 early-out fires return true, and the empty stream slips through — the projection then writes a snapshot with the old expected version and a concurrency check fires when any other operation commits.

Fix (cherry-picked from #4489, with master-side reconciliation)

Same approach as the 8.0 PR — filter empty streams at the Marten-side appender entry points. Master diverges from 8.0 on the IInlineProjection.ApplyAsync contract:

RichEventAppender.ProcessEventsAsync and QuickEventAppender.ProcessEventsAsync both updated.

Test plan

  • fetch_for_writing_without_appending_does_not_block_unrelated_inserts test cherry-picked from the 8.0 PR; passes on master with the fix
  • All 212 FullyQualifiedName~FetchForWriting tests pass on net10.0 with the fix
  • No regressions vs. master baseline

Companion PR on 8.0: #4489.

🤖 Generated with Claude Code

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) <[email protected]>
@jeremydmiller jeremydmiller merged commit 528cbf2 into master May 19, 2026
6 of 7 checks passed
@jeremydmiller jeremydmiller deleted the fix/4481-concurrency-fetch-without-append-master branch May 19, 2026 13:01
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.

JasperFx.ConcurrencyException when trying to insert a document after FetchForWriting without appending events

1 participant