diff --git a/src/Persistence/EfCoreTests.MultiTenancy/domain_events_scraping_with_managed_multi_tenancy.cs b/src/Persistence/EfCoreTests.MultiTenancy/domain_events_scraping_with_managed_multi_tenancy.cs
new file mode 100644
index 000000000..a78b2a21c
--- /dev/null
+++ b/src/Persistence/EfCoreTests.MultiTenancy/domain_events_scraping_with_managed_multi_tenancy.cs
@@ -0,0 +1,99 @@
+using System.Diagnostics;
+using IntegrationTests;
+using JasperFx;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using SharedPersistenceModels.Items;
+using SharedPersistenceModels.Orders;
+using Shouldly;
+using Wolverine;
+using Wolverine.EntityFrameworkCore;
+using Wolverine.Persistence;
+using Wolverine.Postgresql;
+using Wolverine.RDBMS;
+using Wolverine.Runtime;
+using Wolverine.Tracking;
+
+namespace EfCoreTests.MultiTenancy;
+
+///
+/// Regression guard for the managed-multi-tenancy counterpart of the single-DbContext domain-event
+/// scraper tests in EfCoreTests/DomainEvents/configuration_of_domain_events_scrapers.cs.
+///
+/// The EF Core transactional middleware has two codegen paths: the single-DbContext path commits
+/// through EfCoreEnvelopeTransaction.CommitAsync(), which runs the registered
+/// IDomainEventScrapers; the managed-multi-tenancy path
+/// (StartDatabaseTransactionForDbContext / CommitTenantedDbContextTransaction) used to
+/// commit the raw EF transaction directly and never invoked CommitAsync(), so the scrapers
+/// registered by PublishDomainEventsFromEntityFrameworkCore<T>(x => x.Events) never ran.
+/// A mutated entity's domain events were therefore silently dropped under managed multi-tenancy.
+///
+/// This test mutates an in a handler under a tenant so the entity publishes an
+/// domain event, then asserts (via a Wolverine tracked session) that the
+/// event was scraped and published.
+///
+public class domain_events_scraping_with_managed_multi_tenancy : MultiTenancyCompliance
+{
+ public domain_events_scraping_with_managed_multi_tenancy() : base(DatabaseEngine.PostgreSQL)
+ {
+ }
+
+ public override void Configure(WolverineOptions opts)
+ {
+ opts.PersistMessagesWithPostgresql(Servers.PostgresConnectionString, "domain_event_scraping_mt")
+ .RegisterStaticTenants(tenants =>
+ {
+ tenants.Register("red", tenant1ConnectionString);
+ tenants.Register("blue", tenant2ConnectionString);
+ tenants.Register("green", tenant3ConnectionString);
+ });
+
+ // Little weird, but we have to remove this DbContext to use
+ // the lightweight saga persistence
+ opts.Services.RemoveAll(typeof(OrdersDbContext));
+ opts.AddSagaType();
+
+ opts.Services.AddDbContextWithWolverineManagedMultiTenancy((builder, connectionString, _) =>
+ {
+ builder.UseNpgsql(connectionString.Value, b => b.MigrationsAssembly("MultiTenantedEfCoreWithPostgreSQL"));
+ }, AutoCreate.CreateOrUpdate);
+
+ // The behavior under test: scrape domain events off entities tracked by the tenant DbContext.
+ opts.PublishDomainEventsFromEntityFrameworkCore(x => x.Events);
+
+ // The approval handler + ItemApproved handler live in this test assembly.
+ opts.Discovery.IncludeAssembly(GetType().Assembly);
+ }
+
+ [Fact]
+ public async Task publishes_domain_events_scraped_from_the_tenant_db_context()
+ {
+ var itemId = Guid.NewGuid();
+
+ // Seed the item in the "blue" tenant database.
+ await theHost.InvokeMessageAndWaitAsync(new StartNewItem(itemId, "Latte"), "blue");
+
+ // Approve it under the same tenant - the handler calls Item.Approve(), which publishes an
+ // ItemApproved domain event into the entity's Events collection. The scraper must relay it.
+ var tracked = await theHost.InvokeMessageAndWaitAsync(new ApproveTenantedItem(itemId), "blue");
+
+ tracked.Sent.SingleMessage().Id.ShouldBe(itemId);
+ }
+}
+
+public record ApproveTenantedItem(Guid Id);
+
+public static class ApproveTenantedItemHandler
+{
+ // Taking the ItemsDbContext (via [Entity]) applies the EF Core transactional middleware so the
+ // domain-event scrape path is exercised on commit.
+ public static IStorageAction- Handle(ApproveTenantedItem command, [Entity] Item item)
+ {
+ // Publishes an ItemApproved event internally on the Item entity that we want relayed to Wolverine.
+ item.Approve();
+ return Storage.Update(item);
+ }
+
+ public static void Handle(ItemApproved e) => Debug.WriteLine($"Got item approved for {e.Id}");
+}
diff --git a/src/Persistence/Wolverine.EntityFrameworkCore/Codegen/StartDatabaseTransactionForDbContext.cs b/src/Persistence/Wolverine.EntityFrameworkCore/Codegen/StartDatabaseTransactionForDbContext.cs
index dd6ddd896..fedf6535b 100644
--- a/src/Persistence/Wolverine.EntityFrameworkCore/Codegen/StartDatabaseTransactionForDbContext.cs
+++ b/src/Persistence/Wolverine.EntityFrameworkCore/Codegen/StartDatabaseTransactionForDbContext.cs
@@ -4,6 +4,7 @@
using JasperFx.Core.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
+using Wolverine.EntityFrameworkCore.Internals;
using Wolverine.Persistence;
using Wolverine.Runtime;
@@ -83,12 +84,24 @@ public override IEnumerable FindVariables(IMethodVariables chain)
/// so the chain does not also add a standalone FlushOutgoingMessages
/// postprocessor (which would flush after the response, and before the commit). See GH-2917.
///
+///
+/// Before committing, this frame runs any registered s against the
+/// tenant DbContext exactly as
+/// does on the single-DbContext path (via ). Unlike that
+/// path, the multi-tenant DbContext is created at runtime inside
+/// , so the
+/// enlisted is never
+/// surfaced as a codegen variable and its CommitAsync is not in the generated chain. The scrape
+/// loop is therefore inlined here so that PublishDomainEventsFromEntityFrameworkCore works under
+/// managed multi-tenancy too.
+///
internal class CommitTenantedDbContextTransaction : AsyncFrame, IFlushesMessages
{
private readonly Type _dbContextType;
private Variable _dbContext = null!;
private Variable _context = null!;
private Variable _cancellation = null!;
+ private Variable _scrapers = null!;
public CommitTenantedDbContextTransaction(Type dbContextType)
{
@@ -97,6 +110,12 @@ public CommitTenantedDbContextTransaction(Type dbContextType)
public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
{
+ writer.WriteComment(
+ "Scrape any domain events out of the tenant DbContext before committing (mirrors EfCoreEnvelopeTransaction.CommitAsync)");
+ writer.Write($"BLOCK:foreach (var scraper in {_scrapers.Usage})");
+ writer.Write($"await scraper.{nameof(IDomainEventScraper.ScrapeEvents)}({_dbContext.Usage}, {_context.Usage}).ConfigureAwait(false);");
+ writer.FinishBlock();
+
writer.WriteComment(
"Commit the EF Core transaction and flush outgoing messages before writing the response (GH-2917)");
writer.Write($"await {_dbContext.Usage}.Database.CommitTransactionAsync({_cancellation.Usage}).ConfigureAwait(false);");
@@ -106,6 +125,9 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
public override IEnumerable FindVariables(IMethodVariables chain)
{
+ _scrapers = chain.FindVariable(typeof(IEnumerable));
+ yield return _scrapers;
+
_dbContext = chain.FindVariable(_dbContextType);
yield return _dbContext;