Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/EFCore.Relational/Diagnostics/RelationalEventId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ private enum Id
PendingModelChangesWarning,
NonTransactionalMigrationOperationWarning,
AcquiringMigrationLock,
MigrationsUserTransactionWarning,

// Query events
QueryClientEvaluationWarning = CoreEventId.RelationalBaseId + 500,
Expand Down Expand Up @@ -764,6 +765,19 @@ private static EventId MakeMigrationsId(Id id)
/// </remarks>
public static readonly EventId AcquiringMigrationLock = MakeMigrationsId(Id.AcquiringMigrationLock);

/// <summary>
/// A migration lock is being acquired.
/// </summary>
/// <remarks>
/// <para>
/// This event is in the <see cref="DbLoggerCategory.Migrations" /> category.
/// </para>
/// <para>
/// This event uses the <see cref="EventData" /> payload when used with a <see cref="DiagnosticSource" />.
/// </para>
/// </remarks>
public static readonly EventId MigrationsUserTransactionWarning = MakeMigrationsId(Id.MigrationsUserTransactionWarning);

private static readonly string _queryPrefix = DbLoggerCategory.Query.Name + ".";

private static EventId MakeQueryId(Id id)
Expand Down
30 changes: 30 additions & 0 deletions src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2425,6 +2425,36 @@ private static string AcquiringMigrationLock(EventDefinitionBase definition, Eve
return d.GenerateMessage();
}

/// <summary>
/// Logs for the <see cref="RelationalEventId.MigrationsUserTransactionWarning" /> event.
/// </summary>
/// <param name="diagnostics">The diagnostics logger to use.</param>
public static void MigrationsUserTransactionWarning(
this IDiagnosticsLogger<DbLoggerCategory.Migrations> 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();
}

/// <summary>
/// Logs for the <see cref="RelationalEventId.QueryPossibleUnintendedUseOfEqualsWarning" /> event.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,15 @@ public abstract class RelationalLoggingDefinitions : LoggingDefinitions
[EntityFrameworkInternal]
public EventDefinitionBase? LogAcquiringMigrationLock;

/// <summary>
/// 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.
/// </summary>
[EntityFrameworkInternal]
public EventDefinitionBase? LogMigrationsUserTransactionWarning;

/// <summary>
/// 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
Expand Down
72 changes: 51 additions & 21 deletions src/EFCore.Relational/Migrations/HistoryRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ protected HistoryRepository(HistoryRepositoryDependencies dependencies)
TableSchema = relationalOptions.MigrationsHistoryTableSchema;
}

/// <summary>
/// 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.
/// </summary>
public abstract LockReleaseBehavior LockReleaseBehavior { get; }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming: I'd love to just call this LockingScope, but it's true that for Explicit we still take it ourselves, so it seems to indeed apply only to the release part...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dotnet/efteam API review question

Copy link
Copy Markdown
Member

@roji roji Aug 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am also OK with the current naming if we don't get around to discussing this (same with the other naming proposals below).

Copy link
Copy Markdown
Member Author

@AndriySvyryd AndriySvyryd Aug 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. We won't have any meetings for this, so if anyone else in @dotnet/efteam has an opinion - speak up


/// <summary>
/// Relational provider-specific dependencies for this service.
/// </summary>
Expand Down Expand Up @@ -120,14 +128,15 @@ protected virtual string ProductVersionColumnName
/// </summary>
/// <returns><see langword="true" /> if the table already exists, <see langword="false" /> otherwise.</returns>
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)));

/// <summary>
/// Checks whether or not the history table exists.
Expand All @@ -139,15 +148,16 @@ public virtual bool Exists()
/// </returns>
/// <exception cref="OperationCanceledException">If the <see cref="CancellationToken" /> is canceled.</exception>
public virtual async Task<bool> 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));

/// <summary>
/// Interprets the result of executing <see cref="ExistsSql" />.
Expand All @@ -173,13 +183,15 @@ public virtual string GetCreateScript()
/// Creates the history table.
/// </summary>
public virtual void Create()
=> Dependencies.MigrationCommandExecutor.ExecuteNonQuery(GetCreateCommands(), Dependencies.Connection);
=> Dependencies.MigrationCommandExecutor.ExecuteNonQuery(
GetCreateCommands(), Dependencies.Connection, new MigrationExecutionState(), commitTransaction: true);

/// <summary>
/// Creates the history table.
/// </summary>
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);

/// <summary>
/// Returns the migration commands that will create the history table.
Expand All @@ -194,19 +206,37 @@ protected virtual IReadOnlyList<MigrationCommand> GetCreateCommands()
return commandList;
}

bool IHistoryRepository.CreateIfNotExists()
=> Dependencies.MigrationCommandExecutor.ExecuteNonQuery(
GetCreateIfNotExistsCommands(), Dependencies.Connection, new MigrationExecutionState(), commitTransaction: true)
!= 0;

async Task<bool> IHistoryRepository.CreateIfNotExistsAsync(CancellationToken cancellationToken)
=> (await Dependencies.MigrationCommandExecutor.ExecuteNonQueryAsync(
GetCreateIfNotExistsCommands(), Dependencies.Connection, new MigrationExecutionState(), commitTransaction: true, cancellationToken: cancellationToken).ConfigureAwait(false))
!= 0;

private IReadOnlyList<MigrationCommand> GetCreateIfNotExistsCommands()
=> Dependencies.MigrationsSqlGenerator.Generate([new SqlOperation
{
Sql = GetCreateIfNotExistsScript(),
SuppressTransaction = true
}]);

/// <summary>
/// Gets an exclusive lock on the database.
/// </summary>
/// <returns>An object that can be disposed to release the lock.</returns>
public abstract IDisposable GetDatabaseLock();
public abstract IMigrationsDatabaseLock AcquireDatabaseLock();

/// <summary>
/// Gets an exclusive lock on the database.
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
///
/// <returns>An object that can be disposed to release the lock.</returns>
/// <exception cref="OperationCanceledException">If the <see cref="CancellationToken" /> is canceled.</exception>
public abstract Task<IAsyncDisposable> GetDatabaseLockAsync(CancellationToken cancellationToken = default);
public abstract Task<IMigrationsDatabaseLock> AcquireDatabaseLockAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Configures the entity type mapped to the history table.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ public HistoryRepositoryDependencies(
IRelationalTypeMappingSource typeMappingSource,
ICurrentDbContext currentContext,
IModelRuntimeInitializer modelRuntimeInitializer,
IRelationalCommandDiagnosticsLogger commandLogger)
IRelationalCommandDiagnosticsLogger commandLogger,
IDiagnosticsLogger<DbLoggerCategory.Migrations> migrationsLogger)
{
DatabaseCreator = databaseCreator;
RawSqlCommandBuilder = rawSqlCommandBuilder;
Expand All @@ -75,6 +76,7 @@ public HistoryRepositoryDependencies(
CurrentContext = currentContext;
ModelRuntimeInitializer = modelRuntimeInitializer;
CommandLogger = commandLogger;
MigrationsLogger = migrationsLogger;
}

/// <summary>
Expand Down Expand Up @@ -146,4 +148,9 @@ public HistoryRepositoryDependencies(
/// The command logger
/// </summary>
public IRelationalCommandDiagnosticsLogger CommandLogger { get; init; }

/// <summary>
/// The migrations logger
/// </summary>
public IDiagnosticsLogger<DbLoggerCategory.Migrations> MigrationsLogger { get; init; }
}
49 changes: 43 additions & 6 deletions src/EFCore.Relational/Migrations/IHistoryRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,44 @@ public interface IHistoryRepository
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>
/// A task that represents the asynchronous operation. The task result contains
/// <see langword="true" /> if the table already exists, <see langword="false" /> otherwise.
/// A task that represents the asynchronous operation.
/// </returns>
/// <exception cref="OperationCanceledException">If the <see cref="CancellationToken" /> is canceled.</exception>
Task CreateAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Creates the history table if it doesn't exist.
/// </summary>
/// <returns><see langword="true" /> if the table was created, <see langword="false" /> otherwise.</returns>
bool CreateIfNotExists()
{
if (!Exists())
{
Create();
return true;
}
return false;
}

/// <summary>
/// Creates the history table.
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>
/// A task that represents the asynchronous operation. The task result contains
/// <see langword="true" /> if the table was created, <see langword="false" /> otherwise.
/// </returns>
/// <exception cref="OperationCanceledException">If the <see cref="CancellationToken" /> is canceled.</exception>
async Task<bool> CreateIfNotExistsAsync(CancellationToken cancellationToken = default)
{
if (!await ExistsAsync(cancellationToken).ConfigureAwait(false))
{
await CreateAsync(cancellationToken).ConfigureAwait(false);
return true;
}
return false;
}

/// <summary>
/// Queries the history table for all migrations that have been applied.
/// </summary>
Expand All @@ -75,18 +107,23 @@ Task<IReadOnlyList<HistoryRow>> GetAppliedMigrationsAsync(
CancellationToken cancellationToken = default);

/// <summary>
/// Gets an exclusive lock on the database.
/// The condition under witch the lock is released implicitly.
/// </summary>
LockReleaseBehavior LockReleaseBehavior { get; }

/// <summary>
/// Acquires an exclusive lock on the database.
/// </summary>
/// <returns>An object that can be disposed to release the lock.</returns>
IDisposable GetDatabaseLock();
IMigrationsDatabaseLock AcquireDatabaseLock();

/// <summary>
/// Gets an exclusive lock on the database.
/// Acquires an exclusive lock on the database asynchronously.
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>An object that can be disposed to release the lock.</returns>
/// <exception cref="OperationCanceledException">If the <see cref="CancellationToken" /> is canceled.</exception>
Task<IAsyncDisposable> GetDatabaseLockAsync(CancellationToken cancellationToken = default);
Task<IMigrationsDatabaseLock> AcquireDatabaseLockAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Generates a SQL script that will create the history table.
Expand Down
42 changes: 42 additions & 0 deletions src/EFCore.Relational/Migrations/IMigrationCommandExecutor.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
Expand Down Expand Up @@ -28,6 +30,24 @@ void ExecuteNonQuery(
IEnumerable<MigrationCommand> migrationCommands,
IRelationalConnection connection);

/// <summary>
/// Executes the given commands using the given database connection.
/// </summary>
/// <param name="migrationCommands">The commands to execute.</param>
/// <param name="connection">The connection to use.</param>
/// <param name="executionState">The state of the current migration execution.</param>
/// <param name="commitTransaction">
/// Indicates whether the transaction started by this call should be commited.
/// If <see langword="false" />, the transaction will be made available in <paramref name="executionState"/>.
/// </param>
/// <param name="isolationLevel">The isolation level for the transaction.</param>
int ExecuteNonQuery(
IReadOnlyList<MigrationCommand> migrationCommands,
IRelationalConnection connection,
MigrationExecutionState executionState,
bool commitTransaction,
IsolationLevel? isolationLevel = null);

/// <summary>
/// Executes the given commands using the given database connection.
/// </summary>
Expand All @@ -40,4 +60,26 @@ Task ExecuteNonQueryAsync(
IEnumerable<MigrationCommand> migrationCommands,
IRelationalConnection connection,
CancellationToken cancellationToken = default);

/// <summary>
/// Executes the given commands using the given database connection.
/// </summary>
/// <param name="migrationCommands">The commands to execute.</param>
/// <param name="connection">The connection to use.</param>
/// <param name="executionState">The state of the current migration execution.</param>
/// <param name="commitTransaction">
/// Indicates whether the transaction started by this call should be commited.
/// If <see langword="false" />, the transaction will be made available in <paramref name="executionState"/>.
/// </param>
/// <param name="isolationLevel">The isolation level for the transaction.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
/// <exception cref="OperationCanceledException">If the <see cref="CancellationToken" /> is canceled.</exception>
Task<int> ExecuteNonQueryAsync(
IReadOnlyList<MigrationCommand> migrationCommands,
IRelationalConnection connection,
MigrationExecutionState executionState,
bool commitTransaction,
IsolationLevel? isolationLevel = null,
CancellationToken cancellationToken = default);
}
Loading