Skip to content

EF Core Lightweight mode: HTTP endpoint that injects the DbContext and cascades only via tuple return is still sent before commit (RequiresOutbox gate) #3358

Description

@jeremydmiller

Follow-up to #3291 (fixed by #3298) and #3353 (fixed by #3357). Filed for completeness while auditing that area — the remaining uncovered shape in the same defect family.

Summary

The GH-3291 branch in EFCorePersistenceFrameProvider.ApplyTransactionSupport(chain, container) — the two-argument overload reached from AutoApplyTransactions — gates the Lightweight-mode outbox enlistment on chain.RequiresOutbox():

else if (isHttpChain(chain)
         && !isMultiTenanted(container, dbContextType)
         && chain.RequiresOutbox()          // <-- this gate
         && chain.ShouldFlushOutgoingMessages()
         && hasDatabaseBackedMessagePersistence(container))

HttpChain.RequiresOutbox() only reflects an injected IMessageBus/MessageContext service dependency (HttpChain.cs, ServiceDependencies(...).Contains(typeof(IMessageBus))). An endpoint that injects the DbContext (so CanApply is true and AutoApplyTransactions claims the chain through this overload) but cascades only via its return tuple never satisfies it:

[WolverinePost("/ef/lightweight/dbcontext-cascade")]
public static (IResult, ItemStored) Post(CreateItemCommand command, ItemsDbContext db)
{
    var item = new Item { Id = Guid.NewGuid(), Name = command.Name };
    db.Items.Add(item);
    return (Results.Ok(), new ItemStored(item.Id));
}

Verified

Probed on main + #3357 (EF-Core-only host, TransactionMiddlewareMode.Lightweight, AutoApplyTransactions, durable local queues, PostgreSQL message store), instrumenting the generated chain:

enlist=False cascade=True save=True standaloneFlush=True requiresOutbox=False

No EnlistInOutboxAsync; EnqueueCascadingAsync runs before SaveChangesAsync, and with MessageContext.Transaction null the send-now branch of MessageBus.PersistOrSendAsync dispatches the cascade before the commit — the same runtime symptom as #3291/#3353, reached through the third and last endpoint shape:

Endpoint shape Claimed by Status
injects IMessageBus, publishes explicitly two-arg overload fixed by #3298
storage action only, no DbContext injection entityType overload fixed by #3357
injects DbContext, cascades via tuple two-arg overload this issue

Suggested fix

Since #3357 the branch already requires database-backed message persistence (hasDatabaseBackedMessagePersistence, mirroring EfCoreEnvelopeTransaction's constructor precondition), so the RequiresOutbox() gate could simply be dropped — enlisting an endpoint that never cascades is a no-op flush at commit time, which is exactly the trade #3357 made for the storage-action overload. Alternatively, teach HttpChain.RequiresOutbox() to also return true when the chain has cascading return values (note RequiresOutbox() is only consumed by the EF Core frame provider plus a diagnostics usage source, so the blast radius is small either way).

Either way this should come with test automation mirroring Bug_3353_lightweight_storage_action_cascade (the Wolverine.Http.Tests.EfCoreOnly pinned-ApplicationAssembly pattern): assert EnlistInOutboxAsync present, no pre-commit dispatch, enlist → SaveChangesAsync → CommitAsync ordering.

🤖 Generated with Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions