Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions src/Persistence/EfCoreTests/transaction_middleware_mode_tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<CleanDbContext>(x =>
x.UseSqlServer(Servers.SqlServerConnectionString));

opts.PersistMessagesWithSqlServer(Servers.SqlServerConnectionString, "txmode");
opts.UseEntityFrameworkCoreTransactions(TransactionMiddlewareMode.Eager);
opts.Policies.AutoApplyTransactions();

opts.Discovery.DisableConventionalDiscovery()
.IncludeType<LightweightStorageSideEffectHandler>();
}).StartAsync();

// Force compilation
host.GetRuntime().Handlers.HandlerFor<LightweightStorageSideEffectMessage>();
var chain = host.GetRuntime().Handlers.ChainFor<LightweightStorageSideEffectMessage>();

// The [Transactional(Mode = Lightweight)] should override even with Storage side effects
chain.Middleware.OfType<EnrollDbContextInTransaction>().ShouldBeEmpty();
chain.Middleware.OfType<StartDatabaseTransactionForDbContext>().ShouldBeEmpty();

chain.Postprocessors.OfType<MethodCall>()
.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<CleanDbContext>(x =>
x.UseSqlServer(Servers.SqlServerConnectionString));

opts.PersistMessagesWithSqlServer(Servers.SqlServerConnectionString, "txmode");
opts.UseEntityFrameworkCoreTransactions(TransactionMiddlewareMode.Lightweight);
opts.Policies.AutoApplyTransactions();

opts.Discovery.DisableConventionalDiscovery()
.IncludeType<EagerStorageSideEffectHandler>();
}).StartAsync();

// Force compilation
host.GetRuntime().Handlers.HandlerFor<EagerStorageSideEffectMessage>();
var chain = host.GetRuntime().Handlers.ChainFor<EagerStorageSideEffectMessage>();

// The [Transactional(Mode = Eager)] should override the Lightweight default
chain.Middleware.OfType<EnrollDbContextInTransaction>().ShouldNotBeEmpty();

chain.Postprocessors.OfType<MethodCall>()
.Any(x => x.Method.Name == nameof(DbContext.SaveChangesAsync))
.ShouldBeTrue();
}

[Fact]
public async Task default_mode_is_eager()
{
Expand Down Expand Up @@ -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<Item> 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<Item> Handle(EagerStorageSideEffectMessage message)
{
return Storage.Insert(new Item { Id = Guid.NewGuid(), Name = "test" });
}
}

#endregion
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Reflection;
using ImTools;
using JasperFx;
using JasperFx.CodeGeneration;
Expand All @@ -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;
Expand Down Expand Up @@ -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<IWolverineRuntime>();
if (runtime.Stores.HasAncillaryStoreFor(dbContextType))
Expand Down Expand Up @@ -170,6 +169,44 @@ public void ApplyTransactionSupport(IChain chain, IServiceContainer container)
}
}

/// <summary>
/// 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
/// </summary>
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<TransactionalAttribute>();
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<TransactionalAttribute>();
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));
Expand All @@ -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)
{
Expand Down
7 changes: 7 additions & 0 deletions src/Wolverine/Attributes/TransactionalAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ public TransactionMiddlewareMode Mode
}
private TransactionMiddlewareMode _mode;

/// <summary>
/// Returns true if <see cref="Mode"/> was explicitly set on this attribute instance.
/// Used by persistence providers to resolve the effective mode even before
/// <see cref="Modify"/> has been called (e.g. when side effects are processed at startup).
/// </summary>
public bool IsModeExplicitlySet => _modeExplicitlySet;

public TransactionalAttribute()
{
}
Expand Down
Loading