Skip to content

Enlist Lightweight-mode HTTP storage-action chains in the EF Core outbox (GH-3353)#3357

Merged
jeremydmiller merged 1 commit into
mainfrom
fix/3353-efcore-lightweight-storage-action-http-outbox
Jul 10, 2026
Merged

Enlist Lightweight-mode HTTP storage-action chains in the EF Core outbox (GH-3353)#3357
jeremydmiller merged 1 commit into
mainfrom
fix/3353-efcore-lightweight-storage-action-http-outbox

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #3353. Follow-up to #3291 / #3298.

The bug

The #3291 fix landed in EFCorePersistenceFrameProvider.ApplyTransactionSupport(chain, container) — the two-argument overload reached from AutoApplyTransactions. 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 the UsingEfCoreTransaction chain tag, and AutoApplyTransactions only claims chains with a DbContext service dependency, so a Wolverine.Http endpoint that persists purely through a storage action (never injecting the DbContext) was claimed only by the unfixed overload. In Lightweight mode its cascaded messages were dispatched by the send-now branch of MessageBus.PersistOrSendAsync before SaveChangesAsync commits.

The fix

Mirror the #3291 branch into the entityType overload — reusing the exact same proven frames (EnlistDbContextInOutbox middleware + CommitEfCoreEnvelopeTransaction postprocessor) — with one deliberate difference: no chain.RequiresOutbox() gate. As the issue analysis predicted, that gate is unsatisfiable there: the overload runs from SideEffectPolicy (registered ahead of OutgoingMessagesPolicy in the WolverineOptions constructor), and HttpChain.RequiresOutbox() only reflects an injected IMessageBus/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 EfCoreEnvelopeTransaction constructor's precondition (TryFindMessageDatabase). Without that guard, a persistence-less EF Core application (plain AddDbContext, no message store) would generate code that throws InvalidOperationException on 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.Tests assembly can't host that (its endpoints assume Marten at chain-construction time — [Entity]-loaded Marten documents fail EntityAttribute.Modify without it), so this PR adds a tiny Wolverine.Http.Tests.EfCoreOnly assembly holding just the endpoint + DbContext, and the test pins opts.ApplicationAssembly there (the established generate_openapi_without_database pattern). With no Marten registered, EF Core is naturally the only persistence frame provider — no provider-ordering games needed.

Bug_3353_lightweight_storage_action_cascade asserts at the codegen surface, matching the sibling #3291 reproducers: EnlistInOutboxAsync present, no standalone FlushOutgoingMessages postprocessor, and enlist → SaveChangesAsyncCommitAsync ordering. Red on main at the EnlistInOutboxAsync assertion; green with the fix.

Verification

  • Both new tests green; full Wolverine.Http.Tests: 812 passed / 0 failed (net9.0).
  • EfCoreTests: 186/187 — the one failure is Bug_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 own with_http_posts_using_storage_actions — HTTP storage actions over multi-tenant EF Core — passes everywhere.)
  • Full wolverine.slnx Release build: 0 warnings, 0 errors.

🤖 Generated with Claude Code

…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>
@jeremydmiller
jeremydmiller merged commit 27e5d5f into main Jul 10, 2026
24 of 25 checks passed
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant