Skip to content

fix(efcore): enlist HTTP endpoint outbox in Lightweight mode so cascades flush after commit (GH-3291)#3298

Merged
jeremydmiller merged 1 commit into
mainfrom
fix/efcore-lightweight-http-outbox-3291
Jul 5, 2026
Merged

fix(efcore): enlist HTTP endpoint outbox in Lightweight mode so cascades flush after commit (GH-3291)#3298
jeremydmiller merged 1 commit into
mainfrom
fix/efcore-lightweight-http-outbox-3291

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Fixes #3291.

Problem

In TransactionMiddlewareMode.Lightweight, cascading messages returned from a Wolverine.Http endpoint (the (IResult, TMessage) tuple-return style) are sent to their destination immediately, before SaveChangesAsync commits the endpoint's database work — silently dropping the transactional-outbox guarantee the HTTP docs advertise. A downstream handler can observe the database before the row that triggered the message exists.

The determining factor is whether the endpoint's MessageContext is enlisted in the outbox (Transaction != null):

  • A message handler has an incoming envelope, so MessageContext.ReadEnvelope sets Transaction = this — its cascades buffer and flush after commit in both modes.
  • An HTTP endpoint has no incoming envelope. The only thing that enlists its context is the EnrollDbContextInTransaction middleware, which is added only in Eager mode. In Lightweight mode Transaction stays null, so MessageBus.PersistOrSendAsync takes the StoreAndForwardAsync() (send-now) branch. The standalone FlushOutgoingMessages postprocessor then has nothing left to flush.

So the bug is specific to HTTP endpoints in Lightweight mode; message handlers are unaffected.

Fix (restricted to HttpChain)

EFCorePersistenceFrameProvider.ApplyTransactionSupport now, for an HTTP chain in Lightweight mode that requires the outbox, inserts a new EnlistDbContextInOutbox middleware that enrolls the DbContext + IMessageContext in the outbox without an explicit BeginTransactionAsync:

  • EfCoreEnvelopeTransaction.PersistOutgoingAsync writes the outgoing envelope via DbContext.Add(new OutgoingMessage(...)), which SaveChangesAsync commits atomically with the endpoint's entity — so the cascade now buffers and flushes after the commit (same commit-then-flush ordering as Eager).
  • Skipping BeginTransactionAsync keeps this compatible with EF Core's retrying execution strategy (EnableRetryOnFailure), which forbids user-initiated transactions and is the most common reason apps end up in Lightweight mode in the first place.
  • EnlistDbContextInOutbox implements IFlushesMessages, so HttpChain no longer adds a standalone (pre-commit) FlushOutgoingMessages; the existing CommitEfCoreEnvelopeTransaction postprocessor does the post-commit flush — reusing the Eager path's proven placement (TrackActivity with Transactional Outbox doesn't work with Wolverine.Http #2917) with no double-flush.

The HTTP chain is detected via chain.Scoping == MiddlewareScoping.HttpEndpoints (no Wolverine.Http type reference from the EF Core assembly). The multi-tenant path is intentionally left unchanged.

Tests

Bug_3291_lightweight_http_cascade_flushes_before_commit (Wolverine.Http.Tests) boots a Lightweight-mode EF Core host with a cascading HTTP endpoint and asserts at the codegen surface (the runtime symptom races the durability agent, per the sibling Eager reproducer):

  • no standalone FlushOutgoingMessages postprocessor,
  • the generated code enrolls via EnlistInOutboxAsync,
  • ordering is enroll → SaveChangesAsyncCommitAsync (flush after commit).

Verified failing before the fix and passing after.

Not covered here

Multi-tenant EF Core + Lightweight HTTP endpoints (separate, pre-existing scenario). The ai-skills wolverine-handlers-efcore guidance is being updated separately (JasperFx/ai-skills#85).

…des flush after commit (GH-3291)

A Wolverine.Http endpoint has no incoming envelope, so - unlike a message handler,
whose MessageContext is enlisted by ReadEnvelope at runtime - its MessageContext.Transaction
stays null. In TransactionMiddlewareMode.Lightweight the EF Core transaction middleware never
enrolls the endpoint's DbContext in the outbox, so cascaded messages take the send-now branch
and are dispatched BEFORE the SaveChangesAsync postprocessor commits, silently dropping the
transactional-outbox guarantee for HTTP endpoints (message handlers are unaffected).

EFCorePersistenceFrameProvider.ApplyTransactionSupport now, for an HTTP chain in Lightweight
mode that requires the outbox, inserts a new EnlistDbContextInOutbox middleware that enrolls the
DbContext + IMessageContext WITHOUT an explicit BeginTransactionAsync. The outgoing envelope is
written via DbContext.Add and committed atomically by SaveChangesAsync, then flushed by the
existing CommitEfCoreEnvelopeTransaction postprocessor - same commit-then-flush ordering as Eager
(GH-2917), minus the explicit transaction. Skipping BeginTransactionAsync keeps this compatible
with EF Core's EnableRetryOnFailure. EnlistDbContextInOutbox implements IFlushesMessages so
HttpChain does not also add a standalone pre-commit FlushOutgoingMessages (no double-flush).

Restricted to HttpChain via chain.Scoping == MiddlewareScoping.HttpEndpoints (no Wolverine.Http
reference from the EF Core assembly). Multi-tenant path left unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jeremydmiller
jeremydmiller merged commit f204932 into main Jul 5, 2026
26 checks passed
This was referenced Jul 14, 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.

EF Core Lightweight transaction mode: Wolverine.Http endpoint cascading messages are relayed **before** the DbContext transaction commits

1 participant