Skip to content

Don't start an implicit EF Core transaction on ScheduleAsync for a Wolverine-mapped DbContext (closes #3121)#3122

Merged
jeremydmiller merged 2 commits into
mainfrom
fix-3121-efcore-schedule-implicit-transaction
Jun 16, 2026
Merged

Don't start an implicit EF Core transaction on ScheduleAsync for a Wolverine-mapped DbContext (closes #3121)#3122
jeremydmiller merged 2 commits into
mainfrom
fix-3121-efcore-schedule-implicit-transaction

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Problem

For a Wolverine-mapped DbContext, IDbContextOutbox.ScheduleAsync starts an explicit EF Core transaction the caller never asked for, while PublishAsync does not:

outbox.Enroll(db);
await outbox.PublishAsync(new TestMsg());   // no implicit EF transaction
await outbox.ScheduleAsync(new TestMsg(), TimeSpan.FromMinutes(1));  // starts one ❌

Root cause

ScheduleAsync produces an envelope with EnvelopeStatus.Scheduled, which IEnvelopeTransaction.PersistAsync routes to PersistIncomingAsync (IEnvelopeTransaction.cs:33-34). EfCoreEnvelopeTransaction.PersistIncomingAsync began an explicit transaction unconditionally, before checking IsWolverineEnabled(). But for a mapped DbContext the envelope is simply tracked via DbContext.Add(new IncomingMessage(...)) and flushed inside SaveChangesAsync's own implicit transaction — the explicit one is pure overhead.

This is an oversight from a prior refactor, not a deliberate tradeoff. Commit 5af99ae93 ("Outbox behavior alignment with EF Core transaction model") moved the unconditional BeginTransactionAsync out of both PersistOutgoingAsync overloads into their raw (non-mapped) branch — but left PersistIncomingAsync untouched. That's the entire asymmetry the reporter sees.

Fix

Apply the same alignment to PersistIncomingAsync: only begin an explicit transaction in the raw / non-IsWolverineEnabled branch (where an ADO command must share the caller's transaction). The mapped branch just tracks the entity, exactly like PersistOutgoingAsync. One method; no API change.

The two other callers are unaffected:

  • TryMakeEagerIdempotencyCheckAsync begins its own explicit transaction (EfCoreEnvelopeTransaction.cs:140-143).
  • CommitAsync's handled-idempotency insert already relied on a later SaveChanges to flush the tracked Add, not on this forced transaction.

Tests

  • New regression test persisting_against_mapped_dbcontext_does_not_start_an_explicit_transaction — asserts that persisting a scheduled/incoming and an outgoing envelope against a Wolverine-mapped DbContext leaves Database.CurrentTransaction null.
  • The full end_to_end_efcore_persistence class + eager-idempotency tests (23 total) remain green, confirming the raw-mode persistence and idempotency paths are unaffected.

🤖 Generated with Claude Code

jeremydmiller and others added 2 commits June 16, 2026 13:00
…/incoming envelopes against a Wolverine-mapped DbContext (closes #3121)

EfCoreEnvelopeTransaction.PersistIncomingAsync began an explicit transaction
unconditionally, before checking IsWolverineEnabled(). For a Wolverine-mapped
DbContext the envelope is just tracked via DbContext.Add and flushed inside
SaveChangesAsync's own implicit transaction, so the explicit transaction was
unnecessary — and it made IDbContextOutbox.ScheduleAsync start a transaction the
caller never asked for, while PublishAsync (PersistOutgoingAsync) did not.

This asymmetry was an oversight: commit 5af99ae ("Outbox behavior alignment
with EF Core transaction model") moved the unconditional BeginTransactionAsync out
of both PersistOutgoingAsync overloads into their raw (non-mapped) branch, but left
PersistIncomingAsync untouched. This applies the same alignment: the explicit
transaction is now only started in the raw branch, where an ADO command must share
the caller's transaction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eager/lazy transaction (#3121)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jeremydmiller jeremydmiller merged commit 5403532 into main Jun 16, 2026
24 checks passed
This was referenced Jun 16, 2026
This was referenced Jun 23, 2026
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.

1 participant