diff --git a/src/Persistence/EfCoreTests/transaction_middleware_mode_tests.cs b/src/Persistence/EfCoreTests/transaction_middleware_mode_tests.cs index 476e513a0..ad7369115 100644 --- a/src/Persistence/EfCoreTests/transaction_middleware_mode_tests.cs +++ b/src/Persistence/EfCoreTests/transaction_middleware_mode_tests.cs @@ -2,6 +2,7 @@ using JasperFx.CodeGeneration.Frames; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Hosting; +using SharedPersistenceModels.Items; using Shouldly; using Wolverine; using Wolverine.Attributes; @@ -156,6 +157,69 @@ public async Task transactional_attribute_eager_overrides_lightweight_default() .ShouldBeTrue(); } + [Fact] + public async Task lightweight_attribute_with_storage_side_effects_should_not_add_transaction_frame() + { + using var host = await Host.CreateDefaultBuilder() + .UseWolverine(opts => + { + opts.Durability.Mode = DurabilityMode.Solo; + + opts.Services.AddDbContextWithWolverineIntegration(x => + x.UseSqlServer(Servers.SqlServerConnectionString)); + + opts.PersistMessagesWithSqlServer(Servers.SqlServerConnectionString, "txmode"); + opts.UseEntityFrameworkCoreTransactions(TransactionMiddlewareMode.Eager); + opts.Policies.AutoApplyTransactions(); + + opts.Discovery.DisableConventionalDiscovery() + .IncludeType(); + }).StartAsync(); + + // Force compilation + host.GetRuntime().Handlers.HandlerFor(); + var chain = host.GetRuntime().Handlers.ChainFor(); + + // The [Transactional(Mode = Lightweight)] should override even with Storage side effects + chain.Middleware.OfType().ShouldBeEmpty(); + chain.Middleware.OfType().ShouldBeEmpty(); + + chain.Postprocessors.OfType() + .Any(x => x.Method.Name == nameof(DbContext.SaveChangesAsync)) + .ShouldBeTrue(); + } + + [Fact] + public async Task eager_attribute_with_storage_side_effects_should_add_transaction_frame() + { + using var host = await Host.CreateDefaultBuilder() + .UseWolverine(opts => + { + opts.Durability.Mode = DurabilityMode.Solo; + + opts.Services.AddDbContextWithWolverineIntegration(x => + x.UseSqlServer(Servers.SqlServerConnectionString)); + + opts.PersistMessagesWithSqlServer(Servers.SqlServerConnectionString, "txmode"); + opts.UseEntityFrameworkCoreTransactions(TransactionMiddlewareMode.Lightweight); + opts.Policies.AutoApplyTransactions(); + + opts.Discovery.DisableConventionalDiscovery() + .IncludeType(); + }).StartAsync(); + + // Force compilation + host.GetRuntime().Handlers.HandlerFor(); + var chain = host.GetRuntime().Handlers.ChainFor(); + + // The [Transactional(Mode = Eager)] should override the Lightweight default + chain.Middleware.OfType().ShouldNotBeEmpty(); + + chain.Postprocessors.OfType() + .Any(x => x.Method.Name == nameof(DbContext.SaveChangesAsync)) + .ShouldBeTrue(); + } + [Fact] public async Task default_mode_is_eager() { @@ -253,4 +317,26 @@ public static void Handle(LightweightAutoApplyMessage message, CleanDbContext db } } +public record LightweightStorageSideEffectMessage; + +public class LightweightStorageSideEffectHandler +{ + [Transactional(Mode = TransactionMiddlewareMode.Lightweight)] + public static Insert Handle(LightweightStorageSideEffectMessage message) + { + return Storage.Insert(new Item { Id = Guid.NewGuid(), Name = "test" }); + } +} + +public record EagerStorageSideEffectMessage; + +public class EagerStorageSideEffectHandler +{ + [Transactional(Mode = TransactionMiddlewareMode.Eager)] + public static Insert Handle(EagerStorageSideEffectMessage message) + { + return Storage.Insert(new Item { Id = Guid.NewGuid(), Name = "test" }); + } +} + #endregion diff --git a/src/Persistence/Wolverine.EntityFrameworkCore/Codegen/EFCorePersistenceFrameProvider.cs b/src/Persistence/Wolverine.EntityFrameworkCore/Codegen/EFCorePersistenceFrameProvider.cs index c786b8bed..000012ea6 100644 --- a/src/Persistence/Wolverine.EntityFrameworkCore/Codegen/EFCorePersistenceFrameProvider.cs +++ b/src/Persistence/Wolverine.EntityFrameworkCore/Codegen/EFCorePersistenceFrameProvider.cs @@ -1,3 +1,4 @@ +using System.Reflection; using ImTools; using JasperFx; using JasperFx.CodeGeneration; @@ -8,6 +9,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Wolverine.Attributes; using Wolverine.Configuration; using Wolverine.EntityFrameworkCore.Internals; using Wolverine.Persistence; @@ -125,10 +127,7 @@ public void ApplyTransactionSupport(IChain chain, IServiceContainer container) var dbContextType = DetermineDbContextType(chain, container); - // Resolve effective mode: per-chain override from [Transactional] attribute, or default - var mode = chain.Tags.TryGetValue(TransactionModeKey, out var modeObj) - ? (TransactionMiddlewareMode)modeObj - : DefaultMode; + var mode = ResolveEffectiveMode(chain); var runtime = container.Services.GetRequiredService(); if (runtime.Stores.HasAncillaryStoreFor(dbContextType)) @@ -170,6 +169,44 @@ public void ApplyTransactionSupport(IChain chain, IServiceContainer container) } } + /// + /// Resolves the effective transaction mode for a chain by checking (in order): + /// 1. The chain tag (set when TransactionalAttribute.Modify has already run) + /// 2. The [Transactional] attribute directly on handler methods/types (for when + /// side effects are processed by SideEffectPolicy before the attribute's Modify runs) + /// 3. The configured DefaultMode + /// + internal TransactionMiddlewareMode ResolveEffectiveMode(IChain chain) + { + // Check the tag first (set by TransactionalAttribute.Modify when it has already run) + if (chain.Tags.TryGetValue(TransactionModeKey, out var modeObj)) + { + return (TransactionMiddlewareMode)modeObj; + } + + // Check handler method and type attributes directly for when SideEffectPolicy + // processes Storage return types before TransactionalAttribute.Modify has run + foreach (var call in chain.HandlerCalls()) + { + var methodAttr = call.Method.GetCustomAttribute(); + if (methodAttr is { IsModeExplicitlySet: true }) + { + // Cache it in the tag for subsequent calls + chain.Tags[TransactionModeKey] = methodAttr.Mode; + return methodAttr.Mode; + } + + var typeAttr = call.HandlerType.GetCustomAttribute(); + if (typeAttr is { IsModeExplicitlySet: true }) + { + chain.Tags[TransactionModeKey] = typeAttr.Mode; + return typeAttr.Mode; + } + } + + return DefaultMode; + } + private bool isMultiTenanted(IServiceContainer container, Type dbContextType) { return container.HasRegistrationFor(typeof(IDbContextBuilder<>).MakeGenericType(dbContextType)); @@ -182,10 +219,7 @@ public void ApplyTransactionSupport(IChain chain, IServiceContainer container, T var dbType = DetermineDbContextType(entityType, container); - // Resolve effective mode: per-chain override from [Transactional] attribute, or default - var mode = chain.Tags.TryGetValue(TransactionModeKey, out var modeObj) - ? (TransactionMiddlewareMode)modeObj - : DefaultMode; + var mode = ResolveEffectiveMode(chain); if (mode == TransactionMiddlewareMode.Eager) { diff --git a/src/Wolverine/Attributes/TransactionalAttribute.cs b/src/Wolverine/Attributes/TransactionalAttribute.cs index bcb939aac..0f9f153f4 100644 --- a/src/Wolverine/Attributes/TransactionalAttribute.cs +++ b/src/Wolverine/Attributes/TransactionalAttribute.cs @@ -53,6 +53,13 @@ public TransactionMiddlewareMode Mode } private TransactionMiddlewareMode _mode; + /// + /// Returns true if was explicitly set on this attribute instance. + /// Used by persistence providers to resolve the effective mode even before + /// has been called (e.g. when side effects are processed at startup). + /// + public bool IsModeExplicitlySet => _modeExplicitlySet; + public TransactionalAttribute() { }