From 6a8ccd27de7e9609c1003ff1ab3faffc6470bc40 Mon Sep 17 00:00:00 2001 From: Laurents Meyer Date: Mon, 13 Jun 2022 22:04:50 +0200 Subject: [PATCH] Fix transactions issues (#129) * Fix active transaction support and disposed handling. * Add transaction baseline tests. * Fix transaction tests. --- src/EFCore.Jet.Data/JetTransaction.cs | 66 ++++++++++++++- test/EFCore.Jet.Data.Tests/TransactionTest.cs | 80 +++++++++++++++++++ ...actionJettest.cs => TransactionJetTest.cs} | 30 ++----- 3 files changed, 149 insertions(+), 27 deletions(-) create mode 100644 test/EFCore.Jet.Data.Tests/TransactionTest.cs rename test/EFCore.Jet.FunctionalTests/{TransactionJettest.cs => TransactionJetTest.cs} (67%) diff --git a/src/EFCore.Jet.Data/JetTransaction.cs b/src/EFCore.Jet.Data/JetTransaction.cs index f833dfd..b20e81a 100644 --- a/src/EFCore.Jet.Data/JetTransaction.cs +++ b/src/EFCore.Jet.Data/JetTransaction.cs @@ -1,11 +1,15 @@ -using System.Data; +using System; +using System.Data; using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; namespace EntityFrameworkCore.Jet.Data { internal class JetTransaction : DbTransaction { - private readonly JetConnection _connection; + private JetConnection _connection; + private bool _disposed; internal virtual DbTransaction WrappedTransaction { get; } @@ -22,8 +26,12 @@ namespace EntityFrameworkCore.Jet.Data public override void Commit() { + if (_disposed) + throw new ObjectDisposedException(nameof(JetTransaction)); + LogHelper.ShowCommandHeader("--- Commit"); WrappedTransaction.Commit(); + _connection.ActiveTransaction = null; } @@ -35,9 +43,63 @@ namespace EntityFrameworkCore.Jet.Data public override void Rollback() { + if (_disposed) + throw new ObjectDisposedException(nameof(JetTransaction)); + LogHelper.ShowCommandHeader("^^^ Rollback"); WrappedTransaction.Rollback(); + _connection.ActiveTransaction = null; } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + if (_connection?.ActiveTransaction == this) + { + if (Connection.State == ConnectionState.Open) + { + Rollback(); + } + + _connection.ActiveTransaction = null; + } + } + } + finally + { + _disposed = true; + _connection = null; + + base.Dispose(disposing); + } + } + + public override Task CommitAsync(CancellationToken cancellationToken = new CancellationToken()) + { + if (_disposed) + throw new ObjectDisposedException(nameof(JetTransaction)); + + return base.CommitAsync(cancellationToken); + } + + public override ValueTask DisposeAsync() + { + if (_disposed) + throw new ObjectDisposedException(nameof(JetTransaction)); + + return base.DisposeAsync(); + } + + public override Task RollbackAsync(CancellationToken cancellationToken = new CancellationToken()) + { + if (_disposed) + throw new ObjectDisposedException(nameof(JetTransaction)); + + return base.RollbackAsync(cancellationToken); + } } } \ No newline at end of file diff --git a/test/EFCore.Jet.Data.Tests/TransactionTest.cs b/test/EFCore.Jet.Data.Tests/TransactionTest.cs new file mode 100644 index 0000000..ac82971 --- /dev/null +++ b/test/EFCore.Jet.Data.Tests/TransactionTest.cs @@ -0,0 +1,80 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace EntityFrameworkCore.Jet.Data.Tests +{ + [TestClass] + public class TransactionTest + { + private const string StoreName = nameof(TransactionTest) + ".accdb"; + + private JetConnection _connection; + + [TestInitialize] + public void Setup() + { + _connection = Helpers.CreateAndOpenDatabase(StoreName); + + var script = @" +CREATE TABLE `Cookies` ( + `CookieId` counter NOT NULL, + `Name` text NULL, + CONSTRAINT `PK_Cookies` PRIMARY KEY (`CookieId`) +); + +INSERT INTO `Cookies` (`Name`) VALUES ('Basic'); +INSERT INTO `Cookies` (`Name`) VALUES ('Chocolate Chip'); +"; + + Helpers.ExecuteScript(_connection, script); + } + + [TestCleanup] + public void TearDown() + { + _connection?.Close(); + Helpers.DeleteDatabase(StoreName); + } + + [TestMethod] + public void Delete_rollback_implicit() + { + using (var transaction = _connection.BeginTransaction()) + { + using var deleteCommand = _connection.CreateCommand(); + deleteCommand.CommandText = @"delete * from `Cookies` where `Name` = 'Basic'"; + deleteCommand.Transaction = transaction; + var affected = deleteCommand.ExecuteNonQuery(); + + Assert.AreEqual(1, affected); + } + + using var verifyCommand = _connection.CreateCommand(); + verifyCommand.CommandText = @"select count(*) as `Count` from `Cookies`"; + var count = verifyCommand.ExecuteScalar(); + + Assert.AreEqual(2, count); + } + + [TestMethod] + public void Delete_rollback_explicit() + { + using (var transaction = _connection.BeginTransaction()) + { + using var deleteCommand = _connection.CreateCommand(); + deleteCommand.CommandText = @"delete * from `Cookies` where `Name` = 'Basic'"; + deleteCommand.Transaction = transaction; + var affected = deleteCommand.ExecuteNonQuery(); + + transaction.Rollback(); + + Assert.AreEqual(1, affected); + } + + using var verifyCommand = _connection.CreateCommand(); + verifyCommand.CommandText = @"select count(*) as `Count` from `Cookies`"; + var count = verifyCommand.ExecuteScalar(); + + Assert.AreEqual(2, count); + } + } +} \ No newline at end of file diff --git a/test/EFCore.Jet.FunctionalTests/TransactionJettest.cs b/test/EFCore.Jet.FunctionalTests/TransactionJetTest.cs similarity index 67% rename from test/EFCore.Jet.FunctionalTests/TransactionJettest.cs rename to test/EFCore.Jet.FunctionalTests/TransactionJetTest.cs index 90d799a..2920ec7 100644 --- a/test/EFCore.Jet.FunctionalTests/TransactionJettest.cs +++ b/test/EFCore.Jet.FunctionalTests/TransactionJetTest.cs @@ -1,9 +1,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using EntityFrameworkCore.Jet.Data; using EntityFrameworkCore.Jet.FunctionalTests.TestUtilities; using EntityFrameworkCore.Jet.Infrastructure; -using Microsoft.EntityFrameworkCore.Infrastructure; using EntityFrameworkCore.Jet.Storage.Internal; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.TestUtilities; @@ -17,9 +15,10 @@ namespace EntityFrameworkCore.Jet.FunctionalTests { } - protected override bool SnapshotSupported => true; - - protected override bool AmbientTransactionsSupported => true; + protected override bool SnapshotSupported => false; + protected override bool AmbientTransactionsSupported => false; + protected override bool DirtyReadsOccur => false; + protected override bool SavepointsSupported => false; protected override DbContext CreateContextWithConnectionString() { @@ -37,26 +36,7 @@ namespace EntityFrameworkCore.Jet.FunctionalTests public class TransactionJetFixture : TransactionFixtureBase { protected override ITestStoreFactory TestStoreFactory => JetTestStoreFactory.Instance; - - protected override void Seed(PoolableDbContext context) - { - base.Seed(context); - - context.Database.ExecuteSqlRaw("ALTER DATABASE [" + StoreName + "] SET ALLOW_SNAPSHOT_ISOLATION ON"); - context.Database.ExecuteSqlRaw("ALTER DATABASE [" + StoreName + "] SET READ_COMMITTED_SNAPSHOT ON"); - } - - public override void Reseed() - { - using (var context = CreateContext()) - { - context.Set().RemoveRange(context.Set()); - context.SaveChanges(); - - base.Seed(context); - } - } - + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) { new JetDbContextOptionsBuilder(