Skip to content

Commit a9f9faa

Browse files
committed
Treat dispose of an active MongoTransaction as rollback
1 parent 2732144 commit a9f9faa

File tree

3 files changed

+114
-63
lines changed

3 files changed

+114
-63
lines changed

src/MongoDB.EntityFrameworkCore/Storage/MongoTransaction.cs

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
using System;
1717
using System.Diagnostics;
18-
using System.Linq;
1918
using System.Threading;
2019
using System.Threading.Tasks;
2120
using Microsoft.EntityFrameworkCore;
@@ -91,7 +90,6 @@ internal static MongoTransaction Start(
9190

9291
var transaction = new MongoTransaction(session, context, transactionId, transactionManager, transactionLogger);
9392
transactionLogger.TransactionStarted(transaction, async, startTime, stopwatch.Elapsed);
94-
9593
return transaction;
9694
}
9795

@@ -234,22 +232,49 @@ public TransactionOptions TransactionOptions
234232
/// <inheritdoc />
235233
public void Dispose()
236234
{
237-
if (_transactionState == TransactionState.Disposed) return;
238-
239-
AssertCorrectState("Dispose", TransactionState.Committed, TransactionState.RolledBack, TransactionState.Failed);
240-
_transactionState = TransactionState.Disposed;
241-
session.Dispose();
235+
switch (_transactionState)
236+
{
237+
case TransactionState.Disposed:
238+
return;
239+
240+
case TransactionState.Active:
241+
Rollback();
242+
return;
243+
244+
case TransactionState.Committed:
245+
case TransactionState.RolledBack:
246+
case TransactionState.Failed:
247+
_transactionState = TransactionState.Disposed;
248+
session.Dispose();
249+
return;
250+
251+
default:
252+
throw new InvalidOperationException($"Can not Dispose MongoTransaction {TransactionId} because it is {_transactionState}.");
253+
}
242254
}
243255

244256
/// <inheritdoc />
245-
public ValueTask DisposeAsync()
257+
public async ValueTask DisposeAsync()
246258
{
247-
if (_transactionState == TransactionState.Disposed) return ValueTask.CompletedTask;
248-
249-
AssertCorrectState("Dispose", TransactionState.Committed, TransactionState.RolledBack, TransactionState.Failed);
250-
_transactionState = TransactionState.Disposed;
251-
session.Dispose();
252-
return ValueTask.CompletedTask;
259+
switch (_transactionState)
260+
{
261+
case TransactionState.Disposed:
262+
return;
263+
264+
case TransactionState.Active:
265+
await RollbackAsync();
266+
return;
267+
268+
case TransactionState.Committed:
269+
case TransactionState.RolledBack:
270+
case TransactionState.Failed:
271+
_transactionState = TransactionState.Disposed;
272+
session.Dispose();
273+
return;
274+
275+
default:
276+
throw new InvalidOperationException($"Can not Dispose MongoTransaction {TransactionId} because it is {_transactionState}.");
277+
}
253278
}
254279

255280
private void AssertCorrectState(string action, TransactionState validState)
@@ -258,11 +283,4 @@ private void AssertCorrectState(string action, TransactionState validState)
258283
throw new
259284
InvalidOperationException($"Can not {action} MongoTransaction {TransactionId} because it is {_transactionState}.");
260285
}
261-
262-
private void AssertCorrectState(string action, params TransactionState[] validStates)
263-
{
264-
if (!validStates.Contains(_transactionState))
265-
throw new
266-
InvalidOperationException($"Can not {action} MongoTransaction {TransactionId} because it is {_transactionState}.");
267-
}
268286
}

tests/MongoDB.EntityFrameworkCore.FunctionalTests/Design/Generated/EF8/SimpleContextModelBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ partial void Initialize()
2020
EveryTypeEntityType.CreateAnnotations(everyType);
2121
OwnedEntityEntityType.CreateAnnotations(ownedEntity);
2222

23-
AddAnnotation("ProductVersion", "8.0.20");
23+
AddAnnotation("ProductVersion", "8.0.22");
2424
}
2525
}
2626
}

tests/MongoDB.EntityFrameworkCore.FunctionalTests/Storage/TransactionTests.cs

Lines changed: 74 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -273,27 +273,6 @@ await db.Entities.AddRangeAsync(
273273
}
274274
}
275275

276-
[Fact]
277-
public void Explicit_transaction_dispose_without_commit_or_rollback_throws_and_changes_not_visible()
278-
{
279-
var collection = database.CreateCollection<SimpleEntity>();
280-
281-
// Use `using` to force Dispose without commit/rollback
282-
var ex = Assert.Throws<InvalidOperationException>(() =>
283-
{
284-
using var db = SingleEntityDbContext.Create(collection);
285-
using var transaction = db.Database.BeginTransaction();
286-
db.Entities.Add(new SimpleEntity { name = "pending" });
287-
db.SaveChanges();
288-
// Intentionally not committing or rolling back; disposing transaction should throw
289-
});
290-
291-
Assert.Contains("Dispose", ex.Message);
292-
293-
using var dbCheck = SingleEntityDbContext.Create(collection);
294-
Assert.Empty(dbCheck.Entities); // nothing committed
295-
}
296-
297276
[Fact]
298277
public void Ambient_TransactionScope_blocks_begin_explicit_transaction()
299278
{
@@ -526,39 +505,29 @@ public async Task Same_context_reads_its_own_writes_inside_explicit_transaction_
526505
}
527506

528507
[Fact]
529-
public void Disposing_explicit_transaction_without_commit_or_rollback_throws_and_does_not_commit()
508+
public void Disposing_explicit_transaction_without_commit_or_rollback_does_not_commit()
530509
{
531510
var collection = database.CreateCollection<SimpleEntity>();
532511

533-
var ex = Assert.Throws<InvalidOperationException>(() =>
534-
{
535-
using var db = SingleEntityDbContext.Create(collection);
536-
using var transaction = db.Database.BeginTransaction();
537-
db.Entities.Add(new SimpleEntity { name = "pending" });
538-
db.SaveChanges();
539-
// Transaction goes out of scope without Commit/Rollback -> Dispose should throw
540-
});
512+
using var db = SingleEntityDbContext.Create(collection);
513+
using var transaction = db.Database.BeginTransaction();
514+
db.Entities.Add(new SimpleEntity { name = "pending" });
515+
db.SaveChanges();
541516

542-
Assert.Contains("Dispose", ex.Message);
543517
using var check = SingleEntityDbContext.Create(collection);
544518
Assert.Empty(check.Entities);
545519
}
546520

547521
[Fact]
548-
public async Task Disposing_explicit_transaction_without_commit_or_rollback_throws_and_does_not_commit_async()
522+
public async Task Disposing_explicit_transaction_without_commit_or_rollback_does_not_commit_async()
549523
{
550524
var collection = database.CreateCollection<SimpleEntity>();
551525

552-
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
553-
{
554-
await using var db = SingleEntityDbContext.Create(collection);
555-
await using var transaction = await db.Database.BeginTransactionAsync();
556-
await db.Entities.AddAsync(new SimpleEntity { name = "pending" });
557-
await db.SaveChangesAsync();
558-
// Transaction goes out of scope without Commit/Rollback -> DisposeAsync should throw
559-
});
526+
await using var db = SingleEntityDbContext.Create(collection);
527+
await using var transaction = await db.Database.BeginTransactionAsync();
528+
await db.Entities.AddAsync(new SimpleEntity { name = "pending" });
529+
await db.SaveChangesAsync();
560530

561-
Assert.Contains("Dispose", ex.Message);
562531
await using var check = SingleEntityDbContext.Create(collection);
563532
Assert.Empty(await check.Entities.ToListAsync());
564533
}
@@ -667,4 +636,68 @@ public void Can_start_new_explicit_transaction_on_same_context_after_rollback()
667636
var names = check.Entities.Select(e => e.name).OrderBy(n => n).ToList();
668637
Assert.Equal(["transaction2-committed"], names);
669638
}
639+
640+
[Theory]
641+
[InlineData(false)]
642+
// [InlineData(true)]
643+
public void Can_execute_transasction_via_execution_strategy(bool succeeds)
644+
{
645+
var collection = database.CreateCollection<SimpleEntity>(values: [succeeds]);
646+
647+
var db = SingleEntityDbContext.Create(collection);
648+
var strategy = db.Database.CreateExecutionStrategy();
649+
var expectedCount = succeeds ? 2 : 0;
650+
651+
try
652+
{
653+
strategy.ExecuteInTransaction(
654+
db,
655+
operation: context =>
656+
{
657+
Assert.NotNull(context.Entities);
658+
context.Entities.Add(new SimpleEntity { name = "First" });
659+
context.Entities.Add(new SimpleEntity { name = "Second" });
660+
context.SaveChanges();
661+
if (!succeeds) throw new Exception("Expected failure");
662+
},
663+
verifySucceeded: _ => true);
664+
}
665+
catch (Exception ex) when (ex.Message == "Expected failure")
666+
{
667+
}
668+
669+
Assert.Equal(expectedCount, db.Entities.Count());
670+
}
671+
672+
[Theory]
673+
[InlineData(false)]
674+
[InlineData(true)]
675+
public async Task Can_execute_transasction_via_execution_strategy_async(bool succeeds)
676+
{
677+
var collection = database.CreateCollection<SimpleEntity>(values: [succeeds]);
678+
679+
var db = SingleEntityDbContext.Create(collection);
680+
var strategy = db.Database.CreateExecutionStrategy();
681+
var expectedCount = succeeds ? 2 : 0;
682+
683+
try
684+
{
685+
await strategy.ExecuteInTransactionAsync(
686+
db,
687+
operation: async (context, cancellationToken) =>
688+
{
689+
Assert.NotNull(context.Entities);
690+
context.Entities.Add(new SimpleEntity { name = "First" });
691+
context.Entities.Add(new SimpleEntity { name = "Second" });
692+
await context.SaveChangesAsync(cancellationToken);
693+
if (!succeeds) throw new Exception("Expected failure");
694+
},
695+
verifySucceeded: (_, _) => Task.FromResult(true));
696+
}
697+
catch (Exception ex) when (ex.Message == "Expected failure")
698+
{
699+
}
700+
701+
Assert.Equal(expectedCount, db.Entities.Count());
702+
}
670703
}

0 commit comments

Comments
 (0)