Skip to content

Save the scraped domain-event envelopes before committing the tenant transaction (GH-3744) - #3754

Merged
jeremydmiller merged 2 commits into
mainfrom
gh/3744-efcore-scraped-envelopes
Aug 1, 2026
Merged

Save the scraped domain-event envelopes before committing the tenant transaction (GH-3744)#3754
jeremydmiller merged 2 commits into
mainfrom
gh/3744-efcore-scraped-envelopes

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Fixes #3744.

The bug

Under managed database-per-tenant multi-tenancy, a domain event scraped by
PublishDomainEventsFromEntityFrameworkCore is published but its durable envelope is never persisted.
The aggregate commits; the envelope silently vanishes.

Root cause

EFCorePersistenceFrameProvider adds a SaveChangesAsync postprocessor, and then
applyEagerCommitOrLightweightFlush appends the commit frame after it. So
CommitTenantedDbContextTransaction runs the scrapers and commits with no second save.

On an enrolled (Wolverine-mapped) DbContext a durable route persists its envelope by adding an entity
to the change tracker (EfCoreEnvelopeTransaction.PersistOutgoingAsync / PersistIncomingAsync), so
everything the scrape produced was still sitting in the tracker when the transaction committed.

The failure is quiet in the worst way: the domain event is still published in memory, so
tracked-session assertions pass while the durable row is missing. That includes the existing
domain_events_scraping_with_managed_multi_tenancy test, which asserts on tracked.Sent — it passes
both before and after this change.

The fix

One SaveChangesAsync between the scrape loop and the commit, so the envelopes land inside the
transaction. A no-op when the scrape produced nothing.

Verification

Run with the reporter's own repro from the issue (managed tenancy + durable local queue + Testcontainers
Postgres), referencing this branch's source directly rather than a packed feed:

Wolverine build Result
published 6.24.0 Aggregate rows: 1 / Durable envelope rows: 0BUG REPRODUCED
this branch Aggregate rows: 1 / Durable envelope rows: 1 → clean exit
this branch, frame reverted back to 0BUG REPRODUCED

The passing run also emits the INSERT INTO wolverine.wolverine_incoming_envelopes that is entirely
absent from the baseline output.

Regression check — full EfCoreTests.MultiTenancy: 78 failed / 115 passed with this change, and
78 failed / 115 passed on unmodified main
. Identical; those failures are pre-existing SQL Server
connect errors in my environment, unrelated to this change.

What this PR deliberately does NOT include

No automated regression test. Two attempts built on EfCoreTests.MultiTenancy passed with and
without the fix, and I discarded them rather than commit false assurance. The reasons are worth knowing
for whoever writes the real one:

  • ItemsDbContext is not enrolled by default, so it takes the raw-ADO branch of
    PersistOutgoingAsync — which this change does not touch. .Enroll<ItemsDbContext>() is required to
    exercise the change-tracker path at all.
  • The main message store and the tenant databases use different schemas, so the obvious
    "count rows in wolverine.wolverine_outgoing_envelopes" assertion looks at the wrong table and reads
    a missing table as zero.

A valid test needs an enrolled DbContext on the managed-tenancy path, asserting against the schema the
enrolled context actually maps to. Happy to follow up with that if you'd rather it land together.

EfCoreEnvelopeTransaction.CommitAsync is left alone. It has the same scrape-then-commit shape on
the single-DbContext path and looks like a sibling defect, but the repro does not exercise it and this
fix does not need it. I had that change in and backed it out once I confirmed the codegen frame alone is
sufficient — adding a SaveChangesAsync there would flush any pending tracked change on a widely-used
path, and I did not want to ship that unverified. Worth its own issue.

🤖 Generated with Claude Code

jeremydmiller and others added 2 commits July 31, 2026 14:13
…transaction (GH-3744)

Under managed database-per-tenant multi-tenancy, a domain event scraped
by PublishDomainEventsFromEntityFrameworkCore was published but its
durable envelope was never persisted: the aggregate committed and the
envelope silently vanished.

EFCorePersistenceFrameProvider adds a SaveChangesAsync postprocessor and
then appends the commit frame AFTER it, so
CommitTenantedDbContextTransaction ran the scrapers and committed with no
second save. On an enrolled (Wolverine-mapped) DbContext a durable route
persists its envelope by adding an entity to the change tracker
(EfCoreEnvelopeTransaction.PersistOutgoingAsync / PersistIncomingAsync),
so everything the scrape produced was still sitting in the tracker when
the transaction committed.

The failure is quiet in the worst way: the domain event is still
PUBLISHED in memory, so tracked-session assertions -- including the
existing domain_events_scraping_with_managed_multi_tenancy test -- pass
while the durable row is missing.

Verified with the reporter's own repro from the issue (managed tenancy +
durable local queue + testcontainers Postgres):

  before: Aggregate rows: 1, Durable envelope rows: 0  ("BUG REPRODUCED")
  after:  Aggregate rows: 1, Durable envelope rows: 1

and re-confirmed by reverting this frame alone, which puts the failure
straight back.

No automated regression test yet. Two attempts built on
EfCoreTests.MultiTenancy passed with AND without the fix and were
discarded rather than committed as false assurance -- ItemsDbContext is
not enrolled by default, so it takes the raw-ADO branch of
PersistOutgoingAsync that this change does not touch, and the main store
and tenant databases use different schemas, which makes the obvious row
assertion look at the wrong table. A valid test needs an enrolled
DbContext on the managed-tenancy path.

EfCoreEnvelopeTransaction.CommitAsync has the same scrape-then-commit
shape on the single-DbContext path and is a suspected sibling defect, but
the repro does not exercise it and this change is not needed to fix it,
so it is deliberately left alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jeremydmiller
jeremydmiller merged commit a16cb48 into main Aug 1, 2026
59 of 60 checks 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.

Managed EF Core multi-tenancy drops durable envelopes scraped after the final SaveChanges

1 participant