-
-
Notifications
You must be signed in to change notification settings - Fork 278
Description
When using Wolverine managed EF Core multi-tenancy + durable local queues + InvokeForTenantAsync(request/reply), returning domain events as the second item of:
Task<(TResponse, IEnumerable<object>)>
causes Wolverine to start an EF Core transaction during context.EnqueueCascadingAsync(outgoing2), but the generated handler code only calls SaveChangesAsync() and never commits the started transaction. The DbContext is disposed at the end of the handler, so the transaction is rolled back and the entity insert does not persist.
Additionally, even though the transaction is rolled back (customer row not stored), the cascaded message handler (CustomerCreatedHandler) still executes. With UseDurableLocalQueues() and transactional outbox, we would expect the cascaded message to not be dispatched if the transaction is not committed.
Versions / environment
- WolverineFx: 5.10.0
- WolverineFx.EntityFrameworkCore: 5.10.0
- WolverineFx.SqlServer: 5.10.0
- EF Core: 10.0.2
- SQL Server
- Target framework: net10.0
Expected behavior
- When handler returns
(result, [new DomainEvent(...)]):- the customer insert is committed, and
- the domain event is stored in the outbox and later dispatched.
- If the transaction rolls back, the domain event should not be dispatched (durable local).
Actual behavior
- When the cascading enumerable contains at least one event:
- transaction is started during
EnqueueCascadingAsync - generated adapter ends with
SaveChangesAsync()only, no explicit commit - customer insert is rolled back (row missing in DB)
- but
CustomerCreatedHandlerstill fires (unexpected under durable local/outbox)
- transaction is started during
- When the handler returns an empty enumerable (
[]), the customer insert commits normally.
Repro project
MultiTenantMessaging.zip
Steps to reproduce
- Set two connection strings in appsettings.json:
- ConnectionStrings:main = Wolverine message store DB
- ConnectionStrings:tenant1 = tenant DB that contains Customers
- Run the app.
The app invokes two tenant commands:
CreateCustomerNoEvents (returns[])- persistsCreateCustomerWithEvents (returns [new CustomerCreated(...)])- customer row is NOT persisted, butCustomerCreatedHandlerstill runs.
Minimal handler code
[Transactional]
[WolverineHandler]
public sealed class CreateCustomerWithEventsHandler(ICustomerRepository repository)
{
public async Task<(CreateCustomerResult, IEnumerable<object>)> Handle(
CreateCustomerWithEvents request,
CancellationToken ct)
{
var customer = new Customer { Id = Guid.NewGuid(), Name = request.Name };
await repository.AddAsync(customer, ct);
return (new CreateCustomerResult(customer.Id, customer.Name),
[new CustomerCreated(customer.Id)]);
}
}Generated handler adapter
Internal/Generated/WolverineHandlers/CreateCustomerWithEventsHandler*.cs:
// Outgoing, cascaded message
await context.EnqueueCascadingAsync(outgoing2).ConfigureAwait(false);
// Added by EF Core Transaction Middleware
var result_of_SaveChangesAsync = await tenantDbContext.SaveChangesAsync(cancellation).ConfigureAwait(false);EnqueueCascadingAsync(outgoing2) appears to start an EF transaction (via EfCoreEnvelopeTransaction), but there is no explicit commit afterward. Because the DbContext is disposed at the end of the generated handler method, the transaction is rolled back and the customer row is not saved.
Despite that, CustomerCreatedHandler still fires, seems to violate the durable local/outbox expectation that outgoing messages are only dispatched after successful commit.