diff --git a/src/EFCore.Relational/Diagnostics/RelationalEventId.cs b/src/EFCore.Relational/Diagnostics/RelationalEventId.cs index cabde564e20..c05c73c46a3 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalEventId.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalEventId.cs @@ -80,6 +80,7 @@ private enum Id PendingModelChangesWarning, NonTransactionalMigrationOperationWarning, AcquiringMigrationLock, + MigrationsUserTransactionWarning, // Query events QueryClientEvaluationWarning = CoreEventId.RelationalBaseId + 500, @@ -764,6 +765,19 @@ private static EventId MakeMigrationsId(Id id) /// public static readonly EventId AcquiringMigrationLock = MakeMigrationsId(Id.AcquiringMigrationLock); + /// + /// A migration lock is being acquired. + /// + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId MigrationsUserTransactionWarning = MakeMigrationsId(Id.MigrationsUserTransactionWarning); + private static readonly string _queryPrefix = DbLoggerCategory.Query.Name + "."; private static EventId MakeQueryId(Id id) diff --git a/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs b/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs index a8afec569e1..177b7e90bba 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs @@ -2425,6 +2425,36 @@ private static string AcquiringMigrationLock(EventDefinitionBase definition, Eve return d.GenerateMessage(); } + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + public static void MigrationsUserTransactionWarning( + this IDiagnosticsLogger diagnostics) + { + var definition = RelationalResources.LogMigrationsUserTransaction(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics); + } + + if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = new EventData( + definition, + MigrationsUserTransactionWarning); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + } + } + + private static string MigrationsUserTransactionWarning(EventDefinitionBase definition, EventData payload) + { + var d = (EventDefinition)definition; + return d.GenerateMessage(); + } + /// /// Logs for the event. /// diff --git a/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs b/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs index 9b57a07998e..762172c8944 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs @@ -367,6 +367,15 @@ public abstract class RelationalLoggingDefinitions : LoggingDefinitions [EntityFrameworkInternal] public EventDefinitionBase? LogAcquiringMigrationLock; + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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. + /// + [EntityFrameworkInternal] + public EventDefinitionBase? LogMigrationsUserTransactionWarning; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.Relational/Migrations/HistoryRepository.cs b/src/EFCore.Relational/Migrations/HistoryRepository.cs index 78dc3b24013..bfb565786c0 100644 --- a/src/EFCore.Relational/Migrations/HistoryRepository.cs +++ b/src/EFCore.Relational/Migrations/HistoryRepository.cs @@ -47,6 +47,14 @@ protected HistoryRepository(HistoryRepositoryDependencies dependencies) TableSchema = relationalOptions.MigrationsHistoryTableSchema; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 abstract LockReleaseBehavior LockReleaseBehavior { get; } + /// /// Relational provider-specific dependencies for this service. /// @@ -120,14 +128,15 @@ protected virtual string ProductVersionColumnName /// /// if the table already exists, otherwise. public virtual bool Exists() - => InterpretExistsResult( - Dependencies.RawSqlCommandBuilder.Build(ExistsSql).ExecuteScalar( - new RelationalCommandParameterObject( - Dependencies.Connection, - null, - null, - Dependencies.CurrentContext.Context, - Dependencies.CommandLogger, CommandSource.Migrations))); + => Dependencies.DatabaseCreator.Exists() + && InterpretExistsResult( + Dependencies.RawSqlCommandBuilder.Build(ExistsSql).ExecuteScalar( + new RelationalCommandParameterObject( + Dependencies.Connection, + null, + null, + Dependencies.CurrentContext.Context, + Dependencies.CommandLogger, CommandSource.Migrations))); /// /// Checks whether or not the history table exists. @@ -139,15 +148,16 @@ public virtual bool Exists() /// /// If the is canceled. public virtual async Task ExistsAsync(CancellationToken cancellationToken = default) - => InterpretExistsResult( - await Dependencies.RawSqlCommandBuilder.Build(ExistsSql).ExecuteScalarAsync( - new RelationalCommandParameterObject( - Dependencies.Connection, - null, - null, - Dependencies.CurrentContext.Context, - Dependencies.CommandLogger, CommandSource.Migrations), - cancellationToken).ConfigureAwait(false)); + => await Dependencies.DatabaseCreator.ExistsAsync(cancellationToken).ConfigureAwait(false) + && InterpretExistsResult( + await Dependencies.RawSqlCommandBuilder.Build(ExistsSql).ExecuteScalarAsync( + new RelationalCommandParameterObject( + Dependencies.Connection, + null, + null, + Dependencies.CurrentContext.Context, + Dependencies.CommandLogger, CommandSource.Migrations), + cancellationToken).ConfigureAwait(false)); /// /// Interprets the result of executing . @@ -173,13 +183,15 @@ public virtual string GetCreateScript() /// Creates the history table. /// public virtual void Create() - => Dependencies.MigrationCommandExecutor.ExecuteNonQuery(GetCreateCommands(), Dependencies.Connection); + => Dependencies.MigrationCommandExecutor.ExecuteNonQuery( + GetCreateCommands(), Dependencies.Connection, new MigrationExecutionState(), commitTransaction: true); /// /// Creates the history table. /// public virtual Task CreateAsync(CancellationToken cancellationToken = default) - => Dependencies.MigrationCommandExecutor.ExecuteNonQueryAsync(GetCreateCommands(), Dependencies.Connection, cancellationToken); + => Dependencies.MigrationCommandExecutor.ExecuteNonQueryAsync( + GetCreateCommands(), Dependencies.Connection, new MigrationExecutionState(), commitTransaction: true, cancellationToken: cancellationToken); /// /// Returns the migration commands that will create the history table. @@ -194,19 +206,37 @@ protected virtual IReadOnlyList GetCreateCommands() return commandList; } + bool IHistoryRepository.CreateIfNotExists() + => Dependencies.MigrationCommandExecutor.ExecuteNonQuery( + GetCreateIfNotExistsCommands(), Dependencies.Connection, new MigrationExecutionState(), commitTransaction: true) + != 0; + + async Task IHistoryRepository.CreateIfNotExistsAsync(CancellationToken cancellationToken) + => (await Dependencies.MigrationCommandExecutor.ExecuteNonQueryAsync( + GetCreateIfNotExistsCommands(), Dependencies.Connection, new MigrationExecutionState(), commitTransaction: true, cancellationToken: cancellationToken).ConfigureAwait(false)) + != 0; + + private IReadOnlyList GetCreateIfNotExistsCommands() + => Dependencies.MigrationsSqlGenerator.Generate([new SqlOperation + { + Sql = GetCreateIfNotExistsScript(), + SuppressTransaction = true + }]); + /// /// Gets an exclusive lock on the database. /// /// An object that can be disposed to release the lock. - public abstract IDisposable GetDatabaseLock(); + public abstract IMigrationsDatabaseLock AcquireDatabaseLock(); /// /// Gets an exclusive lock on the database. /// /// A to observe while waiting for the task to complete. + /// /// An object that can be disposed to release the lock. /// If the is canceled. - public abstract Task GetDatabaseLockAsync(CancellationToken cancellationToken = default); + public abstract Task AcquireDatabaseLockAsync(CancellationToken cancellationToken = default); /// /// Configures the entity type mapped to the history table. diff --git a/src/EFCore.Relational/Migrations/HistoryRepositoryDependencies.cs b/src/EFCore.Relational/Migrations/HistoryRepositoryDependencies.cs index b5f40bd8ba2..a8bf28d3f22 100644 --- a/src/EFCore.Relational/Migrations/HistoryRepositoryDependencies.cs +++ b/src/EFCore.Relational/Migrations/HistoryRepositoryDependencies.cs @@ -59,7 +59,8 @@ public HistoryRepositoryDependencies( IRelationalTypeMappingSource typeMappingSource, ICurrentDbContext currentContext, IModelRuntimeInitializer modelRuntimeInitializer, - IRelationalCommandDiagnosticsLogger commandLogger) + IRelationalCommandDiagnosticsLogger commandLogger, + IDiagnosticsLogger migrationsLogger) { DatabaseCreator = databaseCreator; RawSqlCommandBuilder = rawSqlCommandBuilder; @@ -75,6 +76,7 @@ public HistoryRepositoryDependencies( CurrentContext = currentContext; ModelRuntimeInitializer = modelRuntimeInitializer; CommandLogger = commandLogger; + MigrationsLogger = migrationsLogger; } /// @@ -146,4 +148,9 @@ public HistoryRepositoryDependencies( /// The command logger /// public IRelationalCommandDiagnosticsLogger CommandLogger { get; init; } + + /// + /// The migrations logger + /// + public IDiagnosticsLogger MigrationsLogger { get; init; } } diff --git a/src/EFCore.Relational/Migrations/IHistoryRepository.cs b/src/EFCore.Relational/Migrations/IHistoryRepository.cs index 7bf3a461650..2189e6cea87 100644 --- a/src/EFCore.Relational/Migrations/IHistoryRepository.cs +++ b/src/EFCore.Relational/Migrations/IHistoryRepository.cs @@ -50,12 +50,44 @@ public interface IHistoryRepository /// /// A to observe while waiting for the task to complete. /// - /// A task that represents the asynchronous operation. The task result contains - /// if the table already exists, otherwise. + /// A task that represents the asynchronous operation. /// /// If the is canceled. Task CreateAsync(CancellationToken cancellationToken = default); + /// + /// Creates the history table if it doesn't exist. + /// + /// if the table was created, otherwise. + bool CreateIfNotExists() + { + if (!Exists()) + { + Create(); + return true; + } + return false; + } + + /// + /// Creates the history table. + /// + /// A to observe while waiting for the task to complete. + /// + /// A task that represents the asynchronous operation. The task result contains + /// if the table was created, otherwise. + /// + /// If the is canceled. + async Task CreateIfNotExistsAsync(CancellationToken cancellationToken = default) + { + if (!await ExistsAsync(cancellationToken).ConfigureAwait(false)) + { + await CreateAsync(cancellationToken).ConfigureAwait(false); + return true; + } + return false; + } + /// /// Queries the history table for all migrations that have been applied. /// @@ -75,18 +107,23 @@ Task> GetAppliedMigrationsAsync( CancellationToken cancellationToken = default); /// - /// Gets an exclusive lock on the database. + /// The condition under witch the lock is released implicitly. + /// + LockReleaseBehavior LockReleaseBehavior { get; } + + /// + /// Acquires an exclusive lock on the database. /// /// An object that can be disposed to release the lock. - IDisposable GetDatabaseLock(); + IMigrationsDatabaseLock AcquireDatabaseLock(); /// - /// Gets an exclusive lock on the database. + /// Acquires an exclusive lock on the database asynchronously. /// /// A to observe while waiting for the task to complete. /// An object that can be disposed to release the lock. /// If the is canceled. - Task GetDatabaseLockAsync(CancellationToken cancellationToken = default); + Task AcquireDatabaseLockAsync(CancellationToken cancellationToken = default); /// /// Generates a SQL script that will create the history table. diff --git a/src/EFCore.Relational/Migrations/IMigrationCommandExecutor.cs b/src/EFCore.Relational/Migrations/IMigrationCommandExecutor.cs index 7069ba9c045..039bbaa4f15 100644 --- a/src/EFCore.Relational/Migrations/IMigrationCommandExecutor.cs +++ b/src/EFCore.Relational/Migrations/IMigrationCommandExecutor.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Data; + namespace Microsoft.EntityFrameworkCore.Migrations; /// @@ -28,6 +30,24 @@ void ExecuteNonQuery( IEnumerable migrationCommands, IRelationalConnection connection); + /// + /// Executes the given commands using the given database connection. + /// + /// The commands to execute. + /// The connection to use. + /// The state of the current migration execution. + /// + /// Indicates whether the transaction started by this call should be commited. + /// If , the transaction will be made available in . + /// + /// The isolation level for the transaction. + int ExecuteNonQuery( + IReadOnlyList migrationCommands, + IRelationalConnection connection, + MigrationExecutionState executionState, + bool commitTransaction, + IsolationLevel? isolationLevel = null); + /// /// Executes the given commands using the given database connection. /// @@ -40,4 +60,26 @@ Task ExecuteNonQueryAsync( IEnumerable migrationCommands, IRelationalConnection connection, CancellationToken cancellationToken = default); + + /// + /// Executes the given commands using the given database connection. + /// + /// The commands to execute. + /// The connection to use. + /// The state of the current migration execution. + /// + /// Indicates whether the transaction started by this call should be commited. + /// If , the transaction will be made available in . + /// + /// The isolation level for the transaction. + /// A to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. + /// If the is canceled. + Task ExecuteNonQueryAsync( + IReadOnlyList migrationCommands, + IRelationalConnection connection, + MigrationExecutionState executionState, + bool commitTransaction, + IsolationLevel? isolationLevel = null, + CancellationToken cancellationToken = default); } diff --git a/src/EFCore.Relational/Migrations/IMigrationsDatabaseLock.cs b/src/EFCore.Relational/Migrations/IMigrationsDatabaseLock.cs new file mode 100644 index 00000000000..b08ea1b9faa --- /dev/null +++ b/src/EFCore.Relational/Migrations/IMigrationsDatabaseLock.cs @@ -0,0 +1,62 @@ +// 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.Migrations; + +/// +/// Represents an exclusive lock on the database that is used to ensure that only one migration application can be run at a time. +/// +/// +/// Typically only database providers implement this. +/// +public interface IMigrationsDatabaseLock : IDisposable, IAsyncDisposable +{ + /// + /// The history repository. + /// + protected IHistoryRepository HistoryRepository { get; } + + /// + /// Acquires an exclusive lock on the database again if the current one was already released. + /// + /// Indicates whether the connection was reopened. + /// + /// Indicates whether the transaction was restarted. + /// if there's no current transaction. + /// + /// An object that can be disposed to release the lock. + IMigrationsDatabaseLock ReacquireIfNeeded(bool connectionReopened, bool? transactionRestarted) + { + if ((connectionReopened && HistoryRepository.LockReleaseBehavior == LockReleaseBehavior.Connection) + || (transactionRestarted is true && HistoryRepository.LockReleaseBehavior == LockReleaseBehavior.Transaction)) + { + Dispose(); + return HistoryRepository.AcquireDatabaseLock(); + } + + return this; + } + + /// + /// Acquires an exclusive lock on the database again, if the current one was already released. + /// + /// Indicates whether the connection was reopened. + /// + /// Indicates whether the transaction was restarted. + /// if there's no current transaction. + /// + /// A to observe while waiting for the task to complete. + /// An object that can be disposed to release the lock. + async Task ReacquireIfNeededAsync( + bool connectionReopened, bool? transactionRestarted, CancellationToken cancellationToken = default) + { + if ((connectionReopened && HistoryRepository.LockReleaseBehavior == LockReleaseBehavior.Connection) + || (transactionRestarted is true && HistoryRepository.LockReleaseBehavior == LockReleaseBehavior.Transaction)) + { + await DisposeAsync().ConfigureAwait(false); + return await HistoryRepository.AcquireDatabaseLockAsync(cancellationToken).ConfigureAwait(false); + } + + return this; + } +} diff --git a/src/EFCore.Relational/Migrations/Internal/MigrationCommandExecutor.cs b/src/EFCore.Relational/Migrations/Internal/MigrationCommandExecutor.cs index 4b2d78a85bc..68c6ba4a886 100644 --- a/src/EFCore.Relational/Migrations/Internal/MigrationCommandExecutor.cs +++ b/src/EFCore.Relational/Migrations/Internal/MigrationCommandExecutor.cs @@ -28,74 +28,120 @@ public class MigrationCommandExecutor(IExecutionStrategy executionStrategy) : IM public virtual void ExecuteNonQuery( IEnumerable migrationCommands, IRelationalConnection connection) + => ExecuteNonQuery( + migrationCommands.ToList(), connection, new MigrationExecutionState(), commitTransaction: true); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 virtual int ExecuteNonQuery( + IReadOnlyList migrationCommands, + IRelationalConnection connection, + MigrationExecutionState executionState, + bool commitTransaction, + System.Data.IsolationLevel? isolationLevel = null) { - // TODO: Remove ToList, see #19710 - var commands = migrationCommands.ToList(); - var userTransaction = connection.CurrentTransaction; - if (userTransaction is not null - && (commands.Any(x => x.TransactionSuppressed) || executionStrategy.RetriesOnFailure)) + var inUserTransaction = connection.CurrentTransaction is not null && executionState.Transaction == null; + if (inUserTransaction + && (migrationCommands.Any(x => x.TransactionSuppressed) || executionStrategy.RetriesOnFailure)) { throw new NotSupportedException(RelationalStrings.TransactionSuppressedMigrationInUserTransaction); } - using (new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled)) - { - var parameters = new ExecuteParameters(commands, connection); - if (userTransaction is null) - { - executionStrategy.Execute(parameters, static (_, p) => Execute(p, beginTransaction: true), verifySucceeded: null); - } - else - { - Execute(parameters, beginTransaction: false); - } - } + using var transactionScope = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled); + + return executionStrategy.Execute( + (migrationCommands, connection, inUserTransaction, executionState, commitTransaction, isolationLevel), + static (_, s) => Execute( + s.migrationCommands, + s.connection, + s.executionState, + beginTransaction: !s.inUserTransaction, + commitTransaction: !s.inUserTransaction && s.commitTransaction, + s.isolationLevel), + verifySucceeded: null); } - private static bool Execute(ExecuteParameters parameters, bool beginTransaction) + private static int Execute( + IReadOnlyList migrationCommands, + IRelationalConnection connection, + MigrationExecutionState executionState, + bool beginTransaction, + bool commitTransaction, + System.Data.IsolationLevel? isolationLevel) { - var migrationCommands = parameters.MigrationCommands; - var connection = parameters.Connection; - IDbContextTransaction? transaction = null; - connection.Open(); + var result = 0; + var connectionOpened = connection.Open(); + Check.DebugAssert(!connectionOpened || executionState.Transaction == null, + "executionState.Transaction should be null"); + try { - for (var i = parameters.CurrentCommandIndex; i < migrationCommands.Count; i++) + for (var i = executionState.LastCommittedCommandIndex; i < migrationCommands.Count; i++) { var command = migrationCommands[i]; - if (transaction == null + if (executionState.Transaction == null && !command.TransactionSuppressed && beginTransaction) { - transaction = connection.BeginTransaction(); + executionState.Transaction = isolationLevel == null + ? connection.BeginTransaction() + : connection.BeginTransaction(isolationLevel.Value); + if (executionState.DatabaseLock != null) + { + executionState.DatabaseLock = executionState.DatabaseLock.ReacquireIfNeeded( + connectionOpened, transactionRestarted: true); + connectionOpened = false; + } } - if (transaction != null + if (executionState.Transaction != null && command.TransactionSuppressed) { - transaction.Commit(); - transaction.Dispose(); - transaction = null; - parameters.CurrentCommandIndex = i; + executionState.Transaction.Commit(); + executionState.Transaction.Dispose(); + executionState.Transaction = null; + executionState.LastCommittedCommandIndex = i; + executionState.AnyOperationPerformed = true; + + if (executionState.DatabaseLock != null) + { + executionState.DatabaseLock = executionState.DatabaseLock.ReacquireIfNeeded( + connectionOpened, transactionRestarted: null); + connectionOpened = false; + } } - command.ExecuteNonQuery(connection); + result = command.ExecuteNonQuery(connection); - if (transaction == null) + if (executionState.Transaction == null) { - parameters.CurrentCommandIndex = i + 1; + executionState.LastCommittedCommandIndex = i + 1; + executionState.AnyOperationPerformed = true; } } - transaction?.Commit(); + if (commitTransaction + && executionState.Transaction != null) + { + executionState.Transaction.Commit(); + executionState.Transaction.Dispose(); + executionState.Transaction = null; + } } - finally + catch { - transaction?.Dispose(); + executionState.Transaction?.Dispose(); + executionState.Transaction = null; connection.Close(); + throw; } - return true; + connection.Close(); + return result; } /// @@ -104,95 +150,136 @@ private static bool Execute(ExecuteParameters parameters, bool beginTransaction) /// 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 virtual async Task ExecuteNonQueryAsync( + public virtual Task ExecuteNonQueryAsync( IEnumerable migrationCommands, IRelationalConnection connection, CancellationToken cancellationToken = default) + => ExecuteNonQueryAsync( + migrationCommands.ToList(), connection, new MigrationExecutionState(), commitTransaction: true, System.Data.IsolationLevel.Unspecified, cancellationToken); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 virtual async Task ExecuteNonQueryAsync( + IReadOnlyList migrationCommands, + IRelationalConnection connection, + MigrationExecutionState executionState, + bool commitTransaction, + System.Data.IsolationLevel? isolationLevel = null, + CancellationToken cancellationToken = default) { - var commands = migrationCommands.ToList(); - var userTransaction = connection.CurrentTransaction; - if (userTransaction is not null - && (commands.Any(x => x.TransactionSuppressed) || executionStrategy.RetriesOnFailure)) + var inUserTransaction = connection.CurrentTransaction is not null && executionState.Transaction == null; + if (inUserTransaction + && (migrationCommands.Any(x => x.TransactionSuppressed) || executionStrategy.RetriesOnFailure)) { throw new NotSupportedException(RelationalStrings.TransactionSuppressedMigrationInUserTransaction); } using var transactionScope = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled); - var parameters = new ExecuteParameters(commands, connection); - if (userTransaction is null) - { - await executionStrategy.ExecuteAsync( - parameters, - static (_, p, ct) => ExecuteAsync(p, beginTransaction: true, ct), - verifySucceeded: null, - cancellationToken).ConfigureAwait(false); - } - else - { - await ExecuteAsync(parameters, beginTransaction: false, cancellationToken).ConfigureAwait(false); - } + return await executionStrategy.ExecuteAsync( + (migrationCommands, connection, inUserTransaction, executionState, commitTransaction, isolationLevel), + static (_, s, ct) => ExecuteAsync( + s.migrationCommands, + s.connection, + s.executionState, + beginTransaction: !s.inUserTransaction, + commitTransaction: !s.inUserTransaction && s.commitTransaction, + s.isolationLevel, + ct), + verifySucceeded: null, + cancellationToken).ConfigureAwait(false); } - private static async Task ExecuteAsync(ExecuteParameters parameters, bool beginTransaction, CancellationToken cancellationToken) + private static async Task ExecuteAsync( + IReadOnlyList migrationCommands, + IRelationalConnection connection, + MigrationExecutionState executionState, + bool beginTransaction, + bool commitTransaction, + System.Data.IsolationLevel? isolationLevel, + CancellationToken cancellationToken) { - var migrationCommands = parameters.MigrationCommands; - var connection = parameters.Connection; - IDbContextTransaction? transaction = null; - await connection.OpenAsync(cancellationToken).ConfigureAwait(false); + var result = 0; + var connectionOpened = await connection.OpenAsync(cancellationToken).ConfigureAwait(false); + Check.DebugAssert(!connectionOpened || executionState.Transaction == null, + "executionState.Transaction should be null"); + try { - for (var i = parameters.CurrentCommandIndex; i < migrationCommands.Count; i++) + for (var i = executionState.LastCommittedCommandIndex; i < migrationCommands.Count; i++) { + var lockReacquired = false; var command = migrationCommands[i]; - if (transaction == null + if (executionState.Transaction == null && !command.TransactionSuppressed && beginTransaction) { - transaction = await connection.BeginTransactionAsync(cancellationToken) + executionState.Transaction = await (isolationLevel == null + ? connection.BeginTransactionAsync(cancellationToken) + : connection.BeginTransactionAsync(isolationLevel.Value, cancellationToken)) .ConfigureAwait(false); + + if (executionState.DatabaseLock != null) + { + executionState.DatabaseLock = await executionState.DatabaseLock.ReacquireIfNeededAsync( + connectionOpened, transactionRestarted: true, cancellationToken) + .ConfigureAwait(false); + lockReacquired = true; + } } - if (transaction != null + if (executionState.Transaction != null && command.TransactionSuppressed) { - await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); - await transaction.DisposeAsync().ConfigureAwait(false); - transaction = null; - parameters.CurrentCommandIndex = i; + await executionState.Transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + await executionState.Transaction.DisposeAsync().ConfigureAwait(false); + executionState.Transaction = null; + executionState.LastCommittedCommandIndex = i; + executionState.AnyOperationPerformed = true; + + if (executionState.DatabaseLock != null + && !lockReacquired) + { + executionState.DatabaseLock = await executionState.DatabaseLock.ReacquireIfNeededAsync( + connectionOpened, transactionRestarted: null, cancellationToken) + .ConfigureAwait(false); + } } - await command.ExecuteNonQueryAsync(connection, cancellationToken: cancellationToken) + result = await command.ExecuteNonQueryAsync(connection, cancellationToken: cancellationToken) .ConfigureAwait(false); - if (transaction == null) + if (executionState.Transaction == null) { - parameters.CurrentCommandIndex = i + 1; + executionState.LastCommittedCommandIndex = i + 1; + executionState.AnyOperationPerformed = true; } } - if (transaction != null) + if (commitTransaction + && executionState.Transaction != null) { - await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + await executionState.Transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + await executionState.Transaction.DisposeAsync().ConfigureAwait(false); + executionState.Transaction = null; } } - finally + catch { - if (transaction != null) + if (executionState.Transaction != null) { - await transaction.DisposeAsync().ConfigureAwait(false); + await executionState.Transaction.DisposeAsync().ConfigureAwait(false); + executionState.Transaction = null; } - await connection.CloseAsync().ConfigureAwait(false); + throw; } - return true; - } - - private sealed class ExecuteParameters(List migrationCommands, IRelationalConnection connection) - { - public int CurrentCommandIndex; - public List MigrationCommands { get; } = migrationCommands; - public IRelationalConnection Connection { get; } = connection; + await connection.CloseAsync().ConfigureAwait(false); + return result; } } diff --git a/src/EFCore.Relational/Migrations/Internal/Migrator.cs b/src/EFCore.Relational/Migrations/Internal/Migrator.cs index c10cdd105c0..a6644ef6fc1 100644 --- a/src/EFCore.Relational/Migrations/Internal/Migrator.cs +++ b/src/EFCore.Relational/Migrations/Internal/Migrator.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Transactions; using Microsoft.EntityFrameworkCore.Diagnostics.Internal; namespace Microsoft.EntityFrameworkCore.Migrations.Internal; @@ -29,6 +30,7 @@ public class Migrator : IMigrator private readonly IDesignTimeModel _designTimeModel; private readonly string _activeProvider; private readonly IDbContextOptions _contextOptions; + private readonly IExecutionStrategy _executionStrategy; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -52,7 +54,8 @@ public Migrator( IDatabaseProvider databaseProvider, IMigrationsModelDiffer migrationsModelDiffer, IDesignTimeModel designTimeModel, - IDbContextOptions contextOptions) + IDbContextOptions contextOptions, + IExecutionStrategy executionStrategy) { _migrationsAssembly = migrationsAssembly; _historyRepository = historyRepository; @@ -70,8 +73,17 @@ public Migrator( _designTimeModel = designTimeModel; _activeProvider = databaseProvider.Name; _contextOptions = contextOptions; + _executionStrategy = executionStrategy; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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. + /// + protected virtual System.Data.IsolationLevel? MigrationTransactionIsolationLevel => null; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -80,29 +92,90 @@ public Migrator( /// public virtual void Migrate(string? targetMigration) { + var useTransaction = _connection.CurrentTransaction is null; + if (!useTransaction + && _executionStrategy.RetriesOnFailure) + { + throw new NotSupportedException(RelationalStrings.TransactionSuppressedMigrationInUserTransaction); + } + if (RelationalResources.LogPendingModelChanges(_logger).WarningBehavior != WarningBehavior.Ignore && HasPendingModelChanges()) { _logger.PendingModelChangesWarning(_currentContext.Context.GetType()); } + if (!useTransaction) + { + _logger.MigrationsUserTransactionWarning(); + } + _logger.MigrateUsingConnection(this, _connection); + using var transactionScope = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled); + if (!_databaseCreator.Exists()) { _databaseCreator.Create(); } + _connection.Open(); try { - _connection.Open(); + var state = new MigrationExecutionState(); + if (_historyRepository.LockReleaseBehavior != LockReleaseBehavior.Transaction + && useTransaction) + { + state.DatabaseLock = _historyRepository.AcquireDatabaseLock(); + } - _logger.AcquiringMigrationLock(); - using var _ = _historyRepository.GetDatabaseLock(); + _executionStrategy.Execute( + this, + static (_, migrator) => + { + migrator._connection.Open(); + try + { + return migrator._historyRepository.CreateIfNotExists(); + } + finally + { + migrator._connection.Close(); + } + }, + verifySucceeded: null); + + _executionStrategy.Execute( + (Migrator: this, + TargetMigration: targetMigration, + State: state, + UseTransaction: useTransaction), + static (c, s) => s.Migrator.MigrateImplementation(c, s.TargetMigration, s.State, s.UseTransaction), + static (_, s) => new ExecutionResult( + successful: s.Migrator.VerifyMigrationSucceeded(s.TargetMigration, s.State), + result: true)); + } + finally + { + _connection.Close(); + } + } - if (!_historyRepository.Exists()) + private bool MigrateImplementation( + DbContext context, string? targetMigration, MigrationExecutionState state, bool useTransaction) + { + var connectionOpened = _connection.Open(); + try + { + if (useTransaction) { - _historyRepository.Create(); + state.Transaction = MigrationTransactionIsolationLevel == null + ? _connection.BeginTransaction() + : _connection.BeginTransaction(MigrationTransactionIsolationLevel.Value); + + state.DatabaseLock = state.DatabaseLock == null + ? _historyRepository.AcquireDatabaseLock() + : state.DatabaseLock.ReacquireIfNeeded(connectionOpened, useTransaction); } PopulateMigrations( @@ -113,7 +186,15 @@ public virtual void Migrate(string? targetMigration) var commandLists = GetMigrationCommandLists(migratorData); foreach (var commandList in commandLists) { - _migrationCommandExecutor.ExecuteNonQuery(commandList(), _connection); + var (id, getCommands) = commandList; + if (id != state.CurrentMigrationId) + { + state.CurrentMigrationId = id; + state.LastCommittedCommandIndex = 0; + } + + _migrationCommandExecutor.ExecuteNonQuery( + getCommands(), _connection, state, commitTransaction: false, MigrationTransactionIsolationLevel); } var coreOptionsExtension = @@ -123,20 +204,22 @@ public virtual void Migrate(string? targetMigration) var seed = coreOptionsExtension.Seeder; if (seed != null) { - var context = _currentContext.Context; - var operationsPerformed = migratorData.AppliedMigrations.Count != 0 - || migratorData.RevertedMigrations.Count != 0; - using var transaction = context.Database.BeginTransaction(); - seed(context, operationsPerformed); - transaction.Commit(); + seed(context, state.AnyOperationPerformed); } else if (coreOptionsExtension.AsyncSeeder != null) { throw new InvalidOperationException(CoreStrings.MissingSeeder); } + + state.Transaction?.Commit(); + return state.AnyOperationPerformed; } finally { + state.DatabaseLock?.Dispose(); + state.DatabaseLock = null; + state.Transaction?.Dispose(); + state.Transaction = null; _connection.Close(); } } @@ -151,30 +234,96 @@ public virtual async Task MigrateAsync( string? targetMigration, CancellationToken cancellationToken = default) { + var useTransaction = _connection.CurrentTransaction is null; + if (!useTransaction + && _executionStrategy.RetriesOnFailure) + { + throw new NotSupportedException(RelationalStrings.TransactionSuppressedMigrationInUserTransaction); + } + if (RelationalResources.LogPendingModelChanges(_logger).WarningBehavior != WarningBehavior.Ignore && HasPendingModelChanges()) { _logger.PendingModelChangesWarning(_currentContext.Context.GetType()); } + if (!useTransaction) + { + _logger.MigrationsUserTransactionWarning(); + } + _logger.MigrateUsingConnection(this, _connection); + using var transactionScope = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled); + if (!await _databaseCreator.ExistsAsync(cancellationToken).ConfigureAwait(false)) { await _databaseCreator.CreateAsync(cancellationToken).ConfigureAwait(false); } + await _connection.OpenAsync(cancellationToken).ConfigureAwait(false); try { - await _connection.OpenAsync(cancellationToken).ConfigureAwait(false); + var state = new MigrationExecutionState(); + if (_historyRepository.LockReleaseBehavior != LockReleaseBehavior.Transaction + && useTransaction) + { + state.DatabaseLock = await _historyRepository.AcquireDatabaseLockAsync(cancellationToken).ConfigureAwait(false); + } - _logger.AcquiringMigrationLock(); - var dbLock = await _historyRepository.GetDatabaseLockAsync(cancellationToken).ConfigureAwait(false); - await using var _ = dbLock.ConfigureAwait(false); + await _executionStrategy.ExecuteAsync( + this, + static async (_, migrator, ct) => + { + await migrator._connection.OpenAsync(ct).ConfigureAwait(false); + try + { + return await migrator._historyRepository.CreateIfNotExistsAsync(ct).ConfigureAwait(false); + } + finally + { + await migrator._connection.CloseAsync().ConfigureAwait(false); + } + }, + verifySucceeded: null, + cancellationToken).ConfigureAwait(false); + + await _executionStrategy.ExecuteAsync( + (Migrator: this, + TargetMigration: targetMigration, + State: state, + UseTransaction: useTransaction), + async static (c, s, ct) => await s.Migrator.MigrateImplementationAsync( + c, s.TargetMigration, s.State, s.UseTransaction, ct).ConfigureAwait(false), + async static (_, s, ct) => new ExecutionResult( + successful: await s.Migrator.VerifyMigrationSucceededAsync(s.TargetMigration, s.State, ct).ConfigureAwait(false), + result: true), + cancellationToken) + .ConfigureAwait(false); + } + finally + { + await _connection.CloseAsync().ConfigureAwait(false); + } + } - if (!await _historyRepository.ExistsAsync(cancellationToken).ConfigureAwait(false)) + private async Task MigrateImplementationAsync( + DbContext context, string? targetMigration, MigrationExecutionState state, bool useTransaction, CancellationToken cancellationToken = default) + { + var connectionOpened = await _connection.OpenAsync(cancellationToken).ConfigureAwait(false); + try + { + if (useTransaction) { - await _historyRepository.CreateAsync(cancellationToken).ConfigureAwait(false); + state.Transaction = await (MigrationTransactionIsolationLevel == null + ? context.Database.BeginTransactionAsync(cancellationToken) + : context.Database.BeginTransactionAsync(MigrationTransactionIsolationLevel.Value, cancellationToken)) + .ConfigureAwait(false); + + state.DatabaseLock = state.DatabaseLock == null + ? await _historyRepository.AcquireDatabaseLockAsync(cancellationToken).ConfigureAwait(false) + : await state.DatabaseLock.ReacquireIfNeededAsync(connectionOpened, useTransaction, cancellationToken) + .ConfigureAwait(false); } PopulateMigrations( @@ -185,7 +334,15 @@ public virtual async Task MigrateAsync( var commandLists = GetMigrationCommandLists(migratorData); foreach (var commandList in commandLists) { - await _migrationCommandExecutor.ExecuteNonQueryAsync(commandList(), _connection, cancellationToken) + var (id, getCommands) = commandList; + if (id != state.CurrentMigrationId) + { + state.CurrentMigrationId = id; + state.LastCommittedCommandIndex = 0; + } + + await _migrationCommandExecutor.ExecuteNonQueryAsync( + getCommands(), _connection, state, commitTransaction: false, MigrationTransactionIsolationLevel, cancellationToken) .ConfigureAwait(false); } @@ -196,26 +353,36 @@ await _migrationCommandExecutor.ExecuteNonQueryAsync(commandList(), _connection, var seedAsync = coreOptionsExtension.AsyncSeeder; if (seedAsync != null) { - var context = _currentContext.Context; - var operationsPerformed = migratorData.AppliedMigrations.Count != 0 - || migratorData.RevertedMigrations.Count != 0; - var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); - await using var __ = transaction.ConfigureAwait(false); - await seedAsync(context, operationsPerformed, cancellationToken).ConfigureAwait(false); - await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + await seedAsync(context, state.AnyOperationPerformed, cancellationToken).ConfigureAwait(false); } else if (coreOptionsExtension.Seeder != null) { throw new InvalidOperationException(CoreStrings.MissingSeeder); } + + if (state.Transaction != null) + { + await state.Transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + } + return state.AnyOperationPerformed; } finally { - _connection.Close(); + if (state.DatabaseLock != null) + { + state.DatabaseLock.Dispose(); + state.DatabaseLock = null; + } + if (state.Transaction != null) + { + await state.Transaction.DisposeAsync().ConfigureAwait(false); + state.Transaction = null; + } + await _connection.CloseAsync().ConfigureAwait(false); } } - private IEnumerable>> GetMigrationCommandLists(MigratorData parameters) + private IEnumerable<(string, Func>)> GetMigrationCommandLists(MigratorData parameters) { var migrationsToApply = parameters.AppliedMigrations; var migrationsToRevert = parameters.RevertedMigrations; @@ -226,7 +393,7 @@ private IEnumerable>> GetMigrationCommandLi var migration = migrationsToRevert[i]; var index = i; - yield return () => + yield return (migration.GetId(), () => { _logger.MigrationReverting(this, migration); @@ -242,12 +409,12 @@ private IEnumerable>> GetMigrationCommandLi } return commands; - }; + }); } foreach (var migration in migrationsToApply) { - yield return () => + yield return (migration.GetId(), () => { _logger.MigrationApplying(this, migration); @@ -259,7 +426,7 @@ private IEnumerable>> GetMigrationCommandLi } return commands; - }; + }); } if (migrationsToRevert.Count + migrationsToApply.Count == 0) @@ -340,6 +507,26 @@ protected virtual void PopulateMigrations( parameters = new MigratorData(migrationsToApply, migrationsToRevert, actualTargetMigration); } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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. + /// + protected virtual bool VerifyMigrationSucceeded( + string? targetMigration, MigrationExecutionState state) + => false; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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. + /// + protected virtual Task VerifyMigrationSucceededAsync( + string? targetMigration, MigrationExecutionState state, CancellationToken cancellationToken) + => Task.FromResult(false); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.Relational/Migrations/LockReleaseBehavior.cs b/src/EFCore.Relational/Migrations/LockReleaseBehavior.cs new file mode 100644 index 00000000000..bd1f96e995c --- /dev/null +++ b/src/EFCore.Relational/Migrations/LockReleaseBehavior.cs @@ -0,0 +1,25 @@ +// 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.Migrations; + +/// +/// Represents the conditions under which a lock is released implicitly. +/// +public enum LockReleaseBehavior +{ + /// + /// The lock is released when the transaction is committed or rolled back. + /// + Transaction, + + /// + /// The lock is released when the connection is closed. + /// + Connection, + + /// + /// The lock can only be released explicitly. + /// + Explicit +} diff --git a/src/EFCore.Relational/Migrations/MigrationExecutionState.cs b/src/EFCore.Relational/Migrations/MigrationExecutionState.cs new file mode 100644 index 00000000000..accd83e0cfc --- /dev/null +++ b/src/EFCore.Relational/Migrations/MigrationExecutionState.cs @@ -0,0 +1,35 @@ +// 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.Migrations; + +/// +/// Contains the state of the current migration execution. +/// +public sealed class MigrationExecutionState +{ + /// + /// The index of the last command that was committed to the database. + /// + public int LastCommittedCommandIndex { get; set; } + + /// + /// The id the migration that is currently being applied. + /// + public string? CurrentMigrationId { get; set; } + + /// + /// Indicates whether any migration operation was performed. + /// + public bool AnyOperationPerformed { get; set; } + + /// + /// The database lock that is in use. + /// + public IMigrationsDatabaseLock? DatabaseLock { get; set; } + + /// + /// The transaction that is in use. + /// + public IDbContextTransaction? Transaction { get; set; } +} diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index c2350480259..731c62c4ec2 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -2130,7 +2130,7 @@ public static string UnsupportedOperatorForSqlExpression(object? nodeType, objec nodeType, expressionType); /// - /// No relational type mapping can be found for property '{entity}.{property}' and the current provider doesn't specify a default store type for the properties of type '{clrType}'. + /// No relational type mapping can be found for property '{entity}.{property}' and the current provider doesn't specify a default store type for the properties of type '{clrType}'. /// public static string UnsupportedPropertyType(object? entity, object? property, object? clrType) => string.Format( @@ -2256,7 +2256,7 @@ private static readonly ResourceManager _resourceManager = new ResourceManager("Microsoft.EntityFrameworkCore.Properties.RelationalStrings", typeof(RelationalResources).Assembly); /// - /// Acquiring an exclusive lock for migration application. See https://aka.ms/efcore-docs-migrations for more information if this takes too long. + /// Acquiring an exclusive lock for migration application. See https://aka.ms/efcore-docs-migrations-lock for more information if this takes too long. /// public static EventDefinition LogAcquiringMigrationLock(IDiagnosticsLogger logger) { @@ -3424,6 +3424,31 @@ public static EventDefinition LogMigrationAttributeMissingWarning(IDiagn return (EventDefinition)definition; } + /// + /// A transaction was started before applying migrations. This prevents a database lock to be acquired and hence the database will not be protected from concurrent migration applications. The transactions and execution strategy are already managed by EF as needed. Remove the external transaction. + /// + public static EventDefinition LogMigrationsUserTransaction(IDiagnosticsLogger logger) + { + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogMigrationsUserTransactionWarning; + if (definition == null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((RelationalLoggingDefinitions)logger.Definitions).LogMigrationsUserTransactionWarning, + logger, + static logger => new EventDefinition( + logger.Options, + RelationalEventId.MigrationsUserTransactionWarning, + LogLevel.Warning, + "RelationalEventId.MigrationsUserTransactionWarning", + level => LoggerMessage.Define( + level, + RelationalEventId.MigrationsUserTransactionWarning, + _resourceManager.GetString("LogMigrationsUserTransaction")!))); + } + + return (EventDefinition)definition; + } + /// /// Compiling a query which loads related collections for more than one collection navigation, either via 'Include' or through projection, but no 'QuerySplittingBehavior' has been configured. By default, Entity Framework will use 'QuerySplittingBehavior.SingleQuery', which can potentially result in slow query performance. See https://go.microsoft.com/fwlink/?linkid=2134277 for more information. To identify the query that's triggering this warning call 'ConfigureWarnings(w => w.Throw(RelationalEventId.MultipleCollectionIncludeWarning))'. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index d6390b417fc..104daf9dafb 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -1,17 +1,17 @@  - @@ -786,6 +786,10 @@ A [Migration] attribute isn't specified on the '{class}' class. Warning RelationalEventId.MigrationAttributeMissingWarning string + + A transaction was started before applying migrations. This prevents a database lock to be acquired and hence the database will not be protected from concurrent migration applications. The transactions and execution strategy are already managed by EF as needed. Remove the external transaction. + Warning RelationalEventId.MigrationsUserTransactionWarning + Compiling a query which loads related collections for more than one collection navigation, either via 'Include' or through projection, but no 'QuerySplittingBehavior' has been configured. By default, Entity Framework will use 'QuerySplittingBehavior.SingleQuery', which can potentially result in slow query performance. See https://go.microsoft.com/fwlink/?linkid=2134277 for more information. To identify the query that's triggering this warning call 'ConfigureWarnings(w => w.Throw(RelationalEventId.MultipleCollectionIncludeWarning))'. Warning RelationalEventId.MultipleCollectionIncludeWarning diff --git a/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs b/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs index ad6a2bcd7df..96f5ca0f76a 100644 --- a/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs +++ b/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs @@ -116,7 +116,7 @@ public virtual Task DeleteAsync(CancellationToken cancellationToken = default) /// to incrementally update the schema. It is assumed that none of the tables exist in the database. /// public virtual void CreateTables() - => Dependencies.MigrationCommandExecutor.ExecuteNonQuery(GetCreateTablesCommands(), Dependencies.Connection); + => Dependencies.MigrationCommandExecutor.ExecuteNonQuery(GetCreateTablesCommands(), Dependencies.Connection, new MigrationExecutionState(), commitTransaction: true); /// /// Asynchronously creates all tables for the current model in the database. No attempt is made @@ -129,7 +129,7 @@ public virtual void CreateTables() /// If the is canceled. public virtual Task CreateTablesAsync(CancellationToken cancellationToken = default) => Dependencies.MigrationCommandExecutor.ExecuteNonQueryAsync( - GetCreateTablesCommands(), Dependencies.Connection, cancellationToken); + GetCreateTablesCommands(), Dependencies.Connection, new MigrationExecutionState(), commitTransaction: true, cancellationToken: cancellationToken); /// /// Gets the commands that will create all tables from the model. diff --git a/src/EFCore.SqlServer/Migrations/Internal/SqlServerHistoryRepository.cs b/src/EFCore.SqlServer/Migrations/Internal/SqlServerHistoryRepository.cs index cc22d67fca4..202c7a2c131 100644 --- a/src/EFCore.SqlServer/Migrations/Internal/SqlServerHistoryRepository.cs +++ b/src/EFCore.SqlServer/Migrations/Internal/SqlServerHistoryRepository.cs @@ -59,8 +59,18 @@ protected override bool InterpretExistsResult(object? value) /// 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 override IDisposable GetDatabaseLock() + public override LockReleaseBehavior LockReleaseBehavior => LockReleaseBehavior.Connection; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 override IMigrationsDatabaseLock AcquireDatabaseLock() { + Dependencies.MigrationsLogger.AcquiringMigrationLock(); + var dbLock = CreateMigrationDatabaseLock(); int result; try @@ -91,8 +101,10 @@ public override IDisposable GetDatabaseLock() /// 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 override async Task GetDatabaseLockAsync(CancellationToken cancellationToken = default) + public override async Task AcquireDatabaseLockAsync(CancellationToken cancellationToken = default) { + Dependencies.MigrationsLogger.AcquiringMigrationLock(); + var dbLock = CreateMigrationDatabaseLock(); int result; try @@ -135,7 +147,8 @@ private SqlServerMigrationDatabaseLock CreateMigrationDatabaseLock() EXEC @result = sp_releaseapplock @Resource = '__EFMigrationsLock', @LockOwner = 'Session'; SELECT @result """), - CreateRelationalCommandParameters()); + CreateRelationalCommandParameters(), + this); private RelationalCommandParameterObject CreateRelationalCommandParameters() => new( diff --git a/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationDatabaseLock.cs b/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationDatabaseLock.cs index c102f6f9705..b31ff900625 100644 --- a/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationDatabaseLock.cs +++ b/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationDatabaseLock.cs @@ -16,11 +16,20 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Migrations.Internal; /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public class SqlServerMigrationDatabaseLock( - IRelationalCommand relationalCommand, + IRelationalCommand releaseLockCommand, RelationalCommandParameterObject relationalCommandParameters, + IHistoryRepository historyRepository, CancellationToken cancellationToken = default) - : IDisposable, IAsyncDisposable + : IMigrationsDatabaseLock { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 virtual IHistoryRepository HistoryRepository => historyRepository; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -28,7 +37,7 @@ public class SqlServerMigrationDatabaseLock( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public void Dispose() - => relationalCommand.ExecuteScalar(relationalCommandParameters); + => releaseLockCommand.ExecuteScalar(relationalCommandParameters); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -37,5 +46,5 @@ public void Dispose() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public async ValueTask DisposeAsync() - => await relationalCommand.ExecuteScalarAsync(relationalCommandParameters, cancellationToken).ConfigureAwait(false); + => await releaseLockCommand.ExecuteScalarAsync(relationalCommandParameters, cancellationToken).ConfigureAwait(false); } diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerDatabaseCreator.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerDatabaseCreator.cs index 589440790a1..e3f3fcdb838 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerDatabaseCreator.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerDatabaseCreator.cs @@ -14,26 +14,19 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; /// 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 class SqlServerDatabaseCreator : RelationalDatabaseCreator +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// 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 class SqlServerDatabaseCreator( + RelationalDatabaseCreatorDependencies dependencies, + ISqlServerConnection connection, + IRawSqlCommandBuilder rawSqlCommandBuilder) : RelationalDatabaseCreator(dependencies) { - private readonly ISqlServerConnection _connection; - private readonly IRawSqlCommandBuilder _rawSqlCommandBuilder; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// 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 SqlServerDatabaseCreator( - RelationalDatabaseCreatorDependencies dependencies, - ISqlServerConnection connection, - IRawSqlCommandBuilder rawSqlCommandBuilder) - : base(dependencies) - { - _connection = connection; - _rawSqlCommandBuilder = rawSqlCommandBuilder; - } + private readonly ISqlServerConnection _connection = connection; + private readonly IRawSqlCommandBuilder _rawSqlCommandBuilder = rawSqlCommandBuilder; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -62,7 +55,7 @@ public override void Create() using (var masterConnection = _connection.CreateMasterConnection()) { Dependencies.MigrationCommandExecutor - .ExecuteNonQuery(CreateCreateOperations(), masterConnection); + .ExecuteNonQuery(CreateCreateOperations(), masterConnection, new MigrationExecutionState(), commitTransaction: true); ClearPool(); } @@ -82,7 +75,7 @@ public override async Task CreateAsync(CancellationToken cancellationToken = def await using (masterConnection.ConfigureAwait(false)) { await Dependencies.MigrationCommandExecutor - .ExecuteNonQueryAsync(CreateCreateOperations(), masterConnection, cancellationToken) + .ExecuteNonQueryAsync(CreateCreateOperations(), masterConnection, new MigrationExecutionState(), commitTransaction: true, cancellationToken: cancellationToken) .ConfigureAwait(false); ClearPool(); @@ -157,8 +150,7 @@ private IReadOnlyList CreateCreateOperations() { var builder = new SqlConnectionStringBuilder(_connection.DbConnection.ConnectionString); return Dependencies.MigrationsSqlGenerator.Generate( - new[] - { + [ new SqlServerCreateDatabaseOperation { Name = builder.InitialCatalog, @@ -166,7 +158,7 @@ private IReadOnlyList CreateCreateOperations() Collation = Dependencies.CurrentContext.Context.GetService() .Model.GetRelationalModel().Collation } - }); + ]); } /// @@ -345,7 +337,7 @@ public override void Delete() using var masterConnection = _connection.CreateMasterConnection(); Dependencies.MigrationCommandExecutor - .ExecuteNonQuery(CreateDropCommands(), masterConnection); + .ExecuteNonQuery(CreateDropCommands(), masterConnection, new MigrationExecutionState(), commitTransaction: true); } /// @@ -361,7 +353,7 @@ public override async Task DeleteAsync(CancellationToken cancellationToken = def var masterConnection = _connection.CreateMasterConnection(); await using var _ = masterConnection.ConfigureAwait(false); await Dependencies.MigrationCommandExecutor - .ExecuteNonQueryAsync(CreateDropCommands(), masterConnection, cancellationToken) + .ExecuteNonQueryAsync(CreateDropCommands(), masterConnection, new MigrationExecutionState(), commitTransaction: true, cancellationToken: cancellationToken) .ConfigureAwait(false); } diff --git a/src/EFCore.Sqlite.Core/Migrations/Internal/SqliteHistoryRepository.cs b/src/EFCore.Sqlite.Core/Migrations/Internal/SqliteHistoryRepository.cs index e346fd1e698..89d8fefc0c8 100644 --- a/src/EFCore.Sqlite.Core/Migrations/Internal/SqliteHistoryRepository.cs +++ b/src/EFCore.Sqlite.Core/Migrations/Internal/SqliteHistoryRepository.cs @@ -103,11 +103,21 @@ public override string GetEndIfScript() /// 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 override IDisposable GetDatabaseLock() + public override LockReleaseBehavior LockReleaseBehavior => LockReleaseBehavior.Explicit; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 override IMigrationsDatabaseLock AcquireDatabaseLock() { + Dependencies.MigrationsLogger.AcquiringMigrationLock(); + if (!InterpretExistsResult( - Dependencies.RawSqlCommandBuilder.Build(CreateExistsSql(LockTableName)) - .ExecuteScalar(CreateRelationalCommandParameters()))) + Dependencies.RawSqlCommandBuilder.Build(CreateExistsSql(LockTableName)) + .ExecuteScalar(CreateRelationalCommandParameters()))) { CreateLockTableCommand().ExecuteNonQuery(CreateRelationalCommandParameters()); } @@ -129,8 +139,6 @@ public override IDisposable GetDatabaseLock() retryDelay = retryDelay.Add(retryDelay); } } - - throw new TimeoutException(); } /// @@ -139,11 +147,14 @@ public override IDisposable GetDatabaseLock() /// 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 override async Task GetDatabaseLockAsync(CancellationToken cancellationToken = default) + public override async Task AcquireDatabaseLockAsync( + CancellationToken cancellationToken = default) { + Dependencies.MigrationsLogger.AcquiringMigrationLock(); + if (!InterpretExistsResult( - await Dependencies.RawSqlCommandBuilder.Build(CreateExistsSql(LockTableName)) - .ExecuteScalarAsync(CreateRelationalCommandParameters(), cancellationToken).ConfigureAwait(false))) + await Dependencies.RawSqlCommandBuilder.Build(CreateExistsSql(LockTableName)) + .ExecuteScalarAsync(CreateRelationalCommandParameters(), cancellationToken).ConfigureAwait(false))) { await CreateLockTableCommand().ExecuteNonQueryAsync(CreateRelationalCommandParameters(), cancellationToken) .ConfigureAwait(false); @@ -167,8 +178,6 @@ await CreateLockTableCommand().ExecuteNonQueryAsync(CreateRelationalCommandParam retryDelay = retryDelay.Add(retryDelay); } } - - throw new TimeoutException(); } private IRelationalCommand CreateLockTableCommand() @@ -206,7 +215,7 @@ DELETE FROM "{LockTableName}" } private SqliteMigrationDatabaseLock CreateMigrationDatabaseLock() - => new(CreateDeleteLockCommand(), CreateRelationalCommandParameters()); + => new(CreateDeleteLockCommand(), CreateRelationalCommandParameters(), this); private RelationalCommandParameterObject CreateRelationalCommandParameters() => new( diff --git a/src/EFCore.Sqlite.Core/Migrations/Internal/SqliteMigrationDatabaseLock.cs b/src/EFCore.Sqlite.Core/Migrations/Internal/SqliteMigrationDatabaseLock.cs index 997cbcd4710..668d2107eaf 100644 --- a/src/EFCore.Sqlite.Core/Migrations/Internal/SqliteMigrationDatabaseLock.cs +++ b/src/EFCore.Sqlite.Core/Migrations/Internal/SqliteMigrationDatabaseLock.cs @@ -10,11 +10,20 @@ namespace Microsoft.EntityFrameworkCore.Sqlite.Migrations.Internal; /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public class SqliteMigrationDatabaseLock( - IRelationalCommand relationalCommand, + IRelationalCommand releaseLockCommand, RelationalCommandParameterObject relationalCommandParameters, + IHistoryRepository historyRepository, CancellationToken cancellationToken = default) - : IDisposable, IAsyncDisposable + : IMigrationsDatabaseLock { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 virtual IHistoryRepository HistoryRepository => historyRepository; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -22,7 +31,7 @@ public class SqliteMigrationDatabaseLock( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public void Dispose() - => relationalCommand.ExecuteScalar(relationalCommandParameters); + => releaseLockCommand.ExecuteScalar(relationalCommandParameters); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -31,5 +40,5 @@ public void Dispose() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public async ValueTask DisposeAsync() - => await relationalCommand.ExecuteScalarAsync(relationalCommandParameters, cancellationToken).ConfigureAwait(false); + => await releaseLockCommand.ExecuteScalarAsync(relationalCommandParameters, cancellationToken).ConfigureAwait(false); } diff --git a/test/EFCore.Design.Tests/Design/DesignTimeServicesTest.cs b/test/EFCore.Design.Tests/Design/DesignTimeServicesTest.cs index c2c58289d54..ee13f8d8c34 100644 --- a/test/EFCore.Design.Tests/Design/DesignTimeServicesTest.cs +++ b/test/EFCore.Design.Tests/Design/DesignTimeServicesTest.cs @@ -192,6 +192,8 @@ public bool IsValidId(string value) public class ExtensionHistoryRepository : IHistoryRepository { + public virtual LockReleaseBehavior LockReleaseBehavior => LockReleaseBehavior.Explicit; + public void Create() => throw new NotImplementedException(); @@ -222,10 +224,10 @@ public string GetCreateIfNotExistsScript() public string GetCreateScript() => throw new NotImplementedException(); - public IDisposable GetDatabaseLock() + public IMigrationsDatabaseLock AcquireDatabaseLock() => throw new NotImplementedException(); - public Task GetDatabaseLockAsync(CancellationToken cancellationToken = default) + public Task AcquireDatabaseLockAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); public string GetDeleteScript(string migrationId) @@ -264,6 +266,8 @@ public bool IsValidId(string value) public class ContextHistoryRepository : IHistoryRepository { + public virtual LockReleaseBehavior LockReleaseBehavior => LockReleaseBehavior.Explicit; + public bool Exists() => throw new NotImplementedException(); @@ -294,10 +298,10 @@ public void Create() public Task CreateAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public IDisposable GetDatabaseLock() + public IMigrationsDatabaseLock AcquireDatabaseLock() => throw new NotImplementedException(); - public Task GetDatabaseLockAsync(CancellationToken cancellationToken = default) + public Task AcquireDatabaseLockAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); public string GetDeleteScript(string migrationId) diff --git a/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs b/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs index 94fa478789e..45f138e904b 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs @@ -125,7 +125,8 @@ var migrationAssembly services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), - services.GetRequiredService()))); + services.GetRequiredService(), + services.GetRequiredService()))); } // ReSharper disable once UnusedTypeParameter @@ -143,6 +144,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) private class MockHistoryRepository : IHistoryRepository { + public virtual LockReleaseBehavior LockReleaseBehavior => LockReleaseBehavior.Explicit; + public string GetBeginIfExistsScript(string migrationId) => null; @@ -182,10 +185,10 @@ public void Create() public Task CreateAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public IDisposable GetDatabaseLock() + public IMigrationsDatabaseLock AcquireDatabaseLock() => throw new NotImplementedException(); - public Task GetDatabaseLockAsync(CancellationToken cancellationToken = default) + public Task AcquireDatabaseLockAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); } diff --git a/test/EFCore.Relational.Tests/Extensions/RelationalDatabaseFacadeExtensionsTest.cs b/test/EFCore.Relational.Tests/Extensions/RelationalDatabaseFacadeExtensionsTest.cs index 3e56284521a..f2d2a935de3 100644 --- a/test/EFCore.Relational.Tests/Extensions/RelationalDatabaseFacadeExtensionsTest.cs +++ b/test/EFCore.Relational.Tests/Extensions/RelationalDatabaseFacadeExtensionsTest.cs @@ -321,6 +321,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) private class FakeHistoryRepository : IHistoryRepository { + public virtual LockReleaseBehavior LockReleaseBehavior => LockReleaseBehavior.Explicit; + public List AppliedMigrations { get; set; } public IReadOnlyList GetAppliedMigrations() @@ -362,10 +364,10 @@ public void Create() public Task CreateAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public IDisposable GetDatabaseLock() + public IMigrationsDatabaseLock AcquireDatabaseLock() => throw new NotImplementedException(); - public Task GetDatabaseLockAsync(CancellationToken cancellationToken = default) + public Task AcquireDatabaseLockAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsInfrastructureSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsInfrastructureSqlServerTest.cs index 8aeb88a4e31..3d1c63e96ef 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsInfrastructureSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsInfrastructureSqlServerTest.cs @@ -1017,13 +1017,16 @@ SELECT 1 EXEC @result = sp_getapplock @Resource = '__EFMigrationsLock', @LockOwner = 'Session', @LockMode = 'Exclusive'; SELECT @result -SELECT OBJECT_ID(N'[__EFMigrationsHistory]'); +IF OBJECT_ID(N'[__EFMigrationsHistory]') IS NULL +BEGIN + CREATE TABLE [__EFMigrationsHistory] ( + [MigrationId] nvarchar(150) NOT NULL, + [ProductVersion] nvarchar(32) NOT NULL, + CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId]) + ); +END; -CREATE TABLE [__EFMigrationsHistory] ( - [MigrationId] nvarchar(150) NOT NULL, - [ProductVersion] nvarchar(32) NOT NULL, - CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId]) -); +SELECT 1 SELECT OBJECT_ID(N'[__EFMigrationsHistory]'); @@ -1047,6 +1050,22 @@ CONSTRAINT [PK_Blogs] PRIMARY KEY ([Id]) THROW 65536, 'Test', 0; END +DECLARE @result int; +EXEC @result = sp_releaseapplock @Resource = '__EFMigrationsLock', @LockOwner = 'Session'; +SELECT @result + +DECLARE @result int; +EXEC @result = sp_getapplock @Resource = '__EFMigrationsLock', @LockOwner = 'Session', @LockMode = 'Exclusive'; +SELECT @result + +SELECT 1 + +SELECT OBJECT_ID(N'[__EFMigrationsHistory]'); + +SELECT [MigrationId], [ProductVersion] +FROM [__EFMigrationsHistory] +ORDER BY [MigrationId]; + IF OBJECT_ID(N'Blogs', N'U') IS NULL BEGIN CREATE TABLE [Blogs] ( @@ -1110,13 +1129,16 @@ SELECT 1 EXEC @result = sp_getapplock @Resource = '__EFMigrationsLock', @LockOwner = 'Session', @LockMode = 'Exclusive'; SELECT @result -SELECT OBJECT_ID(N'[__EFMigrationsHistory]'); +IF OBJECT_ID(N'[__EFMigrationsHistory]') IS NULL +BEGIN + CREATE TABLE [__EFMigrationsHistory] ( + [MigrationId] nvarchar(150) NOT NULL, + [ProductVersion] nvarchar(32) NOT NULL, + CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId]) + ); +END; -CREATE TABLE [__EFMigrationsHistory] ( - [MigrationId] nvarchar(150) NOT NULL, - [ProductVersion] nvarchar(32) NOT NULL, - CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId]) -); +SELECT 1 SELECT OBJECT_ID(N'[__EFMigrationsHistory]'); @@ -1140,6 +1162,22 @@ CONSTRAINT [PK_Blogs] PRIMARY KEY ([Id]) THROW 65536, 'Test', 0; END +DECLARE @result int; +EXEC @result = sp_releaseapplock @Resource = '__EFMigrationsLock', @LockOwner = 'Session'; +SELECT @result + +DECLARE @result int; +EXEC @result = sp_getapplock @Resource = '__EFMigrationsLock', @LockOwner = 'Session', @LockMode = 'Exclusive'; +SELECT @result + +SELECT 1 + +SELECT OBJECT_ID(N'[__EFMigrationsHistory]'); + +SELECT [MigrationId], [ProductVersion] +FROM [__EFMigrationsHistory] +ORDER BY [MigrationId]; + IF OBJECT_ID(N'Blogs', N'U') IS NULL BEGIN CREATE TABLE [Blogs] ( @@ -2078,7 +2116,8 @@ IF EXISTS(select * from sys.databases where name='TransactionSuppressed') public override MigrationsContext CreateContext() { var options = AddOptions(TestStore.AddProviderOptions(new DbContextOptionsBuilder())) - .UseSqlServer(TestStore.ConnectionString, b => b.ApplyConfiguration()) + .UseSqlServer(TestStore.ConnectionString, b => b + .ApplyConfiguration()) .UseInternalServiceProvider(ServiceProvider) .Options; return new MigrationsContext(options); diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestSqlServerRetryingExecutionStrategy.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestSqlServerRetryingExecutionStrategy.cs index ec517ff802f..ac0eea2e54c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestSqlServerRetryingExecutionStrategy.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestSqlServerRetryingExecutionStrategy.cs @@ -43,6 +43,13 @@ public TestSqlServerRetryingExecutionStrategy(ExecutionStrategyDependencies depe { } + public TestSqlServerRetryingExecutionStrategy( + ExecutionStrategyDependencies dependencies, + IEnumerable errorNumbersToAdd) + : base(dependencies, DefaultMaxRetryCount, DefaultMaxDelay, _additionalErrorNumbers.Concat(errorNumbersToAdd)) + { + } + protected override bool ShouldRetryOn(Exception exception) { if (base.ShouldRetryOn(exception))