Skip to content

EF Core multi-tenanted transactional middleware emits AssertEagerIdempotencyAsync twice #4128

Description

@jeremydmiller

What

StartDatabaseTransactionForDbContext.GenerateCode (the multi-tenant counterpart to EnrollDbContextInTransaction) emits the eager idempotency check twice, under identical conditions:

https://github.com/JasperFx/wolverine/blob/main/src/Persistence/Wolverine.EntityFrameworkCore/Codegen/StartDatabaseTransactionForDbContext.cs#L32-L50

writer.Write("BLOCK:try");

// EF Core can only do eager idempotent checks
if (_idempotencyStyle == IdempotencyStyle.Eager || _idempotencyStyle == IdempotencyStyle.Optimistic)
{
    writer.Write($"await {_context!.Usage}.AssertEagerIdempotencyAsync(...);");   // (1)
}

writer.Write($"BLOCK:if ({_dbContext.Usage}.Database.CurrentTransaction == null)");
writer.Write($"await {_dbContext.Usage}.Database.BeginTransactionAsync(...);");
writer.FinishBlock();

// EF Core can only do eager idempotent checks
if (_idempotencyStyle == IdempotencyStyle.Eager || _idempotencyStyle == IdempotencyStyle.Optimistic)
{
    writer.Write($"await {_context!.Usage}.AssertEagerIdempotencyAsync(...);");   // (2) - identical
}

Both blocks sit at the top level of the same try, separated only by the begin-transaction if. Same guard, same emitted statement.

Which one is the duplicate

The non-tenanted sibling EnrollDbContextInTransaction.GenerateCode emits it once, after BeginTransactionAsync and inside the try:

https://github.com/JasperFx/wolverine/blob/main/src/Persistence/Wolverine.EntityFrameworkCore/Codegen/EnrollDbContextInTransaction.cs#L43-L49

So (2) matches the established shape and (1) looks like the copy-paste leftover to remove.

Impact

Mostly wasted work rather than wrong behavior, but not free:

  • Where an IEnvelopeTransaction is attached, call (1) sets Envelope.WasPersistedInInbox = true, so (2) short-circuits on the guard clause at the top of AssertEagerIdempotencyAsync. Cheap.
  • Where Transaction == null || Transaction is MessageContext, AssertEagerIdempotencyAsync takes the Runtime.Storage.Inbox.ExistsAsync(...) branch, which does not set WasPersistedInInbox. That path runs two identical inbox existence queries per message.

Ordering also differs slightly between the two placements — (1) runs the check before the tenant database transaction is opened, (2) after — which is a real semantic difference for the multi-tenant case even if only one call survives.

Found via

Auditing the wolverine-messaging-handler-idempotency AI skill against the source while documenting IdempotencyStyle. Related docs issue: #4127.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions