diff --git a/src/EFCore.Relational/Migrations/HistoryRepository.cs b/src/EFCore.Relational/Migrations/HistoryRepository.cs
index ed111047ece..7a4688d5db5 100644
--- a/src/EFCore.Relational/Migrations/HistoryRepository.cs
+++ b/src/EFCore.Relational/Migrations/HistoryRepository.cs
@@ -23,8 +23,6 @@ namespace Microsoft.EntityFrameworkCore.Migrations;
/// See Database migrations for more information and examples.
///
///
-// TODO: Leverage query pipeline for GetAppliedMigrations
-// TODO: Leverage update pipeline for GetInsertScript & GetDeleteScript
public abstract class HistoryRepository : IHistoryRepository
{
///
@@ -122,15 +120,14 @@ protected virtual string ProductVersionColumnName
///
/// if the table already exists, otherwise.
public virtual bool Exists()
- => Dependencies.DatabaseCreator.Exists()
- && InterpretExistsResult(
- Dependencies.RawSqlCommandBuilder.Build(ExistsSql).ExecuteScalar(
- new RelationalCommandParameterObject(
- Dependencies.Connection,
- null,
- null,
- Dependencies.CurrentContext.Context,
- Dependencies.CommandLogger, CommandSource.Migrations)));
+ => 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.
@@ -142,16 +139,15 @@ public virtual bool Exists()
///
/// If the is canceled.
public virtual async Task ExistsAsync(CancellationToken cancellationToken = default)
- => 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));
+ => 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 .
@@ -171,15 +167,49 @@ await Dependencies.RawSqlCommandBuilder.Build(ExistsSql).ExecuteScalarAsync(
///
/// The SQL script.
public virtual string GetCreateScript()
+ => string.Concat(GetCreateCommands().Select(c => c.CommandText));
+
+ ///
+ /// Creates the history table.
+ ///
+ public virtual void Create()
+ => Dependencies.MigrationCommandExecutor.ExecuteNonQuery(GetCreateCommands(), Dependencies.Connection);
+
+ ///
+ /// Creates the history table.
+ ///
+ public virtual Task CreateAsync(CancellationToken cancellationToken = default)
+ => Dependencies.MigrationCommandExecutor.ExecuteNonQueryAsync(GetCreateCommands(), Dependencies.Connection, cancellationToken);
+
+ ///
+ /// Returns the migration commands that will create the history table.
+ ///
+ /// The migration commands that will create the history table.
+ protected virtual IReadOnlyList GetCreateCommands()
{
var model = EnsureModel();
var operations = Dependencies.ModelDiffer.GetDifferences(null, model.GetRelationalModel());
var commandList = Dependencies.MigrationsSqlGenerator.Generate(operations, model);
-
- return string.Concat(commandList.Select(c => c.CommandText));
+ return commandList;
}
+ ///
+ /// Gets an exclusive lock on the database.
+ ///
+ /// The time to wait for the lock before an exception is thrown.
+ /// An object that can be disposed to release the lock.
+ public abstract IMigrationDatabaseLock GetDatabaseLock(TimeSpan timeout);
+
+ ///
+ /// Gets an exclusive lock on the database.
+ ///
+ /// The time to wait for the lock before an exception is thrown.
+ /// 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(TimeSpan timeout, 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 b3e83400593..b5f40bd8ba2 100644
--- a/src/EFCore.Relational/Migrations/HistoryRepositoryDependencies.cs
+++ b/src/EFCore.Relational/Migrations/HistoryRepositoryDependencies.cs
@@ -52,6 +52,7 @@ public HistoryRepositoryDependencies(
IDbContextOptions options,
IMigrationsModelDiffer modelDiffer,
IMigrationsSqlGenerator migrationsSqlGenerator,
+ IMigrationCommandExecutor migrationCommandExecutor,
ISqlGenerationHelper sqlGenerationHelper,
IConventionSetBuilder conventionSetBuilder,
ModelDependencies modelDependencies,
@@ -66,6 +67,7 @@ public HistoryRepositoryDependencies(
Options = options;
ModelDiffer = modelDiffer;
MigrationsSqlGenerator = migrationsSqlGenerator;
+ MigrationCommandExecutor = migrationCommandExecutor;
SqlGenerationHelper = sqlGenerationHelper;
ConventionSetBuilder = conventionSetBuilder;
ModelDependencies = modelDependencies;
@@ -110,6 +112,11 @@ public HistoryRepositoryDependencies(
///
public ISqlGenerationHelper SqlGenerationHelper { get; init; }
+ ///
+ /// The service for executing Migrations operations.
+ ///
+ public IMigrationCommandExecutor MigrationCommandExecutor { get; init; }
+
///
/// The core convention set to use when creating the model.
///
diff --git a/src/EFCore.Relational/Migrations/IHistoryRepository.cs b/src/EFCore.Relational/Migrations/IHistoryRepository.cs
index 70abc61b27d..cce9afe3a65 100644
--- a/src/EFCore.Relational/Migrations/IHistoryRepository.cs
+++ b/src/EFCore.Relational/Migrations/IHistoryRepository.cs
@@ -24,13 +24,13 @@ namespace Microsoft.EntityFrameworkCore.Migrations;
public interface IHistoryRepository
{
///
- /// Checks whether or not the history table exists.
+ /// Checks whether the history table exists.
///
/// if the table already exists, otherwise.
bool Exists();
///
- /// Checks whether or not the history table exists.
+ /// Checks whether the history table exists.
///
/// A to observe while waiting for the task to complete.
///
@@ -40,6 +40,22 @@ public interface IHistoryRepository
/// If the is canceled.
Task ExistsAsync(CancellationToken cancellationToken = default);
+ ///
+ /// Creates the history table.
+ ///
+ void Create();
+
+ ///
+ /// 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 already exists, otherwise.
+ ///
+ /// If the is canceled.
+ Task CreateAsync(CancellationToken cancellationToken = default);
+
///
/// Queries the history table for all migrations that have been applied.
///
@@ -58,6 +74,22 @@ public interface IHistoryRepository
Task> GetAppliedMigrationsAsync(
CancellationToken cancellationToken = default);
+ ///
+ /// Gets an exclusive lock on the database.
+ ///
+ /// The time to wait for the lock before an exception is thrown.
+ /// An object that can be disposed to release the lock.
+ IMigrationDatabaseLock GetDatabaseLock(TimeSpan timeout);
+
+ ///
+ /// Gets an exclusive lock on the database.
+ ///
+ /// The time to wait for the lock before an exception is thrown.
+ /// 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(TimeSpan timeout, CancellationToken cancellationToken = default);
+
///
/// Generates a SQL script that will create the history table.
///
diff --git a/src/EFCore.Relational/Migrations/IMigrationDatabaseLock.cs b/src/EFCore.Relational/Migrations/IMigrationDatabaseLock.cs
new file mode 100644
index 00000000000..da6175e0dda
--- /dev/null
+++ b/src/EFCore.Relational/Migrations/IMigrationDatabaseLock.cs
@@ -0,0 +1,11 @@
+// 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.
+///
+public interface IMigrationDatabaseLock : IDisposable, IAsyncDisposable
+{
+}
diff --git a/src/EFCore.Relational/Migrations/Internal/Migrator.cs b/src/EFCore.Relational/Migrations/Internal/Migrator.cs
index 67d61c60844..6263bec3a9b 100644
--- a/src/EFCore.Relational/Migrations/Internal/Migrator.cs
+++ b/src/EFCore.Relational/Migrations/Internal/Migrator.cs
@@ -61,6 +61,14 @@ public Migrator(
_activeProvider = databaseProvider.Name;
}
+ ///
+ /// 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 TimeSpan LockTimeout { get; } = TimeSpan.FromMinutes(30);
+
///
/// 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
@@ -71,29 +79,32 @@ public virtual void Migrate(string? targetMigration = null)
{
_logger.MigrateUsingConnection(this, _connection);
- if (!_historyRepository.Exists())
+ if (!_databaseCreator.Exists())
+ {
+ _databaseCreator.Create();
+ }
+
+ try
{
- if (!_databaseCreator.Exists())
+ _connection.Open();
+
+ using var _ = _historyRepository.GetDatabaseLock(LockTimeout);
+
+ if (!_historyRepository.Exists())
{
- _databaseCreator.Create();
+ _historyRepository.Create();
}
- var command = _rawSqlCommandBuilder.Build(
- _historyRepository.GetCreateScript());
+ var commandLists = GetMigrationCommandLists(_historyRepository.GetAppliedMigrations(), targetMigration);
- command.ExecuteNonQuery(
- new RelationalCommandParameterObject(
- _connection,
- null,
- null,
- _currentContext.Context,
- _commandLogger, CommandSource.Migrations));
+ foreach (var commandList in commandLists)
+ {
+ _migrationCommandExecutor.ExecuteNonQuery(commandList(), _connection);
+ }
}
-
- var commandLists = GetMigrationCommandLists(_historyRepository.GetAppliedMigrations(), targetMigration);
- foreach (var commandList in commandLists)
+ finally
{
- _migrationCommandExecutor.ExecuteNonQuery(commandList(), _connection);
+ _connection.Close();
}
}
@@ -109,35 +120,36 @@ public virtual async Task MigrateAsync(
{
_logger.MigrateUsingConnection(this, _connection);
- if (!await _historyRepository.ExistsAsync(cancellationToken).ConfigureAwait(false))
+ if (!await _databaseCreator.ExistsAsync(cancellationToken).ConfigureAwait(false))
{
- if (!await _databaseCreator.ExistsAsync(cancellationToken).ConfigureAwait(false))
+ await _databaseCreator.CreateAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ try
+ {
+ await _connection.OpenAsync(cancellationToken).ConfigureAwait(false);
+
+ var dbLock = await _historyRepository.GetDatabaseLockAsync(LockTimeout, cancellationToken).ConfigureAwait(false);
+ await using var _ = dbLock.ConfigureAwait(false);
+
+ if (!await _historyRepository.ExistsAsync(cancellationToken).ConfigureAwait(false))
{
- await _databaseCreator.CreateAsync(cancellationToken).ConfigureAwait(false);
+ await _historyRepository.CreateAsync(cancellationToken).ConfigureAwait(false);
}
- var command = _rawSqlCommandBuilder.Build(
- _historyRepository.GetCreateScript());
-
- await command.ExecuteNonQueryAsync(
- new RelationalCommandParameterObject(
- _connection,
- null,
- null,
- _currentContext.Context,
- _commandLogger, CommandSource.Migrations),
- cancellationToken)
- .ConfigureAwait(false);
- }
-
- var commandLists = GetMigrationCommandLists(
- await _historyRepository.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false),
- targetMigration);
+ var commandLists = GetMigrationCommandLists(
+ await _historyRepository.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false),
+ targetMigration);
- foreach (var commandList in commandLists)
+ foreach (var commandList in commandLists)
+ {
+ await _migrationCommandExecutor.ExecuteNonQueryAsync(commandList(), _connection, cancellationToken)
+ .ConfigureAwait(false);
+ }
+ }
+ finally
{
- await _migrationCommandExecutor.ExecuteNonQueryAsync(commandList(), _connection, cancellationToken)
- .ConfigureAwait(false);
+ _connection.Close();
}
}
@@ -303,8 +315,9 @@ public virtual string GenerateScript(
.Append(_sqlGenerationHelper.BatchTerminator);
}
- var transactionStarted = false;
-
+ var idempotencyEnd = idempotent
+ ? _historyRepository.GetEndIfScript()
+ : null;
for (var i = 0; i < migrationsToRevert.Count; i++)
{
var migration = migrationsToRevert[i];
@@ -314,107 +327,82 @@ public virtual string GenerateScript(
_logger.MigrationGeneratingDownScript(this, migration, fromMigration, toMigration, idempotent);
- foreach (var command in GenerateDownSql(migration, previousMigration, options))
- {
- if (!noTransactions)
- {
- if (!transactionStarted && !command.TransactionSuppressed)
- {
- builder
- .AppendLine(_sqlGenerationHelper.StartTransactionStatement)
- .Append(_sqlGenerationHelper.BatchTerminator);
- transactionStarted = true;
- }
-
- if (transactionStarted && command.TransactionSuppressed)
- {
- builder
- .AppendLine(_sqlGenerationHelper.CommitTransactionStatement)
- .Append(_sqlGenerationHelper.BatchTerminator);
- transactionStarted = false;
- }
- }
+ var idempotencyCondition = idempotent
+ ? _historyRepository.GetBeginIfExistsScript(migration.GetId())
+ : null;
- if (idempotent)
- {
- builder.AppendLine(_historyRepository.GetBeginIfExistsScript(migration.GetId()));
- using (builder.Indent())
- {
- builder.AppendLines(command.CommandText);
- }
-
- builder.Append(_historyRepository.GetEndIfScript());
- }
- else
- {
- builder.Append(command.CommandText);
- }
-
- builder.Append(_sqlGenerationHelper.BatchTerminator);
- }
-
- if (!noTransactions && transactionStarted)
- {
- builder
- .AppendLine(_sqlGenerationHelper.CommitTransactionStatement)
- .Append(_sqlGenerationHelper.BatchTerminator);
- transactionStarted = false;
- }
+ GenerateSqlScript(GenerateDownSql(migration, previousMigration, options), builder, _sqlGenerationHelper, noTransactions, idempotencyCondition, idempotencyEnd);
}
foreach (var migration in migrationsToApply)
{
_logger.MigrationGeneratingUpScript(this, migration, fromMigration, toMigration, idempotent);
- foreach (var command in GenerateUpSql(migration, options))
+ var idempotencyCondition = idempotent
+ ? _historyRepository.GetBeginIfNotExistsScript(migration.GetId())
+ : null;
+
+ GenerateSqlScript(GenerateUpSql(migration, options), builder, _sqlGenerationHelper, noTransactions, idempotencyCondition, idempotencyEnd);
+ }
+
+ return builder.ToString();
+ }
+
+ private static void GenerateSqlScript(
+ IEnumerable commands,
+ IndentedStringBuilder builder,
+ ISqlGenerationHelper sqlGenerationHelper,
+ bool noTransactions = false,
+ string? idempotencyCondition = null,
+ string? idempotencyEnd = null)
+ {
+ var transactionStarted = false;
+ foreach (var command in commands)
+ {
+ if (!noTransactions)
{
- if (!noTransactions)
+ if (!transactionStarted && !command.TransactionSuppressed)
{
- if (!transactionStarted && !command.TransactionSuppressed)
- {
- builder
- .AppendLine(_sqlGenerationHelper.StartTransactionStatement)
- .Append(_sqlGenerationHelper.BatchTerminator);
- transactionStarted = true;
- }
-
- if (transactionStarted && command.TransactionSuppressed)
- {
- builder
- .AppendLine(_sqlGenerationHelper.CommitTransactionStatement)
- .Append(_sqlGenerationHelper.BatchTerminator);
- transactionStarted = false;
- }
+ builder
+ .AppendLine(sqlGenerationHelper.StartTransactionStatement)
+ .Append(sqlGenerationHelper.BatchTerminator);
+ transactionStarted = true;
}
- if (idempotent)
+ if (transactionStarted && command.TransactionSuppressed)
{
- builder.AppendLine(_historyRepository.GetBeginIfNotExistsScript(migration.GetId()));
- using (builder.Indent())
- {
- builder.AppendLines(command.CommandText);
- }
-
- builder.Append(_historyRepository.GetEndIfScript());
+ builder
+ .AppendLine(sqlGenerationHelper.CommitTransactionStatement)
+ .Append(sqlGenerationHelper.BatchTerminator);
+ transactionStarted = false;
}
- else
+ }
+
+ if (idempotencyCondition != null
+ && idempotencyEnd != null)
+ {
+ builder.AppendLine(idempotencyCondition);
+ using (builder.Indent())
{
- builder.Append(command.CommandText);
+ builder.AppendLines(command.CommandText);
}
- builder.Append(_sqlGenerationHelper.BatchTerminator);
+ builder.Append(idempotencyEnd);
}
-
- if (!noTransactions && transactionStarted)
+ else
{
- builder
- .AppendLine(_sqlGenerationHelper.CommitTransactionStatement)
- .Append(_sqlGenerationHelper.BatchTerminator);
- transactionStarted = false;
+ builder.Append(command.CommandText);
}
+
+ builder.Append(sqlGenerationHelper.BatchTerminator);
}
- return builder.ToString();
+ if (!noTransactions && transactionStarted)
+ {
+ builder
+ .AppendLine(sqlGenerationHelper.CommitTransactionStatement)
+ .Append(sqlGenerationHelper.BatchTerminator);
+ }
}
///
@@ -432,7 +420,7 @@ protected virtual IReadOnlyList GenerateUpSql(
return _migrationsSqlGenerator
.Generate(migration.UpOperations, FinalizeModel(migration.TargetModel), options)
- .Concat(new[] { new MigrationCommand(insertCommand, _currentContext.Context, _commandLogger) })
+ .Concat([new MigrationCommand(insertCommand, _currentContext.Context, _commandLogger)])
.ToList();
}
@@ -453,7 +441,7 @@ protected virtual IReadOnlyList GenerateDownSql(
return _migrationsSqlGenerator
.Generate(
migration.DownOperations, previousMigration == null ? null : FinalizeModel(previousMigration.TargetModel), options)
- .Concat(new[] { new MigrationCommand(deleteCommand, _currentContext.Context, _commandLogger) })
+ .Concat([new MigrationCommand(deleteCommand, _currentContext.Context, _commandLogger)])
.ToList();
}
diff --git a/src/EFCore.SqlServer/Migrations/Internal/SqlServerHistoryRepository.cs b/src/EFCore.SqlServer/Migrations/Internal/SqlServerHistoryRepository.cs
index 1169be0a89d..f1c81949784 100644
--- a/src/EFCore.SqlServer/Migrations/Internal/SqlServerHistoryRepository.cs
+++ b/src/EFCore.SqlServer/Migrations/Internal/SqlServerHistoryRepository.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text;
+using Microsoft.Data.SqlClient;
namespace Microsoft.EntityFrameworkCore.SqlServer.Migrations.Internal;
@@ -53,6 +54,94 @@ protected override string ExistsSql
protected override bool InterpretExistsResult(object? value)
=> value != DBNull.Value;
+ ///
+ /// 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 IMigrationDatabaseLock GetDatabaseLock(TimeSpan timeout)
+ {
+ var dbLock = CreateMigrationDatabaseLock();
+ int result;
+ try
+ {
+ result = (int)CreateGetLockCommand(timeout).ExecuteScalar(CreateRelationalCommandParameters())!;
+ }
+ catch
+ {
+ try
+ {
+ dbLock.Dispose();
+ }
+ catch
+ {
+ }
+
+ throw;
+ }
+
+ return result < 0
+ ? throw new TimeoutException()
+ : dbLock;
+ }
+
+ ///
+ /// 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 async Task GetDatabaseLockAsync(TimeSpan timeout, CancellationToken cancellationToken = default)
+ {
+ var dbLock = CreateMigrationDatabaseLock();
+ int result;
+ try
+ {
+ result = (int)(await CreateGetLockCommand(timeout).ExecuteScalarAsync(CreateRelationalCommandParameters(), cancellationToken)
+ .ConfigureAwait(false))!;
+ }
+ catch (Exception)
+ {
+ try
+ {
+ await dbLock.DisposeAsync().ConfigureAwait(false);
+ }
+ catch (Exception)
+ {
+ }
+
+ throw;
+ }
+
+ return result < 0
+ ? throw new TimeoutException()
+ : dbLock;
+ }
+
+ private IRelationalCommand CreateGetLockCommand(TimeSpan timeout)
+ => Dependencies.RawSqlCommandBuilder.Build(@"
+DECLARE @result int;
+EXEC @result = sp_getapplock @Resource = '__EFLock', @LockOwner = 'Session', @LockMode = 'Exclusive', @LockTimeout = @LockTimeout;
+SELECT @result",
+ [new SqlParameter("@LockTimeout", timeout.TotalMilliseconds)]).RelationalCommand;
+
+ private SqlServerMigrationDatabaseLock CreateMigrationDatabaseLock()
+ => new SqlServerMigrationDatabaseLock(
+ Dependencies.RawSqlCommandBuilder.Build(@"
+DECLARE @result int;
+EXEC @result = sp_releaseapplock @Resource = '__EFLock', @LockOwner = 'Session';
+SELECT @result"),
+ CreateRelationalCommandParameters());
+
+ private RelationalCommandParameterObject CreateRelationalCommandParameters()
+ => new(
+ Dependencies.Connection,
+ null,
+ null,
+ Dependencies.CurrentContext.Context,
+ Dependencies.CommandLogger, CommandSource.Migrations);
+
///
/// 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.SqlServer/Migrations/Internal/SqlServerMigrationDatabaseLock.cs b/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationDatabaseLock.cs
new file mode 100644
index 00000000000..78b85c54abf
--- /dev/null
+++ b/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationDatabaseLock.cs
@@ -0,0 +1,42 @@
+// 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.SqlServer.Migrations.Internal;
+
+///
+/// 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.
+///
+///
+/// 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 SqlServerMigrationDatabaseLock
+ (IRelationalCommand relationalCommand,
+ RelationalCommandParameterObject relationalCommandParameters,
+ CancellationToken cancellationToken = default)
+ : IMigrationDatabaseLock
+{
+
+ ///
+ /// 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 void Dispose()
+ => relationalCommand.ExecuteScalar(relationalCommandParameters);
+
+ ///
+ /// 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 async ValueTask DisposeAsync()
+ => await relationalCommand.ExecuteScalarAsync(relationalCommandParameters, cancellationToken).ConfigureAwait(false);
+}
diff --git a/src/EFCore.Sqlite.Core/Migrations/Internal/SqliteHistoryRepository.cs b/src/EFCore.Sqlite.Core/Migrations/Internal/SqliteHistoryRepository.cs
index 1d0a57fd312..f968d0e0350 100644
--- a/src/EFCore.Sqlite.Core/Migrations/Internal/SqliteHistoryRepository.cs
+++ b/src/EFCore.Sqlite.Core/Migrations/Internal/SqliteHistoryRepository.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.Text;
using Microsoft.EntityFrameworkCore.Sqlite.Internal;
namespace Microsoft.EntityFrameworkCore.Sqlite.Migrations.Internal;
@@ -13,6 +14,8 @@ namespace Microsoft.EntityFrameworkCore.Sqlite.Migrations.Internal;
///
public class SqliteHistoryRepository : HistoryRepository
{
+ private static readonly TimeSpan _retryDelay = TimeSpan.FromSeconds(1);
+
///
/// 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
@@ -30,16 +33,20 @@ public SqliteHistoryRepository(HistoryRepositoryDependencies dependencies)
/// 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 override string ExistsSql
+ protected override string ExistsSql => CreateExistsSql(TableName);
+
+ ///
+ /// The name of the table that will serve as a database-wide lock for migrations.
+ ///
+ protected virtual string LockTableName { get; } = "__EFLock";
+
+ private string CreateExistsSql(string tableName)
{
- get
- {
- var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string));
+ var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string));
- return "SELECT COUNT(*) FROM \"sqlite_master\" WHERE \"name\" = "
- + stringTypeMapping.GenerateSqlLiteral(TableName)
- + " AND \"type\" = 'table';";
- }
+ return "SELECT COUNT(*) FROM \"sqlite_master\" WHERE \"name\" = "
+ + stringTypeMapping.GenerateSqlLiteral(tableName)
+ + " AND \"type\" = 'table';";
}
///
@@ -60,7 +67,6 @@ protected override bool InterpretExistsResult(object? value)
public override string GetCreateIfNotExistsScript()
{
var script = GetCreateScript();
-
return script.Insert(script.IndexOf("CREATE TABLE", StringComparison.Ordinal) + 12, " IF NOT EXISTS");
}
@@ -90,4 +96,171 @@ public override string GetBeginIfExistsScript(string migrationId)
///
public override string GetEndIfScript()
=> throw new NotSupportedException(SqliteStrings.MigrationScriptGenerationNotSupported);
+
+ ///
+ /// 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 IMigrationDatabaseLock GetDatabaseLock(TimeSpan timeout)
+ {
+ if (!InterpretExistsResult(Dependencies.RawSqlCommandBuilder.Build(CreateExistsSql(LockTableName))
+ .ExecuteScalar(CreateRelationalCommandParameters())))
+ {
+ CreateLockTableCommand().ExecuteNonQuery(CreateRelationalCommandParameters());
+ }
+
+ var retryDelay = _retryDelay;
+ var startTime = DateTimeOffset.UtcNow;
+ while (DateTimeOffset.UtcNow - startTime < timeout)
+ {
+ var dbLock = CreateMigrationDatabaseLock();
+ var insertCount = CreateInsertLockCommand(DateTimeOffset.UtcNow)
+ .ExecuteScalar(CreateRelationalCommandParameters());
+ if ((long)insertCount! == 1)
+ {
+ return dbLock;
+ }
+
+ using var reader = CreateGetLockCommand().ExecuteReader(CreateRelationalCommandParameters());
+ if (reader.Read())
+ {
+ var timestamp = reader.DbDataReader.GetFieldValue(1);
+ if (DateTimeOffset.UtcNow - timestamp > timeout)
+ {
+ var id = reader.DbDataReader.GetFieldValue(0);
+ CreateDeleteLockCommand(id).ExecuteNonQuery(CreateRelationalCommandParameters());
+ }
+ }
+
+ using var waitEvent = new ManualResetEventSlim(false);
+ waitEvent.WaitHandle.WaitOne(retryDelay);
+ if (retryDelay < TimeSpan.FromMinutes(1))
+ {
+ retryDelay = retryDelay.Add(retryDelay);
+ }
+ }
+
+ throw new TimeoutException();
+ }
+
+ ///
+ /// 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 async Task GetDatabaseLockAsync(TimeSpan timeout, CancellationToken cancellationToken = default)
+ {
+ if (!InterpretExistsResult(await Dependencies.RawSqlCommandBuilder.Build(CreateExistsSql(LockTableName))
+ .ExecuteScalarAsync(CreateRelationalCommandParameters(), cancellationToken).ConfigureAwait(false)))
+ {
+ await CreateLockTableCommand().ExecuteNonQueryAsync(CreateRelationalCommandParameters(), cancellationToken).ConfigureAwait(false);
+ }
+
+ var retryDelay = _retryDelay;
+ var startTime = DateTimeOffset.UtcNow;
+ while (DateTimeOffset.UtcNow - startTime < timeout)
+ {
+ var dbLock = CreateMigrationDatabaseLock();
+ var insertCount = await CreateInsertLockCommand(DateTimeOffset.UtcNow)
+ .ExecuteScalarAsync(CreateRelationalCommandParameters(), cancellationToken)
+ .ConfigureAwait(false);
+ if ((long)insertCount! == 1)
+ {
+ return dbLock;
+ }
+
+ using var reader = await CreateGetLockCommand().ExecuteReaderAsync(CreateRelationalCommandParameters(), cancellationToken)
+ .ConfigureAwait(false);
+ if (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
+ {
+ var timestamp = await reader.DbDataReader.GetFieldValueAsync(1).ConfigureAwait(false);
+ if (DateTimeOffset.UtcNow - timestamp > timeout)
+ {
+ var id = await reader.DbDataReader.GetFieldValueAsync(0).ConfigureAwait(false);
+ await CreateDeleteLockCommand(id).ExecuteNonQueryAsync(CreateRelationalCommandParameters(), cancellationToken)
+ .ConfigureAwait(false);
+ }
+ }
+
+ await Task.Delay(_retryDelay, cancellationToken).ConfigureAwait(true);
+ if (retryDelay < TimeSpan.FromMinutes(1))
+ {
+ retryDelay = retryDelay.Add(retryDelay);
+ }
+ }
+
+ throw new TimeoutException();
+ }
+
+ private IRelationalCommand CreateLockTableCommand()
+ => Dependencies.RawSqlCommandBuilder.Build($"""
+CREATE TABLE IF NOT EXISTS "{LockTableName}" (
+ "Id" INTEGER NOT NULL CONSTRAINT "PK___EFLock" PRIMARY KEY,
+ "Timestamp" TEXT NOT NULL
+);
+""");
+
+ private IRelationalCommand CreateInsertLockCommand(DateTimeOffset timestamp)
+ {
+ var timestampLiteral = Dependencies.TypeMappingSource.GetMapping(typeof(DateTimeOffset)).GenerateSqlLiteral(timestamp);
+
+ return Dependencies.RawSqlCommandBuilder.Build($"""
+INSERT OR IGNORE INTO "{LockTableName}"("Id", "Timestamp") VALUES(1, {timestampLiteral});
+SELECT changes();
+""");
+ }
+
+ private IRelationalCommand CreateGetLockCommand()
+ => Dependencies.RawSqlCommandBuilder.Build($"""
+SELECT "Id", "Timestamp" FROM "{LockTableName}" LIMIT 1;
+""");
+
+ private IRelationalCommand CreateDeleteLockCommand(int? id = null)
+ {
+ var sql = $"""
+DELETE FROM "{LockTableName}"
+""";
+ if (id != null)
+ {
+ sql += $"""
+WHERE "Id" = {id}
+""";
+ }
+ sql += ";";
+ return Dependencies.RawSqlCommandBuilder.Build(sql);
+ }
+
+ private SqliteMigrationDatabaseLock CreateMigrationDatabaseLock()
+ => new(CreateDeleteLockCommand(), CreateRelationalCommandParameters());
+
+ private string GetLockInsertScript(HistoryRow row)
+ {
+ var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string));
+
+ return new StringBuilder().Append("INSERT INTO ")
+ .Append(SqlGenerationHelper.DelimitIdentifier(TableName, TableSchema))
+ .Append(" (")
+ .Append(SqlGenerationHelper.DelimitIdentifier(MigrationIdColumnName))
+ .Append(", ")
+ .Append(SqlGenerationHelper.DelimitIdentifier(ProductVersionColumnName))
+ .AppendLine(")")
+ .Append("VALUES (")
+ .Append(stringTypeMapping.GenerateSqlLiteral(row.MigrationId))
+ .Append(", ")
+ .Append(stringTypeMapping.GenerateSqlLiteral(row.ProductVersion))
+ .Append(')')
+ .AppendLine(SqlGenerationHelper.StatementTerminator)
+ .ToString();
+ }
+
+ private RelationalCommandParameterObject CreateRelationalCommandParameters()
+ => new(
+ Dependencies.Connection,
+ null,
+ null,
+ Dependencies.CurrentContext.Context,
+ Dependencies.CommandLogger, CommandSource.Migrations);
}
diff --git a/src/EFCore.Sqlite.Core/Migrations/Internal/SqliteMigrationDatabaseLock.cs b/src/EFCore.Sqlite.Core/Migrations/Internal/SqliteMigrationDatabaseLock.cs
new file mode 100644
index 00000000000..8063aa4fb6c
--- /dev/null
+++ b/src/EFCore.Sqlite.Core/Migrations/Internal/SqliteMigrationDatabaseLock.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.Sqlite.Migrations.Internal;
+
+///
+/// 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 SqliteMigrationDatabaseLock
+ (IRelationalCommand relationalCommand,
+ RelationalCommandParameterObject relationalCommandParameters,
+ CancellationToken cancellationToken = default)
+ : IMigrationDatabaseLock
+{
+ ///
+ /// 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 void Dispose()
+ => relationalCommand.ExecuteScalar(relationalCommandParameters);
+
+ ///
+ /// 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 async ValueTask DisposeAsync()
+ => await relationalCommand.ExecuteScalarAsync(relationalCommandParameters, cancellationToken).ConfigureAwait(false);
+}
diff --git a/src/EFCore/AutoTransactionBehavior.cs b/src/EFCore/AutoTransactionBehavior.cs
index 25d6e11db23..bd34420276e 100644
--- a/src/EFCore/AutoTransactionBehavior.cs
+++ b/src/EFCore/AutoTransactionBehavior.cs
@@ -4,7 +4,7 @@
namespace Microsoft.EntityFrameworkCore;
///
-/// Indicates whether or not a transaction will be created automatically by if a user transaction
+/// Indicates whether a transaction will be created automatically by if a user transaction
/// wasn't created via 'BeginTransaction' or provided via 'UseTransaction'.
///
public enum AutoTransactionBehavior
diff --git a/src/EFCore/Diagnostics/CoreLoggerExtensions.cs b/src/EFCore/Diagnostics/CoreLoggerExtensions.cs
index a41f9621ec1..cda0afbff27 100644
--- a/src/EFCore/Diagnostics/CoreLoggerExtensions.cs
+++ b/src/EFCore/Diagnostics/CoreLoggerExtensions.cs
@@ -2855,7 +2855,7 @@ private static string StateChangedSensitive(EventDefinitionBase definition, Even
/// The internal entity entry.
/// The property.
/// The value generated.
- /// Indicates whether or not the value is a temporary or permanent value.
+ /// Indicates whether the value is a temporary or permanent value.
public static void ValueGenerated(
this IDiagnosticsLogger diagnostics,
InternalEntityEntry internalEntityEntry,
diff --git a/src/EFCore/Diagnostics/ExecutionStrategyEventData.cs b/src/EFCore/Diagnostics/ExecutionStrategyEventData.cs
index 2557b05a788..d93f36a7afa 100644
--- a/src/EFCore/Diagnostics/ExecutionStrategyEventData.cs
+++ b/src/EFCore/Diagnostics/ExecutionStrategyEventData.cs
@@ -24,7 +24,7 @@ public class ExecutionStrategyEventData : EventData
///
/// The delay before retrying the operation.
///
- /// Indicates whether or not the command was executed asynchronously.
+ /// Indicates whether the command was executed asynchronously.
///
public ExecutionStrategyEventData(
EventDefinitionBase eventDefinition,
@@ -50,7 +50,7 @@ public ExecutionStrategyEventData(
public virtual TimeSpan Delay { get; }
///
- /// Indicates whether or not the operation is being executed asynchronously.
+ /// Indicates whether the operation is being executed asynchronously.
///
public virtual bool IsAsync { get; }
}
diff --git a/src/EFCore/Diagnostics/IDbContextLogger.cs b/src/EFCore/Diagnostics/IDbContextLogger.cs
index 797242a4d3f..04857fb4e82 100644
--- a/src/EFCore/Diagnostics/IDbContextLogger.cs
+++ b/src/EFCore/Diagnostics/IDbContextLogger.cs
@@ -36,7 +36,7 @@ public interface IDbContextLogger
void Log(EventData eventData);
///
- /// Determines whether or not the given event should be logged.
+ /// Determines whether the given event should be logged.
///
/// The ID of the event.
/// The level of the event.
diff --git a/src/EFCore/Diagnostics/IDiagnosticsLogger.cs b/src/EFCore/Diagnostics/IDiagnosticsLogger.cs
index 7f969ce3afa..a06e739d24c 100644
--- a/src/EFCore/Diagnostics/IDiagnosticsLogger.cs
+++ b/src/EFCore/Diagnostics/IDiagnosticsLogger.cs
@@ -61,7 +61,7 @@ public interface IDiagnosticsLogger
IInterceptors? Interceptors { get; }
///
- /// Checks whether or not the message should be sent to the .
+ /// Checks whether the message should be sent to the .
///
/// The definition of the event to log.
///
@@ -104,7 +104,7 @@ void DispatchEventData(
}
///
- /// Determines whether or not an instance is needed based on whether or
+ /// Determines whetheran instance is needed based on whether or
/// not there is a or an enabled for
/// the given event.
///
@@ -136,7 +136,7 @@ bool NeedsEventData(
}
///
- /// Determines whether or not an instance is needed based on whether or
+ /// Determines whether an instance is needed based on whether or
/// not there is a , an , or an enabled for
/// the given event.
///
diff --git a/src/EFCore/Diagnostics/ILoggingOptions.cs b/src/EFCore/Diagnostics/ILoggingOptions.cs
index 82b5881a2fb..8eae16e3809 100644
--- a/src/EFCore/Diagnostics/ILoggingOptions.cs
+++ b/src/EFCore/Diagnostics/ILoggingOptions.cs
@@ -44,6 +44,6 @@ public interface ILoggingOptions : ISingletonOptions
/// Returns if a warning about string values for the given enum type has not yet been performed.
///
/// The type to check.
- /// Whether or not a warning has been issued.
+ /// Whether a warning has been issued.
bool ShouldWarnForStringEnumValueInJson(Type enumType);
}
diff --git a/src/Microsoft.Data.Sqlite.Core/SqliteConnection.cs b/src/Microsoft.Data.Sqlite.Core/SqliteConnection.cs
index a59d762fce6..547d35b39d9 100644
--- a/src/Microsoft.Data.Sqlite.Core/SqliteConnection.cs
+++ b/src/Microsoft.Data.Sqlite.Core/SqliteConnection.cs
@@ -347,14 +347,8 @@ public override void Close()
// NB: Calls RemoveCommand()
command.Dispose();
}
- else
- {
- _commands.Remove(reference);
- }
}
- Debug.Assert(_commands.Count == 0);
-
_commands.Clear();
_innerConnection!.Close();
_innerConnection = null;
@@ -450,7 +444,9 @@ internal void RemoveCommand(SqliteCommand command)
{
for (var i = _commands.Count - 1; i >= 0; i--)
{
- if (_commands[i].TryGetTarget(out var item)
+ var reference = _commands[i];
+ if (reference != null
+ && reference.TryGetTarget(out var item)
&& item == command)
{
_commands.RemoveAt(i);
diff --git a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsInfrastructureTestBase.cs b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsInfrastructureTestBase.cs
index c1ce90789ee..a75f37bd012 100644
--- a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsInfrastructureTestBase.cs
+++ b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsInfrastructureTestBase.cs
@@ -3,6 +3,8 @@
// ReSharper disable InconsistentNaming
+using Microsoft.EntityFrameworkCore.TestUtilities;
+
namespace Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
@@ -22,6 +24,8 @@ protected MigrationsInfrastructureTestBase(TFixture fixture)
protected string ActiveProvider { get; private set; }
+ public static IEnumerable