Skip to content

IDbContextOutbox.ScheduleAsync starts the ef core transaction implicitly #3121

Closed

Description

@Maflend

Describe the bug
When "IDbContextOutbox.PublishAsync" is called, an explicit transaction is not started. This is correct, as EF CORE allows you not to run explicit transactions, but only to use SaveChanges.

When calling "IDbContextOutbox.ScheduleAsync" an explicit transaction is running. This is not correct, since EF CORE allows you not to run explicit transactions, but only to use SaveChanges.
This is not expected for client code. It is up to the client code to decide for itself whether it wants to run an explicit transaction or not.

The wolverine source code shows that the "transaction.PersistOutgoingAsync(envelope);" method is being called for "EnvelopeStatus.Outgoing" which does not trigger an explicit transaction.
And for "EnvelopeStatus.Scheduled" the "transaction.PersistIncomingAsync(envelope)" method is called which triggers an explicit transaction:

public static class EnvelopeTransactionExtensions

To Reproduce

app.MapGet("/t1", async (IDbContextOutbox outbox, AppDbContext db) =>
{
    outbox.Enroll(db);
    await outbox.PublishAsync(new TestMsg()); // does not start an ef core transaction implicitly
    await db.SaveChangesAsync();   // Saves to the wolverine table wolverine_outgoing_envelopes
});

app.MapGet("/t2", async (IDbContextOutbox outbox, AppDbContext db) =>
{
    outbox.Enroll(db);
    await outbox.ScheduleAsync(new TestMsg(), TimeSpan.FromMinutes(1)); // starts the ef core transaction implicitly
    await db.SaveChangesAsync(); // Wolverine itself implicitly started the transaction 😮, and now it turns out that I must definitely CommitAsync.
});

Expected behavior
ScheduleAsync should not run the ef core transaction itself, because PublishAsync does not run.

Activity

  1. jeremydmiller commented on Jun 16, 2026

    @jeremydmiller
    Member

    @Maflend I take pull requests, and you're using Wolverine non-idiomatically up there, and that makes you hard to support. I think you could have an easier time using Wolverine.HTTP and being more idiomatic. This particular usage got missed in the umpteen times we have to adjust EF Core transactions.

    And hey, you're consistently needing a lot of assistance with Wolverine. Please consider the support agreement, or look to contribute some instead.

    A small adjustment is coming up in 6.11 soon.

  2. jeremydmiller commented on Jun 16, 2026

    @jeremydmiller
    Member

    Thanks for the detailed report — you're correct, and there's a fix on the way in #3122.

    Root cause: ScheduleAsync produces an envelope with status Scheduled, which routes to EfCoreEnvelopeTransaction.PersistIncomingAsync. That method was beginning an explicit EF Core transaction unconditionally, whereas PersistOutgoingAsync (used by PublishAsync) only does so for the non-Wolverine-mapped path. The fix makes PersistIncomingAsync mirror the outgoing path, so for a Wolverine-mapped DbContext it just tracks the envelope and lets SaveChangesAsync provide the transaction.

    One important note for your usage: this only applies when the DbContext type is enabled for Wolverine — i.e. you've called modelBuilder.MapWolverineEnvelopeStorage(...) in your OnModelCreating so the envelope storage is mapped into that DbContext. For a plain (non-mapped) DbContext, Wolverine has to persist the envelope through a raw ADO command that must share your transaction, so it will still open one (consistent with how PublishAsync behaves in that same raw scenario). So to get the "no implicit transaction" behavior for both PublishAsync and ScheduleAsync, use a Wolverine-mapped DbContext.

  3. added a commit that references this issue on Jun 16, 2026
    5403532
  4. Maflend commented on Jun 16, 2026

    @Maflend
    Author

    @Maflend I take pull requests, and you're using Wolverine non-idiomatically up there, and that makes you hard to support. I think you could have an easier time using Wolverine.HTTP and being more idiomatic. This particular usage got missed in the umpteen times we have to adjust EF Core transactions.

    And hey, you're consistently needing a lot of assistance with Wolverine. Please consider the support agreement, or look to contribute some instead.

    A small adjustment is coming up in 6.11 soon.

    It would be good to start considering this an idiomatic way of applying it.
    Using Wolverine exclusively as a messaging framework (similar to EasyNetQ or MassTransit) opens the way for it to projects where they are not ready to change the architecture entirely.

    The documentation details the advantages of Wolverine over Mediatr and MVC Controllers, but I understand that refactoring the current code for Wolverine.HTTP or its CQRS is too big a step that not everyone is ready to take.

    At the same time, the problem of reliable delivery via buses is acute for most projects. Wolverine offers ready-made solutions for this task: guaranteed Inbox/Outbox delivery, deferred messages, partitioning, and so on. This is exactly the functionality that everyone needs, and it would be great to be able to implement it point-by-point.

    At first glance, it seems that a step has already been taken towards this by adding an IDbContextOutbox.
    And this is an important step, as it has already allowed us to switch to wolverine on two projects to implement reliable EDA with Kafka.

    Thanks!

  5. added a commit that references this issue on Jun 16, 2026
    000fda0
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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions