Enlist Lightweight-mode HTTP storage-action chains in the EF Core outbox (GH-3353)#3357
Merged
jeremydmiller merged 1 commit intoJul 10, 2026
Conversation
…box (GH-3353) The GH-3291 fix landed only in ApplyTransactionSupport(chain, container), the overload reached from AutoApplyTransactions. The entityType overload - reached from storage-action side effects (IStorageAction<T>, Storage.Insert/Store/Update/Delete) - never enlisted the endpoint's MessageContext, so a Wolverine.Http endpoint in Lightweight mode that persists purely through a storage action dispatched its cascaded messages BEFORE SaveChangesAsync committed. The new branch deliberately does not gate on chain.RequiresOutbox(): that overload runs from SideEffectPolicy, ahead of OutgoingMessagesPolicy, and HttpChain.RequiresOutbox() only reflects an injected IMessageBus or MessageContext - which the storage-action endpoint shape never has. Both HTTP branches now also require database-backed message persistence, matching the EfCoreEnvelopeTransaction constructor's precondition so a persistence-less EF Core application keeps its send-now composition instead of generating code that throws on every request. The reproducer needs an application that uses EF Core but NOT Marten, and the main HTTP test assembly's endpoints assume Marten at chain-construction time, so the endpoint + DbContext live in a new tiny Wolverine.Http.Tests.EfCoreOnly assembly that the test pins endpoint discovery to. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 10, 2026
This was referenced Jul 10, 2026
Merged
jeremydmiller
added a commit
that referenced
this pull request
Jul 10, 2026
…e outbox (GH-3358) (#3362) The GH-3291 branch of EFCorePersistenceFrameProvider.ApplyTransactionSupport (chain, container) was gated on chain.RequiresOutbox(), which for an HttpChain only reflects an injected IMessageBus/IMessageContext dependency. A Lightweight mode HTTP endpoint that injects the DbContext and cascades purely through its return tuple can never satisfy that gate, so its cascaded messages were still dispatched by the send-now branch of MessageBus.PersistOrSendAsync before the SaveChangesAsync postprocessor committed - the third and last endpoint shape in the GH-3291/GH-3353 defect family. Drop the RequiresOutbox() gate. GH-3357's hasDatabaseBackedMessagePersistence guard makes this safe: enlisting an endpoint that never cascades is a no-op flush at commit time, and persistence-less applications keep their send-now composition (pinned by a dedicated guard-rail test). Red-first verified: the new codegen-surface test fails without the gate change. Full Wolverine.Http.Tests 817/0, EfCoreTests 186/187 (known Bug_1846 load flake), full wolverine.slnx Release build 0 warnings / 0 errors. Closes #3358 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 10, 2026
This was referenced Jul 13, 2026
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.
Closes #3353. Follow-up to #3291 / #3298.
The bug
The #3291 fix landed in
EFCorePersistenceFrameProvider.ApplyTransactionSupport(chain, container)— the two-argument overload reached fromAutoApplyTransactions. The entityType overload — reached from the storage-action side-effect return types (IStorageAction<T>,Storage.Insert/Store/Update/Delete) — had no such branch. The two are mutually exclusive via theUsingEfCoreTransactionchain tag, andAutoApplyTransactionsonly claims chains with aDbContextservice dependency, so aWolverine.Httpendpoint that persists purely through a storage action (never injecting theDbContext) was claimed only by the unfixed overload. InLightweightmode its cascaded messages were dispatched by the send-now branch ofMessageBus.PersistOrSendAsyncbeforeSaveChangesAsynccommits.The fix
Mirror the #3291 branch into the entityType overload — reusing the exact same proven frames (
EnlistDbContextInOutboxmiddleware +CommitEfCoreEnvelopeTransactionpostprocessor) — with one deliberate difference: nochain.RequiresOutbox()gate. As the issue analysis predicted, that gate is unsatisfiable there: the overload runs fromSideEffectPolicy(registered ahead ofOutgoingMessagesPolicyin theWolverineOptionsconstructor), andHttpChain.RequiresOutbox()only reflects an injectedIMessageBus/MessageContext— which the storage-action endpoint shape never has. Enlisting an endpoint that turns out not to cascade anything is a no-op flush at commit time.While hardening this, both HTTP branches (the new one and the shipped #3291 one) now also require database-backed message persistence, mirroring the
EfCoreEnvelopeTransactionconstructor's precondition (TryFindMessageDatabase). Without that guard, a persistence-less EF Core application (plainAddDbContext, no message store) would generate code that throwsInvalidOperationExceptionon every request; it now keeps its pre-existing send-now composition. A dedicated test covers this.Message handlers and Eager mode are untouched; multi-tenant EF Core chains are excluded exactly as in #3291.
The reproducer
Per the issue, the bug needs an application that uses EF Core but not Marten. The main
Wolverine.Http.Testsassembly can't host that (its endpoints assume Marten at chain-construction time —[Entity]-loaded Marten documents failEntityAttribute.Modifywithout it), so this PR adds a tinyWolverine.Http.Tests.EfCoreOnlyassembly holding just the endpoint +DbContext, and the test pinsopts.ApplicationAssemblythere (the establishedgenerate_openapi_without_databasepattern). With no Marten registered, EF Core is naturally the only persistence frame provider — no provider-ordering games needed.Bug_3353_lightweight_storage_action_cascadeasserts at the codegen surface, matching the sibling #3291 reproducers:EnlistInOutboxAsyncpresent, no standaloneFlushOutgoingMessagespostprocessor, and enlist →SaveChangesAsync→CommitAsyncordering. Red on main at theEnlistInOutboxAsyncassertion; green with the fix.Verification
Wolverine.Http.Tests: 812 passed / 0 failed (net9.0).EfCoreTests: 186/187 — the one failure isBug_1846_duplicate_execution_of_scheduled_jobs, which passes in isolation (load-flaky locally).EfCoreTests.MultiTenancy: 135/141 in full-suite local runs, but the identical 6 SqlServer failures reproduce on unmodified main in a clean worktree against the same containers, and all 52 SqlServer MT tests pass in isolation on this branch — pre-existing local full-suite interference, not this change. (The MT suite's ownwith_http_posts_using_storage_actions— HTTP storage actions over multi-tenant EF Core — passes everywhere.)wolverine.slnxRelease build: 0 warnings, 0 errors.🤖 Generated with Claude Code