From 8edb1aeee50c96e958195ab59680e250ea96b223 Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Sat, 2 May 2026 16:49:31 -0500 Subject: [PATCH] The real fix to our EF Core + Outbox problem. Bump to 5.36.2 --- Directory.Build.props | 2 +- src/Http/Wolverine.Http/HttpChain.Codegen.cs | 2 ++ .../Optimistic_concurrency_with_ef_core.cs | 17 ++++++++++++++--- .../Codegen/EnrollDbContextInTransaction.cs | 2 +- src/Wolverine/Persistence/IFlushesMessages.cs | 7 +++++++ 5 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 src/Wolverine/Persistence/IFlushesMessages.cs diff --git a/Directory.Build.props b/Directory.Build.props index 7e9b76f2e..29f30553e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -11,7 +11,7 @@ 1570;1571;1572;1573;1574;1587;1591;1701;1702;1711;1735;0618;VSTHRD200 true enable - 5.36.1 + 5.36.2 $(PackageProjectUrl) true true diff --git a/src/Http/Wolverine.Http/HttpChain.Codegen.cs b/src/Http/Wolverine.Http/HttpChain.Codegen.cs index 68c1fa0be..87ec49bfb 100644 --- a/src/Http/Wolverine.Http/HttpChain.Codegen.cs +++ b/src/Http/Wolverine.Http/HttpChain.Codegen.cs @@ -174,6 +174,8 @@ internal IEnumerable DetermineFrames(GenerationRules rules) private bool requiresFlush(Frame[] actionsOnOtherReturnValues) { + if (Middleware.Any(x => x is IFlushesMessages)) return false; + if (Postprocessors.Any(x => x is IFlushesMessages)) return false; if (Postprocessors.Any(x => x.MaySendMessages())) return true; if (actionsOnOtherReturnValues.Any(x => x.MaySendMessages())) return true; diff --git a/src/Persistence/EfCoreTests/Optimistic_concurrency_with_ef_core.cs b/src/Persistence/EfCoreTests/Optimistic_concurrency_with_ef_core.cs index 68d69459f..ae8167b8e 100644 --- a/src/Persistence/EfCoreTests/Optimistic_concurrency_with_ef_core.cs +++ b/src/Persistence/EfCoreTests/Optimistic_concurrency_with_ef_core.cs @@ -20,7 +20,6 @@ namespace EfCoreTests; [Collection("sqlserver")] -[Trait("Category", "Flaky")] public class Optimistic_concurrency_with_ef_core { private readonly ITestOutputHelper _output; @@ -56,15 +55,27 @@ public async Task detect_concurrency_exception_as_SagaConcurrencyException() using var scope = host.Services.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); + // The saga's Id and the message's Id must match — Wolverine looks up the + // saga by the message's correlation Id (the `Id` field on + // UpdateConcurrencyTestSaga). Without a matching row the load throws + // UnknownSagaException before the handler can fake a concurrent update, + // which is what was making this test unconditionally fail (it was tagged + // [Flaky] but the failure was deterministic, not racy). With matching + // ids the saga loads, the handler's `OriginalValue = 999` trick simulates + // a stale read, SaveChangesAsync raises DbUpdateConcurrencyException, and + // EFCorePersistenceFrameProvider.WrapSagaConcurrencyException rethrows it + // as SagaConcurrencyException. + var sagaId = Guid.NewGuid(); await dbContext.ConcurrencyTestSagas.AddAsync(new() { - Id = Guid.NewGuid(), + Id = sagaId, Value = "initial value", Version = 0, }); await dbContext.SaveChangesAsync(); - await Should.ThrowAsync(() => host.InvokeMessageAndWaitAsync(new UpdateConcurrencyTestSaga(Guid.NewGuid(), "updated value"))); + await Should.ThrowAsync(() => + host.InvokeMessageAndWaitAsync(new UpdateConcurrencyTestSaga(sagaId, "updated value"))); } finally { diff --git a/src/Persistence/Wolverine.EntityFrameworkCore/Codegen/EnrollDbContextInTransaction.cs b/src/Persistence/Wolverine.EntityFrameworkCore/Codegen/EnrollDbContextInTransaction.cs index 9df0fc91a..fece85018 100644 --- a/src/Persistence/Wolverine.EntityFrameworkCore/Codegen/EnrollDbContextInTransaction.cs +++ b/src/Persistence/Wolverine.EntityFrameworkCore/Codegen/EnrollDbContextInTransaction.cs @@ -8,7 +8,7 @@ namespace Wolverine.EntityFrameworkCore.Codegen; -internal class EnrollDbContextInTransaction : AsyncFrame +internal class EnrollDbContextInTransaction : AsyncFrame, IFlushesMessages { private readonly Type _dbContextType; private readonly IdempotencyStyle _idempotencyStyle; diff --git a/src/Wolverine/Persistence/IFlushesMessages.cs b/src/Wolverine/Persistence/IFlushesMessages.cs new file mode 100644 index 000000000..355278488 --- /dev/null +++ b/src/Wolverine/Persistence/IFlushesMessages.cs @@ -0,0 +1,7 @@ +namespace Wolverine.Persistence; + +/// +/// Just a marker interface for the codegen that lets the codegen know +/// that a Frame is taking care of flushing messages itself +/// +public interface IFlushesMessages; \ No newline at end of file