fix(efcore): enlist HTTP endpoint outbox in Lightweight mode so cascades flush after commit (GH-3291)#3298
Merged
Conversation
…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>
This was referenced Jul 9, 2026
Merged
This was referenced Jul 10, 2026
This was referenced Jul 10, 2026
This was referenced Jul 14, 2026
Open
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.
Fixes #3291.
Problem
In
TransactionMiddlewareMode.Lightweight, cascading messages returned from aWolverine.Httpendpoint (the(IResult, TMessage)tuple-return style) are sent to their destination immediately, beforeSaveChangesAsynccommits 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
MessageContextis enlisted in the outbox (Transaction != null):MessageContext.ReadEnvelopesetsTransaction = this— its cascades buffer and flush after commit in both modes.EnrollDbContextInTransactionmiddleware, which is added only in Eager mode. In Lightweight modeTransactionstaysnull, soMessageBus.PersistOrSendAsynctakes theStoreAndForwardAsync()(send-now) branch. The standaloneFlushOutgoingMessagespostprocessor 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.ApplyTransactionSupportnow, for an HTTP chain in Lightweight mode that requires the outbox, inserts a newEnlistDbContextInOutboxmiddleware that enrolls the DbContext +IMessageContextin the outbox without an explicitBeginTransactionAsync:EfCoreEnvelopeTransaction.PersistOutgoingAsyncwrites the outgoing envelope viaDbContext.Add(new OutgoingMessage(...)), whichSaveChangesAsynccommits atomically with the endpoint's entity — so the cascade now buffers and flushes after the commit (same commit-then-flush ordering as Eager).BeginTransactionAsynckeeps 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.EnlistDbContextInOutboximplementsIFlushesMessages, soHttpChainno longer adds a standalone (pre-commit)FlushOutgoingMessages; the existingCommitEfCoreEnvelopeTransactionpostprocessor 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(noWolverine.Httptype 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):FlushOutgoingMessagespostprocessor,EnlistInOutboxAsync,SaveChangesAsync→CommitAsync(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-efcoreguidance is being updated separately (JasperFx/ai-skills#85).