diff --git a/src/Persistence/EfCoreTests/MultiTenancy/MultiTenancyCompliance.cs b/src/Persistence/EfCoreTests/MultiTenancy/MultiTenancyCompliance.cs index e39cb9932..52698a60a 100644 --- a/src/Persistence/EfCoreTests/MultiTenancy/MultiTenancyCompliance.cs +++ b/src/Persistence/EfCoreTests/MultiTenancy/MultiTenancyCompliance.cs @@ -179,6 +179,39 @@ public async Task end_to_end_with_default_database() } } + [Fact] + public async Task end_to_end_with_cascading_messages() + { + var blueId = Guid.NewGuid(); + var redId = Guid.NewGuid(); + var greenId = Guid.NewGuid(); + + await theHost.InvokeMessageAndWaitAsync(new StartAndTriggerApproval(blueId, "Blue!"), "blue"); + await theHost.InvokeMessageAndWaitAsync(new StartAndTriggerApproval(redId, "Red!"), "red"); + await theHost.InvokeMessageAndWaitAsync(new StartAndTriggerApproval(greenId, "Green!"), "green"); + + var blueDbContext = await theBuilder.BuildAsync("blue", CancellationToken.None); + var greenDbContext = await theBuilder.BuildAsync("green", CancellationToken.None); + var redDbContext = await theBuilder.BuildAsync("red", CancellationToken.None); + + var blue = await blueDbContext.Items.FindAsync(blueId); + blue.Name.ShouldBe("Blue!"); + blue.Approved.ShouldBeTrue(); + (await greenDbContext.Items.FindAsync(blueId)).ShouldBeNull(); + (await redDbContext.Items.FindAsync(blueId)).ShouldBeNull(); + + (await blueDbContext.Items.FindAsync(redId)).ShouldBeNull(); + (await greenDbContext.Items.FindAsync(redId)).ShouldBeNull(); + var red = await redDbContext.Items.FindAsync(redId); + red.Name.ShouldBe("Red!"); + red.Approved.ShouldBeTrue(); + + (await blueDbContext.Items.FindAsync(greenId)).ShouldBeNull(); + var green = await greenDbContext.Items.FindAsync(greenId); + green.Name.ShouldBe("Green!"); + green.Approved.ShouldBeTrue(); + (await redDbContext.Items.FindAsync(greenId)).ShouldBeNull(); + } [Fact] public async Task with_http_posts_using_storage_actions() diff --git a/src/Persistence/Wolverine.EntityFrameworkCore/Codegen/EFCorePersistenceFrameProvider.cs b/src/Persistence/Wolverine.EntityFrameworkCore/Codegen/EFCorePersistenceFrameProvider.cs index ea4273080..557d884fa 100644 --- a/src/Persistence/Wolverine.EntityFrameworkCore/Codegen/EFCorePersistenceFrameProvider.cs +++ b/src/Persistence/Wolverine.EntityFrameworkCore/Codegen/EFCorePersistenceFrameProvider.cs @@ -131,7 +131,9 @@ public void ApplyTransactionSupport(IChain chain, IServiceContainer container) if (isMultiTenanted(container, dbContextType)) { var createContext = typeof(CreateTenantedDbContext<>).CloseAndBuildAs(dbContextType); + chain.Middleware.Insert(0, createContext); + chain.Middleware.Insert(0, new EnrollTenantedDbContextInTransaction(dbContextType, chain.Idempotency)); } else { @@ -171,6 +173,7 @@ public void ApplyTransactionSupport(IChain chain, IServiceContainer container, T { var createContext = typeof(CreateTenantedDbContext<>).CloseAndBuildAs(dbType); chain.Middleware.Insert(0, createContext); + chain.Middleware.Insert(0, new EnrollTenantedDbContextInTransaction(dbType, chain.Idempotency)); } else { diff --git a/src/Persistence/Wolverine.EntityFrameworkCore/Codegen/EnrollTenantedDbContextInTransaction.cs b/src/Persistence/Wolverine.EntityFrameworkCore/Codegen/EnrollTenantedDbContextInTransaction.cs new file mode 100644 index 000000000..8fcd9f09d --- /dev/null +++ b/src/Persistence/Wolverine.EntityFrameworkCore/Codegen/EnrollTenantedDbContextInTransaction.cs @@ -0,0 +1,62 @@ +using JasperFx.CodeGeneration; +using JasperFx.CodeGeneration.Frames; +using JasperFx.CodeGeneration.Model; +using JasperFx.Core.Reflection; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; +using Wolverine.Persistence; +using Wolverine.Runtime; + +namespace Wolverine.EntityFrameworkCore.Codegen; + +internal class EnrollTenantedDbContextInTransaction : AsyncFrame +{ + private readonly Type _dbContextType; + private readonly IdempotencyStyle _idempotencyStyle; + + private Variable _dbContext; + private Variable _cancellation; + private Variable? _context; + + public EnrollTenantedDbContextInTransaction(Type dbContextType, IdempotencyStyle idempotencyStyle) + { + _dbContextType = dbContextType; + _idempotencyStyle = idempotencyStyle; + } + + public override void GenerateCode(GeneratedMethod method, ISourceWriter writer) + { + writer.Write("BLOCK:try"); + + // EF Core can only do eager idempotent checks + if (_idempotencyStyle == IdempotencyStyle.Eager || _idempotencyStyle == IdempotencyStyle.Optimistic) + { + writer.Write($"await {_context.Usage}.{nameof(MessageContext.AssertEagerIdempotencyAsync)}({_cancellation.Usage});"); + } + + writer.Write($"BLOCK:if ({_dbContext.Usage}.Database.CurrentTransaction == null)"); + writer.Write($"await {_dbContext.Usage}.Database.BeginTransactionAsync({_cancellation.Usage});"); + writer.FinishBlock(); + + Next?.GenerateCode(method, writer); + + writer.Write($"await {_dbContext.Usage}.Database.CommitTransactionAsync({_cancellation.Usage});"); + writer.FinishBlock(); + writer.Write($"BLOCK:catch ({typeof(Exception).FullNameInCode()})"); + writer.Write($"await {_dbContext.Usage}.Database.RollbackTransactionAsync({_cancellation.Usage});"); + writer.Write("throw;"); + writer.FinishBlock(); + } + + public override IEnumerable FindVariables(IMethodVariables chain) + { + _context = chain.FindVariable(typeof(MessageContext)); + yield return _context; + + _dbContext = chain.FindVariable(_dbContextType); + yield return _dbContext; + + _cancellation = chain.FindVariable(typeof(CancellationToken)); + yield return _cancellation; + } +}