From 089eda83250f2ac6cdafc83cbc588d7fb6b2ae7c Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sun, 14 Aug 2022 23:32:24 +0200 Subject: [PATCH] Allow users to always use transactions with SaveChanges Obsoletes AutoTransactionsEnabled, replacing it with a new 3-valued enum. Closes #27574 --- .../Update/Internal/BatchExecutor.cs | 12 +- src/EFCore/AutoTransactionBehavior.cs | 29 ++ .../ChangeTracking/Internal/StateManager.cs | 4 +- src/EFCore/DbContext.cs | 4 +- src/EFCore/Infrastructure/DatabaseFacade.cs | 53 ++- .../DbContextPoolConfigurationSnapshot.cs | 6 +- .../TransactionTestBase.cs | 322 ++++++++++++------ .../DbContextPoolingTest.cs | 20 +- .../TransactionSqlServerTest.cs | 5 +- 9 files changed, 319 insertions(+), 136 deletions(-) create mode 100644 src/EFCore/AutoTransactionBehavior.cs diff --git a/src/EFCore.Relational/Update/Internal/BatchExecutor.cs b/src/EFCore.Relational/Update/Internal/BatchExecutor.cs index 0639a1f8703..4acdafd984c 100644 --- a/src/EFCore.Relational/Update/Internal/BatchExecutor.cs +++ b/src/EFCore.Relational/Update/Internal/BatchExecutor.cs @@ -66,12 +66,14 @@ public virtual int Execute(IEnumerable commandBatches, try { var transactionEnlistManager = connection as ITransactionEnlistmentManager; + var autoTransactionBehavior = CurrentContext.Context.Database.AutoTransactionBehavior; if (transaction == null && transactionEnlistManager?.EnlistedTransaction is null && transactionEnlistManager?.CurrentAmbientTransaction is null - && CurrentContext.Context.Database.AutoTransactionsEnabled // Don't start a transaction if we have a single batch which doesn't require a transaction (single command), for perf. - && (batch.AreMoreBatchesExpected || batch.RequiresTransaction)) + && ((autoTransactionBehavior == AutoTransactionBehavior.WhenNeeded + && (batch.AreMoreBatchesExpected || batch.RequiresTransaction)) + || autoTransactionBehavior == AutoTransactionBehavior.Always)) { transaction = connection.BeginTransaction(); beganTransaction = true; @@ -174,12 +176,14 @@ public virtual async Task ExecuteAsync( try { var transactionEnlistManager = connection as ITransactionEnlistmentManager; + var autoTransactionBehavior = CurrentContext.Context.Database.AutoTransactionBehavior; if (transaction == null && transactionEnlistManager?.EnlistedTransaction is null && transactionEnlistManager?.CurrentAmbientTransaction is null - && CurrentContext.Context.Database.AutoTransactionsEnabled // Don't start a transaction if we have a single batch which doesn't require a transaction (single command), for perf. - && (batch.AreMoreBatchesExpected || batch.RequiresTransaction)) + && ((autoTransactionBehavior == AutoTransactionBehavior.WhenNeeded + && (batch.AreMoreBatchesExpected || batch.RequiresTransaction)) + || autoTransactionBehavior == AutoTransactionBehavior.Always)) { transaction = await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); beganTransaction = true; diff --git a/src/EFCore/AutoTransactionBehavior.cs b/src/EFCore/AutoTransactionBehavior.cs new file mode 100644 index 00000000000..25d6e11db23 --- /dev/null +++ b/src/EFCore/AutoTransactionBehavior.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore; + +/// +/// Indicates whether or not a transaction will be created automatically by if a user transaction +/// wasn't created via 'BeginTransaction' or provided via 'UseTransaction'. +/// +public enum AutoTransactionBehavior +{ + /// + /// Transactions are automatically created as needed. For example, most single SQL statements are implicitly executed within a + /// transaction, and so do not require an explicit one to be created, reducing database round trips. This is the default setting. + /// + WhenNeeded, + + /// + /// Transactions are always created automatically, as long there's no user transaction. This setting may create transactions even + /// when they're not needed, adding additional database round trips which may degrade performance. + /// + Always, + + /// + /// Transactions are never created automatically. Use this options with caution, since the database could be left in an inconsistent + /// state if a failure occurs. + /// + Never +} diff --git a/src/EFCore/ChangeTracking/Internal/StateManager.cs b/src/EFCore/ChangeTracking/Internal/StateManager.cs index 98432da6569..2c08c24d8ec 100644 --- a/src/EFCore/ChangeTracking/Internal/StateManager.cs +++ b/src/EFCore/ChangeTracking/Internal/StateManager.cs @@ -1235,7 +1235,7 @@ protected virtual async Task SaveChangesAsync( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual int SaveChanges(bool acceptAllChangesOnSuccess) - => !Context.Database.AutoTransactionsEnabled + => Context.Database.AutoTransactionBehavior == AutoTransactionBehavior.Never ? SaveChanges(this, acceptAllChangesOnSuccess) : Dependencies.ExecutionStrategy.Execute( (StateManager: this, AcceptAllChangesOnSuccess: acceptAllChangesOnSuccess), @@ -1291,7 +1291,7 @@ private static int SaveChanges(StateManager stateManager, bool acceptAllChangesO public virtual Task SaveChangesAsync( bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) - => !Context.Database.AutoTransactionsEnabled + => Context.Database.AutoTransactionBehavior == AutoTransactionBehavior.Never ? SaveChangesAsync(this, acceptAllChangesOnSuccess, cancellationToken) : Dependencies.ExecutionStrategy.ExecuteAsync( (StateManager: this, AcceptAllChangesOnSuccess: acceptAllChangesOnSuccess), diff --git a/src/EFCore/DbContext.cs b/src/EFCore/DbContext.cs index fc248ee421d..172e2cfdbed 100644 --- a/src/EFCore/DbContext.cs +++ b/src/EFCore/DbContext.cs @@ -869,7 +869,7 @@ private void SetLeaseInternal(DbContextLease lease) || _configurationSnapshot.HasDatabaseConfiguration) { var database = Database; - database.AutoTransactionsEnabled = _configurationSnapshot.AutoTransactionsEnabled; + database.AutoTransactionBehavior = _configurationSnapshot.AutoTransactionBehavior; database.AutoSavepointsEnabled = _configurationSnapshot.AutoSavepointsEnabled; } @@ -917,7 +917,7 @@ void IDbContextPoolable.SnapshotConfiguration() changeDetectorEvents != null, _changeTracker?.AutoDetectChangesEnabled ?? true, _changeTracker?.QueryTrackingBehavior ?? QueryTrackingBehavior.TrackAll, - _database?.AutoTransactionsEnabled ?? true, + _database?.AutoTransactionBehavior ?? AutoTransactionBehavior.WhenNeeded, _database?.AutoSavepointsEnabled ?? true, _changeTracker?.LazyLoadingEnabled ?? true, _changeTracker?.CascadeDeleteTiming ?? CascadeTiming.Immediate, diff --git a/src/EFCore/Infrastructure/DatabaseFacade.cs b/src/EFCore/Infrastructure/DatabaseFacade.cs index 5b1b3535185..7066e76e3ad 100644 --- a/src/EFCore/Infrastructure/DatabaseFacade.cs +++ b/src/EFCore/Infrastructure/DatabaseFacade.cs @@ -376,28 +376,63 @@ public virtual IDbContextTransaction? CurrentTransaction => Dependencies.TransactionManager.CurrentTransaction; /// - /// Gets or sets a value indicating whether or not a transaction will be created - /// automatically by if none of the - /// 'BeginTransaction' or 'UseTransaction' methods have been called. + /// Gets or sets a value indicating whether or not a transaction will be created automatically by + /// if none of the 'BeginTransaction' or 'UseTransaction' methods have been called. /// /// /// - /// Setting this value to will also disable the - /// for + /// Setting this value to will also disable the for + /// /// /// /// The default value is , meaning that will always use a /// transaction when saving changes. /// /// - /// Setting this value to should only be done with caution since the database - /// could be left in a corrupted state if fails. + /// Setting this value to should only be done with caution, since the database could be left in an + /// inconsistent state if failure occurs. /// /// /// See Transactions in EF Core for more information and examples. /// /// - public virtual bool AutoTransactionsEnabled { get; set; } = true; + [Obsolete("Use EnableAutoTransactions instead")] + public virtual bool AutoTransactionsEnabled + { + get => AutoTransactionBehavior is AutoTransactionBehavior.Always or AutoTransactionBehavior.WhenNeeded; + set + { + if (value) + { + if (AutoTransactionBehavior == AutoTransactionBehavior.Never) + { + AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; + } + } + else + { + AutoTransactionBehavior = AutoTransactionBehavior.Never; + } + } + } + + /// + /// Gets or sets a value indicating whether or not a transaction will be created automatically by + /// if neither 'BeginTransaction' nor 'UseTransaction' has been called. + /// + /// + /// + /// The default setting is . + /// + /// + /// Setting this to with caution, since the database could be left in + /// an inconsistent state if failure occurs. + /// + /// + /// See Transactions in EF Core for more information and examples. + /// + /// + public virtual AutoTransactionBehavior AutoTransactionBehavior { get; set; } = AutoTransactionBehavior.WhenNeeded; /// /// Whether a transaction savepoint will be created automatically by if it is called @@ -483,7 +518,7 @@ DbContext IDatabaseFacadeDependenciesAccessor.Context /// void IResettableService.ResetState() { - AutoTransactionsEnabled = true; + AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; AutoSavepointsEnabled = true; } diff --git a/src/EFCore/Internal/DbContextPoolConfigurationSnapshot.cs b/src/EFCore/Internal/DbContextPoolConfigurationSnapshot.cs index 47a7acf1847..3b86dbde3e0 100644 --- a/src/EFCore/Internal/DbContextPoolConfigurationSnapshot.cs +++ b/src/EFCore/Internal/DbContextPoolConfigurationSnapshot.cs @@ -24,7 +24,7 @@ public DbContextPoolConfigurationSnapshot( bool hasChangeDetectorConfiguration, bool autoDetectChangesEnabled, QueryTrackingBehavior queryTrackingBehavior, - bool autoTransactionsEnabled, + AutoTransactionBehavior autoTransactionBehavior, bool autoSavepointsEnabled, bool lazyLoadingEnabled, CascadeTiming cascadeDeleteTiming, @@ -47,8 +47,8 @@ public DbContextPoolConfigurationSnapshot( HasChangeDetectorConfiguration = hasChangeDetectorConfiguration; AutoDetectChangesEnabled = autoDetectChangesEnabled; QueryTrackingBehavior = queryTrackingBehavior; - AutoTransactionsEnabled = autoTransactionsEnabled; AutoSavepointsEnabled = autoSavepointsEnabled; + AutoTransactionBehavior = autoTransactionBehavior; LazyLoadingEnabled = lazyLoadingEnabled; CascadeDeleteTiming = cascadeDeleteTiming; DeleteOrphansTiming = deleteOrphansTiming; @@ -143,7 +143,7 @@ public DbContextPoolConfigurationSnapshot( /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public bool AutoTransactionsEnabled { get; } + public AutoTransactionBehavior AutoTransactionBehavior { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/test/EFCore.Relational.Specification.Tests/TransactionTestBase.cs b/test/EFCore.Relational.Specification.Tests/TransactionTestBase.cs index 1ed6dc8f771..ff5782e7525 100644 --- a/test/EFCore.Relational.Specification.Tests/TransactionTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/TransactionTestBase.cs @@ -31,7 +31,50 @@ protected TransactionTestBase(TFixture fixture) [ConditionalTheory] [InlineData(true)] [InlineData(false)] - public virtual async Task SaveChanges_can_be_used_with_no_transaction(bool async) + public virtual async Task SaveChanges_can_be_used_with_AutoTransactionBehavior_Never(bool async) + { + using (var context = CreateContext()) + { + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; + + context.Add( + new TransactionCustomer { Id = -77, Name = "Bobble" }); + + context.Entry(context.Set().OrderBy(c => c.Id).Last()).State = EntityState.Added; + + if (async) + { + await Assert.ThrowsAsync(() => context.SaveChangesAsync()); + } + else + { + Assert.Throws(() => context.SaveChanges()); + } + + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; + } + + Assert.DoesNotContain(Fixture.ListLoggerFactory.Log, l => l.Id == RelationalEventId.TransactionStarted); + Assert.DoesNotContain(Fixture.ListLoggerFactory.Log, l => l.Id == RelationalEventId.TransactionCommitted); + + using (var context = CreateContext()) + { + Assert.Equal( + new List + { + -77, + 1, + 2, + }, + context.Set().OrderBy(c => c.Id).Select(e => e.Id).ToList()); + } + } + +#pragma warning disable CS0618 // AutoTransactionsEnabled is obsolete + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task SaveChanges_can_be_used_with_AutoTransactionsEnabled_false(bool async) { using (var context = CreateContext()) { @@ -51,9 +94,12 @@ public virtual async Task SaveChanges_can_be_used_with_no_transaction(bool async Assert.Throws(() => context.SaveChanges()); } - context.Database.AutoTransactionsEnabled = true; + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; } + Assert.DoesNotContain(Fixture.ListLoggerFactory.Log, l => l.Id == RelationalEventId.TransactionStarted); + Assert.DoesNotContain(Fixture.ListLoggerFactory.Log, l => l.Id == RelationalEventId.TransactionCommitted); + using (var context = CreateContext()) { Assert.Equal( @@ -66,15 +112,56 @@ public virtual async Task SaveChanges_can_be_used_with_no_transaction(bool async context.Set().OrderBy(c => c.Id).Select(e => e.Id).ToList()); } } +#pragma warning restore CS0618 + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task SaveChanges_can_be_used_with_AutoTransactionBehavior_Always(bool async) + { + using (var context = CreateContext()) + { + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always; + + context.Add( + new TransactionCustomer { Id = -77, Name = "Bobble" }); + + context.Entry(context.Set().OrderBy(c => c.Id).Last()).State = EntityState.Added; + + if (async) + { + await Assert.ThrowsAsync(() => context.SaveChangesAsync()); + } + else + { + Assert.Throws(() => context.SaveChanges()); + } + + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; + } + + Assert.Contains(Fixture.ListLoggerFactory.Log, l => l.Id == RelationalEventId.TransactionStarted); + + using (var context = CreateContext()) + { + Assert.Equal( + new List + { + 1, + 2, + }, + context.Set().OrderBy(c => c.Id).Select(e => e.Id).ToList()); + } + } [ConditionalTheory] [InlineData(true)] [InlineData(false)] - public virtual async Task SaveChanges_implicitly_starts_transaction(bool async) + public virtual async Task SaveChanges_implicitly_starts_transaction_when_needed(bool async) { using (var context = CreateContext()) { - Assert.True(context.Database.AutoTransactionsEnabled); + Assert.Equal(AutoTransactionBehavior.WhenNeeded, context.Database.AutoTransactionBehavior); context.Add( new TransactionCustomer { Id = 77, Name = "Bobble" }); @@ -95,18 +182,20 @@ public virtual async Task SaveChanges_implicitly_starts_transaction(bool async) } [ConditionalTheory] - [InlineData(true, true)] - [InlineData(true, false)] - [InlineData(false, true)] - [InlineData(false, false)] - public virtual async Task SaveChanges_uses_enlisted_transaction(bool async, bool autoTransactionsEnabled) + [InlineData(true, AutoTransactionBehavior.WhenNeeded)] + [InlineData(true, AutoTransactionBehavior.Never)] + [InlineData(true, AutoTransactionBehavior.Always)] + [InlineData(false, AutoTransactionBehavior.WhenNeeded)] + [InlineData(false, AutoTransactionBehavior.Never)] + [InlineData(false, AutoTransactionBehavior.Always)] + public virtual async Task SaveChanges_uses_enlisted_transaction(bool async, AutoTransactionBehavior autoTransactionBehavior) { using (var transaction = new CommittableTransaction(TimeSpan.FromMinutes(10))) { using (var context = CreateContext()) { context.Database.EnlistTransaction(transaction); - context.Database.AutoTransactionsEnabled = autoTransactionsEnabled; + context.Database.AutoTransactionBehavior = autoTransactionBehavior; context.Add( new TransactionCustomer { Id = -77, Name = "Bobble" }); @@ -122,7 +211,7 @@ public virtual async Task SaveChanges_uses_enlisted_transaction(bool async, bool Assert.Throws(() => context.SaveChanges()); } - context.Database.AutoTransactionsEnabled = true; + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; } if (AmbientTransactionsSupported) @@ -138,7 +227,7 @@ public virtual async Task SaveChanges_uses_enlisted_transaction(bool async, bool RelationalResources.LogAmbientTransaction(new TestLogger()).GenerateMessage(), Fixture.ListLoggerFactory.Log.First().Message); - if (!autoTransactionsEnabled) + if (autoTransactionBehavior == AutoTransactionBehavior.Never) { using var context = CreateContext(); context.Entry(context.Set().Single(c => c.Id == -77)).State = EntityState.Deleted; @@ -159,11 +248,14 @@ public virtual async Task SaveChanges_uses_enlisted_transaction(bool async, bool } [ConditionalTheory] - [InlineData(true, true)] - [InlineData(true, false)] - [InlineData(false, true)] - [InlineData(false, false)] - public virtual async Task SaveChanges_uses_enlisted_transaction_after_connection_closed(bool async, bool autoTransactionsEnabled) + [InlineData(true, AutoTransactionBehavior.WhenNeeded)] + [InlineData(true, AutoTransactionBehavior.Never)] + [InlineData(true, AutoTransactionBehavior.Always)] + [InlineData(false, AutoTransactionBehavior.WhenNeeded)] + [InlineData(false, AutoTransactionBehavior.Never)] + [InlineData(false, AutoTransactionBehavior.Always)] + public virtual async Task SaveChanges_uses_enlisted_transaction_after_connection_closed( + bool async, AutoTransactionBehavior autoTransactionBehavior) { if (!AmbientTransactionsSupported) { @@ -175,14 +267,14 @@ public virtual async Task SaveChanges_uses_enlisted_transaction_after_connection using (var transaction = new CommittableTransaction(TimeSpan.FromMinutes(10))) { context.Database.EnlistTransaction(transaction); - context.Database.AutoTransactionsEnabled = autoTransactionsEnabled; + context.Database.AutoTransactionBehavior = autoTransactionBehavior; context.Add( new TransactionCustomer { Id = 77, Name = "Bobble" }); context.Entry(context.Set().OrderBy(c => c.Id).Last()).State = EntityState.Added; - context.Database.AutoTransactionsEnabled = true; + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; } using (var transaction = new CommittableTransaction(TimeSpan.FromMinutes(10))) @@ -206,11 +298,14 @@ public virtual async Task SaveChanges_uses_enlisted_transaction_after_connection } [ConditionalTheory] - [InlineData(true, true)] - [InlineData(true, false)] - [InlineData(false, true)] - [InlineData(false, false)] - public virtual async Task SaveChanges_uses_enlisted_transaction_connectionString(bool async, bool autoTransactionsEnabled) + [InlineData(true, AutoTransactionBehavior.WhenNeeded)] + [InlineData(true, AutoTransactionBehavior.Never)] + [InlineData(true, AutoTransactionBehavior.Always)] + [InlineData(false, AutoTransactionBehavior.WhenNeeded)] + [InlineData(false, AutoTransactionBehavior.Never)] + [InlineData(false, AutoTransactionBehavior.Always)] + public virtual async Task SaveChanges_uses_enlisted_transaction_connectionString( + bool async, AutoTransactionBehavior autoTransactionBehavior) { if (!AmbientTransactionsSupported) { @@ -222,7 +317,7 @@ public virtual async Task SaveChanges_uses_enlisted_transaction_connectionString using var context = CreateContextWithConnectionString(); context.Database.OpenConnection(); context.Database.EnlistTransaction(transaction); - context.Database.AutoTransactionsEnabled = autoTransactionsEnabled; + context.Database.AutoTransactionBehavior = autoTransactionBehavior; context.Add( new TransactionCustomer { Id = 77, Name = "Bobble" }); @@ -240,18 +335,20 @@ public virtual async Task SaveChanges_uses_enlisted_transaction_connectionString context.Database.CloseConnection(); - context.Database.AutoTransactionsEnabled = true; + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; } AssertStoreInitialState(); } [ConditionalTheory] - [InlineData(true, true)] - [InlineData(true, false)] - [InlineData(false, true)] - [InlineData(false, false)] - public virtual async Task SaveChanges_uses_ambient_transaction(bool async, bool autoTransactionsEnabled) + [InlineData(true, AutoTransactionBehavior.WhenNeeded)] + [InlineData(true, AutoTransactionBehavior.Never)] + [InlineData(true, AutoTransactionBehavior.Always)] + [InlineData(false, AutoTransactionBehavior.WhenNeeded)] + [InlineData(false, AutoTransactionBehavior.Never)] + [InlineData(false, AutoTransactionBehavior.Always)] + public virtual async Task SaveChanges_uses_ambient_transaction(bool async, AutoTransactionBehavior autoTransactionBehavior) { if (TestStore.ConnectionState == ConnectionState.Closed) { @@ -262,7 +359,7 @@ public virtual async Task SaveChanges_uses_ambient_transaction(bool async, bool { using (var context = CreateContext()) { - context.Database.AutoTransactionsEnabled = autoTransactionsEnabled; + context.Database.AutoTransactionBehavior = autoTransactionBehavior; context.Add( new TransactionCustomer { Id = -77, Name = "Bobble" }); @@ -278,7 +375,7 @@ public virtual async Task SaveChanges_uses_ambient_transaction(bool async, bool Assert.Throws(() => context.SaveChanges()); } - context.Database.AutoTransactionsEnabled = true; + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; } if (AmbientTransactionsSupported) @@ -312,11 +409,13 @@ public virtual async Task SaveChanges_uses_ambient_transaction(bool async, bool } [ConditionalTheory] - [InlineData(true, true)] - [InlineData(true, false)] - [InlineData(false, true)] - [InlineData(false, false)] - public virtual async Task SaveChanges_uses_ambient_transaction_with_connectionString(bool async, bool autoTransactionsEnabled) + [InlineData(true, AutoTransactionBehavior.WhenNeeded)] + [InlineData(true, AutoTransactionBehavior.Never)] + [InlineData(true, AutoTransactionBehavior.Always)] + [InlineData(false, AutoTransactionBehavior.WhenNeeded)] + [InlineData(false, AutoTransactionBehavior.Never)] + [InlineData(false, AutoTransactionBehavior.Always)] + public virtual async Task SaveChanges_uses_ambient_transaction_with_connectionString(bool async, AutoTransactionBehavior autoTransactionBehavior) { if (!AmbientTransactionsSupported) { @@ -328,7 +427,7 @@ public virtual async Task SaveChanges_uses_ambient_transaction_with_connectionSt { using (TestUtilities.TestStore.CreateTransactionScope()) { - context.Database.AutoTransactionsEnabled = autoTransactionsEnabled; + context.Database.AutoTransactionBehavior = autoTransactionBehavior; connection = context.Database.GetDbConnection(); Assert.Equal(ConnectionState.Closed, connection.State); @@ -349,7 +448,7 @@ public virtual async Task SaveChanges_uses_ambient_transaction_with_connectionSt Assert.Equal(ConnectionState.Closed, connection.State); - context.Database.AutoTransactionsEnabled = true; + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; } } @@ -556,15 +655,18 @@ public virtual async Task SaveChanges_does_not_close_connection_opened_by_user(b } [ConditionalTheory] - [InlineData(true, true)] - [InlineData(true, false)] - [InlineData(false, true)] - [InlineData(false, false)] - public virtual async Task SaveChanges_uses_explicit_transaction_without_committing(bool async, bool autoTransaction) + [InlineData(true, AutoTransactionBehavior.WhenNeeded)] + [InlineData(true, AutoTransactionBehavior.Never)] + [InlineData(true, AutoTransactionBehavior.Always)] + [InlineData(false, AutoTransactionBehavior.WhenNeeded)] + [InlineData(false, AutoTransactionBehavior.Never)] + [InlineData(false, AutoTransactionBehavior.Always)] + public virtual async Task SaveChanges_uses_explicit_transaction_without_committing( + bool async, AutoTransactionBehavior autoTransactionBehavior) { using (var context = CreateContext()) { - context.Database.AutoTransactionsEnabled = autoTransaction; + context.Database.AutoTransactionBehavior = autoTransactionBehavior; var firstEntry = context.Entry(context.Set().OrderBy(c => c.Id).First()); firstEntry.State = EntityState.Deleted; @@ -586,24 +688,26 @@ public virtual async Task SaveChanges_uses_explicit_transaction_without_committi Assert.Equal(EntityState.Detached, firstEntry.State); - context.Database.AutoTransactionsEnabled = true; + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; } AssertStoreInitialState(); } [ConditionalTheory] - [InlineData(true, true)] - [InlineData(true, false)] - [InlineData(false, true)] - [InlineData(false, false)] + [InlineData(true, AutoTransactionBehavior.WhenNeeded)] + [InlineData(true, AutoTransactionBehavior.Never)] + [InlineData(true, AutoTransactionBehavior.Always)] + [InlineData(false, AutoTransactionBehavior.WhenNeeded)] + [InlineData(false, AutoTransactionBehavior.Never)] + [InlineData(false, AutoTransactionBehavior.Always)] public virtual async Task SaveChanges_false_uses_explicit_transaction_without_committing_or_accepting_changes( bool async, - bool autoTransaction) + AutoTransactionBehavior autoTransactionBehavior) { using (var context = CreateContext()) { - context.Database.AutoTransactionsEnabled = autoTransaction; + context.Database.AutoTransactionBehavior = autoTransactionBehavior; var firstEntry = context.Entry(context.Set().OrderBy(c => c.Id).First()); firstEntry.State = EntityState.Deleted; @@ -629,22 +733,25 @@ public virtual async Task SaveChanges_false_uses_explicit_transaction_without_co Assert.Equal(EntityState.Detached, firstEntry.State); - context.Database.AutoTransactionsEnabled = true; + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; } AssertStoreInitialState(); } [ConditionalTheory] - [InlineData(true, true)] - [InlineData(true, false)] - [InlineData(false, true)] - [InlineData(false, false)] - public virtual async Task SaveChanges_uses_explicit_transaction_with_failure_behavior(bool async, bool autoTransaction) + [InlineData(true, AutoTransactionBehavior.WhenNeeded)] + [InlineData(true, AutoTransactionBehavior.Never)] + [InlineData(true, AutoTransactionBehavior.Always)] + [InlineData(false, AutoTransactionBehavior.WhenNeeded)] + [InlineData(false, AutoTransactionBehavior.Never)] + [InlineData(false, AutoTransactionBehavior.Always)] + public virtual async Task SaveChanges_uses_explicit_transaction_with_failure_behavior( + bool async, AutoTransactionBehavior autoTransactionBehavior) { using (var context = CreateContext()) { - context.Database.AutoTransactionsEnabled = autoTransaction; + context.Database.AutoTransactionBehavior = autoTransactionBehavior; using var transaction = context.Database.BeginTransaction(); @@ -692,7 +799,7 @@ public virtual async Task SaveChanges_uses_explicit_transaction_with_failure_beh transaction.Commit(); - context.Database.AutoTransactionsEnabled = true; + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; } if (SavepointsSupported) @@ -704,13 +811,14 @@ public virtual async Task SaveChanges_uses_explicit_transaction_with_failure_beh } [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] - public virtual async Task RelationalTransaction_can_be_committed(bool autoTransaction) + [InlineData(AutoTransactionBehavior.WhenNeeded)] + [InlineData(AutoTransactionBehavior.Never)] + [InlineData(AutoTransactionBehavior.Always)] + public virtual async Task RelationalTransaction_can_be_committed(AutoTransactionBehavior autoTransactionBehavior) { using (var context = CreateContext()) { - context.Database.AutoTransactionsEnabled = autoTransaction; + context.Database.AutoTransactionBehavior = autoTransactionBehavior; using (var transaction = await context.Database.BeginTransactionAsync()) { @@ -719,7 +827,7 @@ public virtual async Task RelationalTransaction_can_be_committed(bool autoTransa transaction.Commit(); } - context.Database.AutoTransactionsEnabled = true; + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; } using (var context = CreateContext()) @@ -729,13 +837,14 @@ public virtual async Task RelationalTransaction_can_be_committed(bool autoTransa } [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] - public virtual async Task RelationalTransaction_can_be_committed_from_context(bool autoTransaction) + [InlineData(AutoTransactionBehavior.WhenNeeded)] + [InlineData(AutoTransactionBehavior.Never)] + [InlineData(AutoTransactionBehavior.Always)] + public virtual async Task RelationalTransaction_can_be_committed_from_context(AutoTransactionBehavior autoTransactionBehavior) { using (var context = CreateContext()) { - context.Database.AutoTransactionsEnabled = autoTransaction; + context.Database.AutoTransactionBehavior = autoTransactionBehavior; using (await context.Database.BeginTransactionAsync()) { @@ -744,7 +853,7 @@ public virtual async Task RelationalTransaction_can_be_committed_from_context(bo context.Database.CommitTransaction(); } - context.Database.AutoTransactionsEnabled = true; + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; } using (var context = CreateContext()) @@ -754,12 +863,13 @@ public virtual async Task RelationalTransaction_can_be_committed_from_context(bo } [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] - public virtual async Task RelationalTransaction_can_be_rolled_back(bool autoTransaction) + [InlineData(AutoTransactionBehavior.WhenNeeded)] + [InlineData(AutoTransactionBehavior.Never)] + [InlineData(AutoTransactionBehavior.Always)] + public virtual async Task RelationalTransaction_can_be_rolled_back(AutoTransactionBehavior autoTransactionBehavior) { using var context = CreateContext(); - context.Database.AutoTransactionsEnabled = autoTransaction; + context.Database.AutoTransactionBehavior = autoTransactionBehavior; using (var transaction = await context.Database.BeginTransactionAsync()) { @@ -770,16 +880,17 @@ public virtual async Task RelationalTransaction_can_be_rolled_back(bool autoTran AssertStoreInitialState(); } - context.Database.AutoTransactionsEnabled = true; + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; } [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] - public virtual async Task RelationalTransaction_can_be_rolled_back_from_context(bool autoTransaction) + [InlineData(AutoTransactionBehavior.WhenNeeded)] + [InlineData(AutoTransactionBehavior.Never)] + [InlineData(AutoTransactionBehavior.Always)] + public virtual async Task RelationalTransaction_can_be_rolled_back_from_context(AutoTransactionBehavior autoTransactionBehavior) { using var context = CreateContext(); - context.Database.AutoTransactionsEnabled = autoTransaction; + context.Database.AutoTransactionBehavior = autoTransactionBehavior; using (await context.Database.BeginTransactionAsync()) { @@ -790,16 +901,17 @@ public virtual async Task RelationalTransaction_can_be_rolled_back_from_context( AssertStoreInitialState(); } - context.Database.AutoTransactionsEnabled = true; + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; } [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] - public virtual void Query_uses_explicit_transaction(bool autoTransaction) + [InlineData(AutoTransactionBehavior.WhenNeeded)] + [InlineData(AutoTransactionBehavior.Never)] + [InlineData(AutoTransactionBehavior.Always)] + public virtual void Query_uses_explicit_transaction(AutoTransactionBehavior autoTransactionBehavior) { using var context = CreateContext(); - context.Database.AutoTransactionsEnabled = autoTransaction; + context.Database.AutoTransactionBehavior = autoTransactionBehavior; using (var transaction = context.Database.BeginTransaction()) { @@ -808,7 +920,7 @@ public virtual void Query_uses_explicit_transaction(bool autoTransaction) using (var innerContext = CreateContextWithConnectionString()) { - innerContext.Database.AutoTransactionsEnabled = autoTransaction; + innerContext.Database.AutoTransactionBehavior = autoTransactionBehavior; if (DirtyReadsOccur) { @@ -826,30 +938,31 @@ public virtual void Query_uses_explicit_transaction(bool autoTransaction) } } - innerContext.Database.AutoTransactionsEnabled = true; + innerContext.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; } using (var innerContext = CreateContext()) { - innerContext.Database.AutoTransactionsEnabled = autoTransaction; + innerContext.Database.AutoTransactionBehavior = autoTransactionBehavior; innerContext.Database.UseTransaction(transaction.GetDbTransaction()); Assert.Equal(Customers.Count - 1, innerContext.Set().Count()); - innerContext.Database.AutoTransactionsEnabled = true; + innerContext.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; } } - context.Database.AutoTransactionsEnabled = true; + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; } [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] - public virtual async Task QueryAsync_uses_explicit_transaction(bool autoTransaction) + [InlineData(AutoTransactionBehavior.WhenNeeded)] + [InlineData(AutoTransactionBehavior.Never)] + [InlineData(AutoTransactionBehavior.Always)] + public virtual async Task QueryAsync_uses_explicit_transaction(AutoTransactionBehavior autoTransactionBehavior) { using var context = CreateContext(); - context.Database.AutoTransactionsEnabled = autoTransaction; + context.Database.AutoTransactionBehavior = autoTransactionBehavior; using (var transaction = await context.Database.BeginTransactionAsync()) { @@ -858,7 +971,7 @@ public virtual async Task QueryAsync_uses_explicit_transaction(bool autoTransact using (var innerContext = CreateContextWithConnectionString()) { - innerContext.Database.AutoTransactionsEnabled = autoTransaction; + innerContext.Database.AutoTransactionBehavior = autoTransactionBehavior; if (DirtyReadsOccur) { @@ -876,39 +989,40 @@ public virtual async Task QueryAsync_uses_explicit_transaction(bool autoTransact } } - innerContext.Database.AutoTransactionsEnabled = true; + innerContext.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; } using (var innerContext = CreateContext()) { - innerContext.Database.AutoTransactionsEnabled = autoTransaction; + innerContext.Database.AutoTransactionBehavior = autoTransactionBehavior; innerContext.Database.UseTransaction(transaction.GetDbTransaction()); Assert.Equal(Customers.Count - 1, await innerContext.Set().CountAsync()); - innerContext.Database.AutoTransactionsEnabled = true; + innerContext.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; } } - context.Database.AutoTransactionsEnabled = true; + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; } [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] - public virtual async Task Can_use_open_connection_with_started_transaction(bool autoTransaction) + [InlineData(AutoTransactionBehavior.WhenNeeded)] + [InlineData(AutoTransactionBehavior.Never)] + [InlineData(AutoTransactionBehavior.Always)] + public virtual async Task Can_use_open_connection_with_started_transaction(AutoTransactionBehavior autoTransactionBehavior) { using (var transaction = TestStore.BeginTransaction()) { using var context = CreateContext(); - context.Database.AutoTransactionsEnabled = autoTransaction; + context.Database.AutoTransactionBehavior = autoTransactionBehavior; context.Database.UseTransaction(transaction); context.Entry(context.Set().OrderBy(c => c.Id).First()).State = EntityState.Deleted; await context.SaveChangesAsync(); - context.Database.AutoTransactionsEnabled = true; + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; } AssertStoreInitialState(); diff --git a/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs b/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs index 0212257e1c8..c56a3441e4b 100644 --- a/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs @@ -109,7 +109,7 @@ public PooledContext(DbContextOptions options) ChangeTracker.AutoDetectChangesEnabled = false; ChangeTracker.LazyLoadingEnabled = false; - Database.AutoTransactionsEnabled = false; + Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; Database.AutoSavepointsEnabled = false; ChangeTracker.CascadeDeleteTiming = CascadeTiming.Never; ChangeTracker.DeleteOrphansTiming = CascadeTiming.Never; @@ -728,7 +728,7 @@ public async Task Context_configuration_is_reset(bool useInterface, bool async) context1.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; context1.ChangeTracker.CascadeDeleteTiming = CascadeTiming.Immediate; context1.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Immediate; - context1.Database.AutoTransactionsEnabled = true; + context1.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; context1.Database.AutoSavepointsEnabled = true; context1.Database.SetCommandTimeout(1); context1.ChangeTracker.Tracking += ChangeTracker_OnTracking; @@ -759,7 +759,7 @@ public async Task Context_configuration_is_reset(bool useInterface, bool async) Assert.Equal(QueryTrackingBehavior.TrackAll, context2.ChangeTracker.QueryTrackingBehavior); Assert.Equal(CascadeTiming.Never, context2.ChangeTracker.CascadeDeleteTiming); Assert.Equal(CascadeTiming.Never, context2.ChangeTracker.DeleteOrphansTiming); - Assert.False(context2.Database.AutoTransactionsEnabled); + Assert.Equal(AutoTransactionBehavior.Never, context2.Database.AutoTransactionBehavior); Assert.False(context2.Database.AutoSavepointsEnabled); Assert.Null(context1.Database.GetCommandTimeout()); @@ -826,7 +826,7 @@ public async Task Context_configuration_is_reset_with_factory(bool async, bool w context1.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; context1.ChangeTracker.CascadeDeleteTiming = CascadeTiming.Immediate; context1.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Immediate; - context1.Database.AutoTransactionsEnabled = true; + context1.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; context1.Database.AutoSavepointsEnabled = true; context1.ChangeTracker.Tracking += ChangeTracker_OnTracking; context1.ChangeTracker.Tracked += ChangeTracker_OnTracked; @@ -878,7 +878,7 @@ public void Change_tracker_can_be_cleared_without_resetting_context_config() context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; context.ChangeTracker.CascadeDeleteTiming = CascadeTiming.Immediate; context.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Immediate; - context.Database.AutoTransactionsEnabled = true; + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; context.Database.AutoSavepointsEnabled = true; context.ChangeTracker.Tracking += ChangeTracker_OnTracking; context.ChangeTracker.Tracked += ChangeTracker_OnTracked; @@ -899,7 +899,7 @@ public void Change_tracker_can_be_cleared_without_resetting_context_config() Assert.Equal(QueryTrackingBehavior.NoTracking, context.ChangeTracker.QueryTrackingBehavior); Assert.Equal(CascadeTiming.Immediate, context.ChangeTracker.CascadeDeleteTiming); Assert.Equal(CascadeTiming.Immediate, context.ChangeTracker.DeleteOrphansTiming); - Assert.True(context.Database.AutoTransactionsEnabled); + Assert.Equal(AutoTransactionBehavior.WhenNeeded, context.Database.AutoTransactionBehavior); Assert.True(context.Database.AutoSavepointsEnabled); Assert.False(_changeTracker_OnTracking); @@ -1005,7 +1005,7 @@ public async Task Default_Context_configuration_is_reset(bool async) context1!.ChangeTracker.AutoDetectChangesEnabled = false; context1.ChangeTracker.LazyLoadingEnabled = false; context1.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - context1.Database.AutoTransactionsEnabled = false; + context1.Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; context1.Database.AutoSavepointsEnabled = false; context1.ChangeTracker.CascadeDeleteTiming = CascadeTiming.Immediate; context1.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Immediate; @@ -1024,7 +1024,7 @@ public async Task Default_Context_configuration_is_reset(bool async) Assert.Equal(QueryTrackingBehavior.TrackAll, context2.ChangeTracker.QueryTrackingBehavior); Assert.Equal(CascadeTiming.Immediate, context2.ChangeTracker.CascadeDeleteTiming); Assert.Equal(CascadeTiming.Immediate, context2.ChangeTracker.DeleteOrphansTiming); - Assert.True(context2.Database.AutoTransactionsEnabled); + Assert.Equal(AutoTransactionBehavior.WhenNeeded, context2.Database.AutoTransactionBehavior); Assert.True(context2.Database.AutoSavepointsEnabled); } @@ -1042,7 +1042,7 @@ public async Task Default_Context_configuration_is_reset_with_factory(bool async context1.ChangeTracker.AutoDetectChangesEnabled = false; context1.ChangeTracker.LazyLoadingEnabled = false; context1.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - context1.Database.AutoTransactionsEnabled = false; + context1.Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; context1.Database.AutoSavepointsEnabled = false; context1.ChangeTracker.CascadeDeleteTiming = CascadeTiming.Immediate; context1.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Immediate; @@ -1058,7 +1058,7 @@ public async Task Default_Context_configuration_is_reset_with_factory(bool async Assert.Equal(QueryTrackingBehavior.TrackAll, context2.ChangeTracker.QueryTrackingBehavior); Assert.Equal(CascadeTiming.Immediate, context2.ChangeTracker.CascadeDeleteTiming); Assert.Equal(CascadeTiming.Immediate, context2.ChangeTracker.DeleteOrphansTiming); - Assert.True(context2.Database.AutoTransactionsEnabled); + Assert.Equal(AutoTransactionBehavior.WhenNeeded, context2.Database.AutoTransactionBehavior); Assert.True(context2.Database.AutoSavepointsEnabled); } diff --git a/test/EFCore.SqlServer.FunctionalTests/TransactionSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/TransactionSqlServerTest.cs index 1e19295f1c6..b0f1d5cb2a7 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TransactionSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TransactionSqlServerTest.cs @@ -26,10 +26,11 @@ public override Task Savepoint_can_be_released(bool async) => Task.CompletedTask; // Test relies on savepoints, which are disabled when MARS is enabled - public override Task SaveChanges_uses_explicit_transaction_with_failure_behavior(bool async, bool autoTransaction) + public override Task SaveChanges_uses_explicit_transaction_with_failure_behavior( + bool async, AutoTransactionBehavior autoTransactionBehavior) => new SqlConnectionStringBuilder(TestStore.ConnectionString).MultipleActiveResultSets ? Task.CompletedTask - : base.SaveChanges_uses_explicit_transaction_with_failure_behavior(async, autoTransaction); + : base.SaveChanges_uses_explicit_transaction_with_failure_behavior(async, autoTransactionBehavior); [ConditionalTheory] [InlineData(true)]