diff --git a/src/EFCore.Relational/Diagnostics/MigrationCommandEventData.cs b/src/EFCore.Relational/Diagnostics/MigrationCommandEventData.cs
new file mode 100644
index 00000000000..3fc5dd35b01
--- /dev/null
+++ b/src/EFCore.Relational/Diagnostics/MigrationCommandEventData.cs
@@ -0,0 +1,50 @@
+// 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.Diagnostics;
+
+///
+/// The event payload for
+/// events of a specific migration.
+///
+///
+/// See Logging, events, and diagnostics for more information and examples.
+///
+public class MigrationCommandEventData : MigratorEventData
+{
+ ///
+ /// Constructs the event payload.
+ ///
+ /// The event definition.
+ /// A delegate that generates a log message for this event.
+ ///
+ /// The in use.
+ ///
+ ///
+ /// The being processed.
+ ///
+ ///
+ /// The being processed.
+ ///
+ public MigrationCommandEventData(
+ EventDefinitionBase eventDefinition,
+ Func messageGenerator,
+ IMigrator migrator,
+ Migration migration,
+ MigrationCommand command)
+ : base(eventDefinition, messageGenerator, migrator)
+ {
+ Migration = migration;
+ MigrationCommand = command;
+ }
+
+ ///
+ /// The being processed.
+ ///
+ public virtual Migration Migration { get; }
+
+ ///
+ /// The being processed.
+ ///
+ public virtual MigrationCommand MigrationCommand { get; }
+}
diff --git a/src/EFCore.Relational/Diagnostics/RelationalEventId.cs b/src/EFCore.Relational/Diagnostics/RelationalEventId.cs
index c8f84558c2a..f487c54ff62 100644
--- a/src/EFCore.Relational/Diagnostics/RelationalEventId.cs
+++ b/src/EFCore.Relational/Diagnostics/RelationalEventId.cs
@@ -77,6 +77,8 @@ private enum Id
MigrationsNotFound,
MigrationAttributeMissingWarning,
ColumnOrderIgnoredWarning,
+ PendingModelChangesWarning,
+ NonTransactionalMigrationOperationWarning,
// Query events
QueryClientEvaluationWarning = CoreEventId.RelationalBaseId + 500,
@@ -721,6 +723,32 @@ private static EventId MakeMigrationsId(Id id)
///
public static readonly EventId ColumnOrderIgnoredWarning = MakeMigrationsId(Id.ColumnOrderIgnoredWarning);
+ ///
+ /// The model contains changes compared to the last migration.
+ ///
+ ///
+ ///
+ /// This event is in the category.
+ ///
+ ///
+ /// This event uses the payload when used with a .
+ ///
+ ///
+ public static readonly EventId PendingModelChangesWarning = MakeMigrationsId(Id.PendingModelChangesWarning);
+
+ ///
+ /// A migration contains a non-transactional operation.
+ ///
+ ///
+ ///
+ /// This event is in the category.
+ ///
+ ///
+ /// This event uses the payload when used with a .
+ ///
+ ///
+ public static readonly EventId NonTransactionalMigrationOperationWarning = MakeMigrationsId(Id.NonTransactionalMigrationOperationWarning);
+
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 6cf6a0d83e7..d704bcf1f1b 100644
--- a/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs
+++ b/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs
@@ -2309,6 +2309,90 @@ private static string MigrationAttributeMissingWarning(EventDefinitionBase defin
return d.GenerateMessage(p.MigrationType.Name);
}
+ ///
+ /// Logs for the event.
+ ///
+ /// The diagnostics logger to use.
+ /// The type being used.
+ public static void PendingModelChangesWarning(
+ this IDiagnosticsLogger diagnostics,
+ Type contextType)
+ {
+ var definition = RelationalResources.LogPendingModelChanges(diagnostics);
+
+ if (diagnostics.ShouldLog(definition))
+ {
+ definition.Log(diagnostics, contextType.ShortDisplayName());
+ }
+
+ if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled))
+ {
+ var eventData = new DbContextTypeEventData(
+ definition,
+ PendingModelChanges,
+ contextType);
+
+ diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled);
+ }
+ }
+
+ private static string PendingModelChanges(EventDefinitionBase definition, EventData payload)
+ {
+ var d = (EventDefinition)definition;
+ var p = (DbContextTypeEventData)payload;
+ return d.GenerateMessage(p.ContextType.ShortDisplayName());
+ }
+
+ ///
+ /// Logs for the event.
+ ///
+ /// The diagnostics logger to use.
+ /// The in use.
+ /// The being processed.
+ /// The being processed.
+ public static void NonTransactionalMigrationOperationWarning(
+ this IDiagnosticsLogger diagnostics,
+ IMigrator migrator,
+ Migration migration,
+ MigrationCommand command)
+ {
+ var definition = RelationalResources.LogNonTransactionalMigrationOperationWarning(diagnostics);
+
+ if (diagnostics.ShouldLog(definition))
+ {
+ var commandText = command.CommandText;
+ if (commandText.Length > 100)
+ {
+ commandText = commandText.Substring(0, 100) + "...";
+ }
+ definition.Log(diagnostics, commandText, migration.GetType().ShortDisplayName());
+ }
+
+ if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled))
+ {
+ var eventData = new MigrationCommandEventData(
+ definition,
+ NonTransactionalMigrationOperationWarning,
+ migrator,
+ migration,
+ command);
+
+ diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled);
+ }
+ }
+
+ private static string NonTransactionalMigrationOperationWarning(EventDefinitionBase definition, EventData payload)
+ {
+ var d = (EventDefinition)definition;
+ var p = (MigrationCommandEventData)payload;
+ var commandText = p.MigrationCommand.CommandText;
+ if (commandText.Length > 100)
+ {
+ commandText = commandText.Substring(0, 100) + "...";
+ }
+ return d.GenerateMessage(commandText, p.Migration.GetType().ShortDisplayName());
+ }
+
///
/// Logs for the event.
///
diff --git a/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs b/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs
index eb2e5ca3386..51e0e3b1aee 100644
--- a/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs
+++ b/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs
@@ -646,6 +646,24 @@ public abstract class RelationalLoggingDefinitions : LoggingDefinitions
[EntityFrameworkInternal]
public EventDefinitionBase? LogColumnOrderIgnoredWarning;
+ ///
+ /// 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? LogPendingModelChanges;
+
+ ///
+ /// 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? LogNonTransactionalMigrationOperationWarning;
+
///
/// 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/Extensions/RelationalDatabaseFacadeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs
index c2cd5273beb..a5ccf7f3fc3 100644
--- a/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs
+++ b/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs
@@ -116,6 +116,40 @@ public static async Task> GetPendingMigrationsAsync(
public static void Migrate(this DatabaseFacade databaseFacade)
=> databaseFacade.GetRelationalService().Migrate();
+ ///
+ /// Applies migrations for the context to the database. Will create the database
+ /// if it does not already exist.
+ ///
+ ///
+ /// The target migration to migrate the database to, or to migrate to the latest.
+ ///
+ ///
+ /// The optional seed method to run after migrating the database. It will be invoked even if no migrations were applied.
+ ///
+ ///
+ /// The maximum amount of time that the migration lock should be held. Unless a catastrophic failure occurs, the
+ /// lock is released when the migration operation completes.
+ ///
+ ///
+ ///
+ /// Note that this API is mutually exclusive with . EnsureCreated does not use migrations
+ /// to create the database and therefore the database that is created cannot be later updated using migrations.
+ ///
+ ///
+ /// See Database migrations for more information and examples.
+ ///
+ ///
+ /// The for the context.
+ [RequiresDynamicCode(
+ "Migrations operations are not supported with NativeAOT"
+ + " Use a migration bundle or an alternate way of executing migration operations.")]
+ public static void Migrate(
+ this DatabaseFacade databaseFacade,
+ Action? seed,
+ string? targetMigration = null,
+ TimeSpan? lockTimeout = null)
+ => databaseFacade.GetRelationalService().Migrate(targetMigration, seed, lockTimeout);
+
///
/// Asynchronously applies any pending migrations for the context to the database. Will create the database
/// if it does not already exist.
@@ -142,6 +176,45 @@ public static Task MigrateAsync(
CancellationToken cancellationToken = default)
=> databaseFacade.GetRelationalService().MigrateAsync(cancellationToken: cancellationToken);
+ ///
+ /// Asynchronously applies migrations for the context to the database. Will create the database
+ /// if it does not already exist.
+ ///
+ /// The for the context.
+ ///
+ /// The target migration to migrate the database to, or to migrate to the latest.
+ ///
+ ///
+ /// The optional seed method to run after migrating the database. It will be invoked even if no migrations were applied.
+ ///
+ ///
+ /// The maximum amount of time that the migration lock should be held. Unless a catastrophic failure occurs, the
+ /// lock is released when the migration operation completes.
+ ///
+ /// A to observe while waiting for the task to complete.
+ ///
+ ///
+ /// Note that this API is mutually exclusive with .
+ /// does not use migrations to create the database and therefore the database
+ /// that is created cannot be later updated using migrations.
+ ///
+ ///
+ /// See Database migrations for more information and examples.
+ ///
+ ///
+ /// A task that represents the asynchronous migration operation.
+ /// If the is canceled.
+ [RequiresDynamicCode(
+ "Migrations operations are not supported with NativeAOT"
+ + " Use a migration bundle or an alternate way of executing migration operations.")]
+ public static Task MigrateAsync(
+ this DatabaseFacade databaseFacade,
+ Func? seed,
+ string? targetMigration = null,
+ TimeSpan? lockTimeout = null,
+ CancellationToken cancellationToken = default)
+ => databaseFacade.GetRelationalService().MigrateAsync(targetMigration, seed, lockTimeout, cancellationToken);
+
///
/// Executes the given SQL against the database and returns the number of rows affected.
///
@@ -974,29 +1047,7 @@ public static bool IsRelational(this DatabaseFacade databaseFacade)
"Migrations operations are not supported with NativeAOT"
+ " Use a migration bundle or an alternate way of executing migration operations.")]
public static bool HasPendingModelChanges(this DatabaseFacade databaseFacade)
- {
- var modelDiffer = databaseFacade.GetRelationalService();
- var migrationsAssembly = databaseFacade.GetRelationalService();
-
- var modelInitializer = databaseFacade.GetRelationalService();
-
- var snapshotModel = migrationsAssembly.ModelSnapshot?.Model;
- if (snapshotModel is IMutableModel mutableModel)
- {
- snapshotModel = mutableModel.FinalizeModel();
- }
-
- if (snapshotModel is not null)
- {
- snapshotModel = modelInitializer.Initialize(snapshotModel);
- }
-
- var designTimeModel = databaseFacade.GetRelationalService();
-
- return modelDiffer.HasDifferences(
- snapshotModel?.GetRelationalModel(),
- designTimeModel.Model.GetRelationalModel());
- }
+ => databaseFacade.GetRelationalService().HasPendingModelChanges();
private static IRelationalDatabaseFacadeDependencies GetFacadeDependencies(DatabaseFacade databaseFacade)
{
diff --git a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs
index 102b26686c4..2a526d3dfc2 100644
--- a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs
+++ b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs
@@ -54,7 +54,7 @@ public static readonly IDictionary RelationalServi
{ typeof(ISqlGenerationHelper), new ServiceCharacteristics(ServiceLifetime.Singleton) },
{ typeof(IRelationalAnnotationProvider), new ServiceCharacteristics(ServiceLifetime.Singleton) },
{ typeof(IMigrationsAnnotationProvider), new ServiceCharacteristics(ServiceLifetime.Singleton) },
- { typeof(IMigrationCommandExecutor), new ServiceCharacteristics(ServiceLifetime.Singleton) },
+ { typeof(IMigrationCommandExecutor), new ServiceCharacteristics(ServiceLifetime.Scoped) },
{ typeof(IRelationalTypeMappingSource), new ServiceCharacteristics(ServiceLifetime.Singleton) },
{ typeof(IUpdateSqlGenerator), new ServiceCharacteristics(ServiceLifetime.Singleton) },
{ typeof(IRelationalTransactionFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) },
@@ -96,7 +96,8 @@ public static readonly IDictionary RelationalServi
typeof(IAggregateMethodCallTranslatorPlugin),
new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true)
},
- { typeof(IMemberTranslatorPlugin), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) }
+ { typeof(IMemberTranslatorPlugin), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) },
+ { typeof(IMigratorPlugin), new ServiceCharacteristics(ServiceLifetime.Singleton, multipleRegistrations: true) }
};
///
diff --git a/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs b/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs
index 64218f235f7..f8adafd96d9 100644
--- a/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs
+++ b/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs
@@ -439,7 +439,9 @@ public static CoreOptionsExtension WithDefaultWarningConfiguration(CoreOptionsEx
.TryWithExplicit(RelationalEventId.IndexPropertiesBothMappedAndNotMappedToTable, WarningBehavior.Throw)
.TryWithExplicit(RelationalEventId.IndexPropertiesMappedToNonOverlappingTables, WarningBehavior.Throw)
.TryWithExplicit(RelationalEventId.ForeignKeyPropertiesMappedToUnrelatedTables, WarningBehavior.Throw)
- .TryWithExplicit(RelationalEventId.StoredProcedureConcurrencyTokenNotMapped, WarningBehavior.Throw));
+ .TryWithExplicit(RelationalEventId.StoredProcedureConcurrencyTokenNotMapped, WarningBehavior.Throw)
+ .TryWithExplicit(RelationalEventId.PendingModelChangesWarning, WarningBehavior.Throw)
+ .TryWithExplicit(RelationalEventId.NonTransactionalMigrationOperationWarning, WarningBehavior.Throw));
///
/// Information/metadata for a .
diff --git a/src/EFCore.Relational/Migrations/IMigrationCommandExecutor.cs b/src/EFCore.Relational/Migrations/IMigrationCommandExecutor.cs
index fe669e35e6c..7069ba9c045 100644
--- a/src/EFCore.Relational/Migrations/IMigrationCommandExecutor.cs
+++ b/src/EFCore.Relational/Migrations/IMigrationCommandExecutor.cs
@@ -8,9 +8,10 @@ namespace Microsoft.EntityFrameworkCore.Migrations;
///
///
///
-/// The service lifetime is . This means a single instance
-/// is used by many instances. The implementation must be thread-safe.
-/// This service cannot depend on services registered as .
+/// The service lifetime is . This means that each
+/// instance will use its own instance of this service.
+/// The implementation may depend on other services registered with any lifetime.
+/// The implementation does not need to be thread-safe.
///
///
/// See Database migrations for more information and examples.
diff --git a/src/EFCore.Relational/Migrations/IMigrator.cs b/src/EFCore.Relational/Migrations/IMigrator.cs
index ac36e8184cc..69225454347 100644
--- a/src/EFCore.Relational/Migrations/IMigrator.cs
+++ b/src/EFCore.Relational/Migrations/IMigrator.cs
@@ -26,33 +26,49 @@ public interface IMigrator
/// Migrates the database to either a specified target migration or up to the latest
/// migration that exists in the .
///
- ///
- /// See Database migrations for more information and examples.
- ///
///
/// The target migration to migrate the database to, or to migrate to the latest.
///
+ ///
+ /// The optional seed method to run after migrating the database. It will be invoked even if no migrations were applied.
+ ///
+ ///
+ /// The maximum amount of time that the migration lock should be held. Unless a catastrophic failure occurs, the
+ /// lock is released when the migration operation completes.
+ ///
+ ///
+ /// See Database migrations for more information and examples.
+ ///
[RequiresUnreferencedCode("Migration generation currently isn't compatible with trimming")]
[RequiresDynamicCode("Migrations operations are not supported with NativeAOT")]
- void Migrate(string? targetMigration = null);
+ void Migrate(string? targetMigration = null, Action? seed = null, TimeSpan? lockTimeout = null);
///
/// Migrates the database to either a specified target migration or up to the latest
/// migration that exists in the .
///
- ///
- /// See Database migrations for more information and examples.
- ///
///
/// The target migration to migrate the database to, or to migrate to the latest.
///
+ ///
+ /// The optional seed method to run after migrating the database. It will be invoked even if no migrations were applied.
+ ///
+ ///
+ /// The maximum amount of time that the migration lock should be held. Unless a catastrophic failure occurs, the
+ /// lock is released when the migration operation completes.
+ ///
/// A to observe while waiting for the task to complete.
/// A task that represents the asynchronous operation
+ ///
+ /// See Database migrations for more information and examples.
+ ///
/// If the is canceled.
[RequiresUnreferencedCode("Migration generation currently isn't compatible with trimming")]
[RequiresDynamicCode("Migrations operations are not supported with NativeAOT")]
Task MigrateAsync(
string? targetMigration = null,
+ Func? seed = null,
+ TimeSpan? lockTimeout = null,
CancellationToken cancellationToken = default);
///
@@ -78,4 +94,16 @@ string GenerateScript(
string? fromMigration = null,
string? toMigration = null,
MigrationsSqlGenerationOptions options = MigrationsSqlGenerationOptions.Default);
+
+ ///
+ /// Returns if the model has pending changes to be applied.
+ ///
+ ///
+ /// if the database model has pending changes
+ /// and a new migration has to be added.
+ ///
+ [RequiresDynamicCode(
+ "Migrations operations are not supported with NativeAOT"
+ + " Use a migration bundle or an alternate way of executing migration operations.")]
+ bool HasPendingModelChanges();
}
diff --git a/src/EFCore.Relational/Migrations/IMigratorData.cs b/src/EFCore.Relational/Migrations/IMigratorData.cs
new file mode 100644
index 00000000000..60531a1c699
--- /dev/null
+++ b/src/EFCore.Relational/Migrations/IMigratorData.cs
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Migrations;
+
+///
+/// A class that holds the results from the last migrations application.
+///
+///
+/// See Database migrations for more information and examples.
+///
+public interface IMigratorData
+{
+ ///
+ /// The migrations that were applied to the database.
+ ///
+ public IReadOnlyList AppliedMigrations { get; }
+
+ ///
+ /// The migrations that were reverted from the database.
+ ///
+ public IReadOnlyList RevertedMigrations { get; }
+
+ ///
+ /// The target migration.
+ /// if all migrations were reverted or no target migration was specified.
+ ///
+ public Migration? TargetMigration { get; }
+}
diff --git a/src/EFCore.Relational/Migrations/IMigratorPlugin.cs b/src/EFCore.Relational/Migrations/IMigratorPlugin.cs
new file mode 100644
index 00000000000..9c1409a9cc0
--- /dev/null
+++ b/src/EFCore.Relational/Migrations/IMigratorPlugin.cs
@@ -0,0 +1,73 @@
+// 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;
+
+///
+///
+/// A service on the EF internal service provider that allows providers or extensions to execute logic
+/// after is called.
+///
+///
+/// This type is typically used by providers or extensions. It is generally not used in application code.
+///
+///
+///
+/// The service lifetime is . This means a single instance
+/// is used by many instances. The implementation must be thread-safe.
+/// This service cannot depend on services registered as .
+///
+public interface IMigratorPlugin
+{
+ ///
+ /// Called by before applying the migrations.
+ ///
+ /// The that is being migrated.
+ /// The that contains the result of the migrations application.
+ ///
+ /// See Database migrations for more information and examples.
+ ///
+ void Migrating(DbContext context, IMigratorData data);
+
+ ///
+ /// Called by before applying the migrations.
+ ///
+ /// The that is being migrated.
+ /// The that contains the result of the migrations application.
+ ///
+ /// See Database migrations for more information and examples.
+ ///
+ /// A to observe while waiting for the task to complete.
+ /// A task that represents the asynchronous operation
+ /// If the is canceled.
+ Task MigratingAsync(
+ DbContext context,
+ IMigratorData data,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Called by after applying the migrations, but before the seeding action.
+ ///
+ /// The that is being migrated.
+ /// The that contains the result of the migrations application.
+ ///
+ /// See Database migrations for more information and examples.
+ ///
+ void Migrated(DbContext context, IMigratorData data);
+
+ ///
+ /// Called by after applying the migrations, but before the seeding action.
+ ///
+ /// The that is being migrated.
+ /// The that contains the result of the migrations application.
+ ///
+ /// See Database migrations for more information and examples.
+ ///
+ /// A to observe while waiting for the task to complete.
+ /// A task that represents the asynchronous operation
+ /// If the is canceled.
+ Task MigratedAsync(
+ DbContext context,
+ IMigratorData data,
+ CancellationToken cancellationToken = default);
+}
diff --git a/src/EFCore.Relational/Migrations/Internal/MigrationCommandExecutor.cs b/src/EFCore.Relational/Migrations/Internal/MigrationCommandExecutor.cs
index 24546240a86..e545d4c13b5 100644
--- a/src/EFCore.Relational/Migrations/Internal/MigrationCommandExecutor.cs
+++ b/src/EFCore.Relational/Migrations/Internal/MigrationCommandExecutor.cs
@@ -13,6 +13,19 @@ namespace Microsoft.EntityFrameworkCore.Migrations.Internal;
///
public class MigrationCommandExecutor : IMigrationCommandExecutor
{
+ private readonly IExecutionStrategy _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.
+ ///
+ public MigrationCommandExecutor(IExecutionStrategy executionStrategy)
+ {
+ _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
@@ -24,53 +37,70 @@ public virtual void ExecuteNonQuery(
IRelationalConnection connection)
{
var userTransaction = connection.CurrentTransaction;
- if (userTransaction is not null && migrationCommands.Any(x => x.TransactionSuppressed))
+ if (userTransaction is not null
+ && (migrationCommands.Any(x => x.TransactionSuppressed) || _executionStrategy.RetriesOnFailure))
{
throw new NotSupportedException(RelationalStrings.TransactionSuppressedMigrationInUserTransaction);
}
using (new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled))
{
- connection.Open();
+ var parameters = new ExecuteParameters(migrationCommands.ToList(), connection);
+ if (userTransaction is null)
+ {
+ _executionStrategy.Execute(parameters, static (_, p) => Execute(p, beginTransaction: true), verifySucceeded: null);
+ }
+ else
+ {
+ Execute(parameters, beginTransaction: false);
+ }
+ }
+ }
- try
+ private static bool Execute(ExecuteParameters parameters, bool beginTransaction)
+ {
+ var migrationCommands = parameters.MigrationCommands;
+ var connection = parameters.Connection;
+ IDbContextTransaction? transaction = null;
+ connection.Open();
+ try
+ {
+ for (var i = parameters.CurrentCommandIndex; i < migrationCommands.Count; i++)
{
- IDbContextTransaction? transaction = null;
+ var command = migrationCommands[i];
+ if (transaction == null
+ && !command.TransactionSuppressed
+ && beginTransaction)
+ {
+ transaction = connection.BeginTransaction();
+ }
- try
+ if (transaction != null
+ && command.TransactionSuppressed)
{
- foreach (var command in migrationCommands)
- {
- if (transaction == null
- && !command.TransactionSuppressed
- && userTransaction is null)
- {
- transaction = connection.BeginTransaction();
- }
-
- if (transaction != null
- && command.TransactionSuppressed)
- {
- transaction.Commit();
- transaction.Dispose();
- transaction = null;
- }
-
- command.ExecuteNonQuery(connection);
- }
-
- transaction?.Commit();
+ transaction.Commit();
+ transaction.Dispose();
+ transaction = null;
+ parameters.CurrentCommandIndex = i;
}
- finally
+
+ command.ExecuteNonQuery(connection);
+
+ if (transaction == null)
{
- transaction?.Dispose();
+ parameters.CurrentCommandIndex = i + 1;
}
}
- finally
- {
- connection.Close();
- }
+
+ transaction?.Commit();
+ }
+ finally
+ {
+ transaction?.Dispose();
+ connection.Close();
}
+
+ return true;
}
///
@@ -85,7 +115,8 @@ public virtual async Task ExecuteNonQueryAsync(
CancellationToken cancellationToken = default)
{
var userTransaction = connection.CurrentTransaction;
- if (userTransaction is not null && migrationCommands.Any(x => x.TransactionSuppressed))
+ if (userTransaction is not null
+ && (migrationCommands.Any(x => x.TransactionSuppressed) || _executionStrategy.RetriesOnFailure))
{
throw new NotSupportedException(RelationalStrings.TransactionSuppressedMigrationInUserTransaction);
}
@@ -93,57 +124,86 @@ public virtual async Task ExecuteNonQueryAsync(
var transactionScope = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled);
try
{
- await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
+ var parameters = new ExecuteParameters(migrationCommands.ToList(), 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);
+ }
+
+ }
+ finally
+ {
+ await transactionScope.DisposeAsyncIfAvailable().ConfigureAwait(false);
+ }
+ }
- try
+ private static async Task ExecuteAsync(ExecuteParameters parameters, bool beginTransaction, CancellationToken cancellationToken)
+ {
+ var migrationCommands = parameters.MigrationCommands;
+ var connection = parameters.Connection;
+ IDbContextTransaction? transaction = null;
+ await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
+ try
+ {
+ for (var i = parameters.CurrentCommandIndex; i < migrationCommands.Count; i++)
{
- IDbContextTransaction? transaction = null;
+ var command = migrationCommands[i];
+ if (transaction == null
+ && !command.TransactionSuppressed
+ && beginTransaction)
+ {
+ transaction = await connection.BeginTransactionAsync(cancellationToken)
+ .ConfigureAwait(false);
+ }
- try
+ if (transaction != null
+ && command.TransactionSuppressed)
{
- foreach (var command in migrationCommands)
- {
- if (transaction == null
- && !command.TransactionSuppressed
- && userTransaction is null)
- {
- transaction = await connection.BeginTransactionAsync(cancellationToken)
- .ConfigureAwait(false);
- }
-
- if (transaction != null
- && command.TransactionSuppressed)
- {
- await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
- await transaction.DisposeAsync().ConfigureAwait(false);
- transaction = null;
- }
-
- await command.ExecuteNonQueryAsync(connection, cancellationToken: cancellationToken)
- .ConfigureAwait(false);
- }
-
- if (transaction != null)
- {
- await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
- }
+ await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
+ await transaction.DisposeAsync().ConfigureAwait(false);
+ transaction = null;
+ parameters.CurrentCommandIndex = i;
}
- finally
+
+ await command.ExecuteNonQueryAsync(connection, cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
+
+ if (transaction == null)
{
- if (transaction != null)
- {
- await transaction.DisposeAsync().ConfigureAwait(false);
- }
+ parameters.CurrentCommandIndex = i + 1;
}
}
- finally
+
+ if (transaction != null)
{
- await connection.CloseAsync().ConfigureAwait(false);
+ await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
}
}
finally
{
- await transactionScope.DisposeAsyncIfAvailable().ConfigureAwait(false);
+ if (transaction != null)
+ {
+ await transaction.DisposeAsync().ConfigureAwait(false);
+ }
+
+ await connection.CloseAsync().ConfigureAwait(false);
}
+
+ return true;
+ }
+
+ private sealed class ExecuteParameters(List migrationCommands, IRelationalConnection connection)
+ {
+ public int CurrentCommandIndex;
+ public List MigrationCommands { get; } = migrationCommands;
+ public IRelationalConnection Connection { get; } = connection;
}
}
diff --git a/src/EFCore.Relational/Migrations/Internal/Migrator.cs b/src/EFCore.Relational/Migrations/Internal/Migrator.cs
index 6263bec3a9b..055fa992ac6 100644
--- a/src/EFCore.Relational/Migrations/Internal/Migrator.cs
+++ b/src/EFCore.Relational/Migrations/Internal/Migrator.cs
@@ -1,6 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using Microsoft.EntityFrameworkCore.Diagnostics.Internal;
+using Microsoft.EntityFrameworkCore.Storage;
+
namespace Microsoft.EntityFrameworkCore.Migrations.Internal;
///
@@ -23,7 +26,11 @@ public class Migrator : IMigrator
private readonly IModelRuntimeInitializer _modelRuntimeInitializer;
private readonly IDiagnosticsLogger _logger;
private readonly IRelationalCommandDiagnosticsLogger _commandLogger;
+ private readonly IEnumerable _plugins;
+ private readonly IMigrationsModelDiffer _migrationsModelDiffer;
+ private readonly IDesignTimeModel _designTimeModel;
private readonly string _activeProvider;
+ private static readonly TimeSpan _defaultLockTimeout = TimeSpan.FromHours(1);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -44,7 +51,10 @@ public Migrator(
IModelRuntimeInitializer modelRuntimeInitializer,
IDiagnosticsLogger logger,
IRelationalCommandDiagnosticsLogger commandLogger,
- IDatabaseProvider databaseProvider)
+ IDatabaseProvider databaseProvider,
+ IEnumerable plugins,
+ IMigrationsModelDiffer migrationsModelDiffer,
+ IDesignTimeModel designTimeModel)
{
_migrationsAssembly = migrationsAssembly;
_historyRepository = historyRepository;
@@ -58,6 +68,9 @@ public Migrator(
_modelRuntimeInitializer = modelRuntimeInitializer;
_logger = logger;
_commandLogger = commandLogger;
+ _plugins = plugins;
+ _migrationsModelDiffer = migrationsModelDiffer;
+ _designTimeModel = designTimeModel;
_activeProvider = databaseProvider.Name;
}
@@ -67,16 +80,14 @@ public Migrator(
/// 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
- /// 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 void Migrate(string? targetMigration = null)
+ public virtual void Migrate(string? targetMigration, Action? seed, TimeSpan? lockTimeout)
{
+ if (RelationalResources.LogPendingModelChanges(_logger).WarningBehavior != WarningBehavior.Ignore
+ && HasPendingModelChanges())
+ {
+ _logger.PendingModelChangesWarning(_currentContext.Context.GetType());
+ }
+
_logger.MigrateUsingConnection(this, _connection);
if (!_databaseCreator.Exists())
@@ -88,19 +99,40 @@ public virtual void Migrate(string? targetMigration = null)
{
_connection.Open();
- using var _ = _historyRepository.GetDatabaseLock(LockTimeout);
+ using var _ = _historyRepository.GetDatabaseLock(lockTimeout ?? _defaultLockTimeout);
if (!_historyRepository.Exists())
{
_historyRepository.Create();
}
- var commandLists = GetMigrationCommandLists(_historyRepository.GetAppliedMigrations(), targetMigration);
+ PopulateMigrations(
+ _historyRepository.GetAppliedMigrations().Select(t => t.MigrationId),
+ targetMigration,
+ out var migratorData);
+ foreach (var plugin in _plugins)
+ {
+ plugin.Migrating(_currentContext.Context, migratorData);
+ }
+
+ var commandLists = GetMigrationCommandLists(migratorData);
foreach (var commandList in commandLists)
{
_migrationCommandExecutor.ExecuteNonQuery(commandList(), _connection);
}
+
+ foreach (var plugin in _plugins)
+ {
+ plugin.Migrated(_currentContext.Context, migratorData);
+ }
+
+ if (seed != null)
+ {
+ using var transaction = _connection.BeginTransaction();
+ seed(_currentContext.Context, migratorData);
+ transaction.Commit();
+ }
}
finally
{
@@ -115,9 +147,17 @@ public virtual void Migrate(string? targetMigration = null)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public virtual async Task MigrateAsync(
- string? targetMigration = null,
+ string? targetMigration,
+ Func? seed,
+ TimeSpan? lockTimeout = null,
CancellationToken cancellationToken = default)
{
+ if (RelationalResources.LogPendingModelChanges(_logger).WarningBehavior != WarningBehavior.Ignore
+ && HasPendingModelChanges())
+ {
+ _logger.PendingModelChangesWarning(_currentContext.Context.GetType());
+ }
+
_logger.MigrateUsingConnection(this, _connection);
if (!await _databaseCreator.ExistsAsync(cancellationToken).ConfigureAwait(false))
@@ -129,7 +169,7 @@ public virtual async Task MigrateAsync(
{
await _connection.OpenAsync(cancellationToken).ConfigureAwait(false);
- var dbLock = await _historyRepository.GetDatabaseLockAsync(LockTimeout, cancellationToken).ConfigureAwait(false);
+ var dbLock = await _historyRepository.GetDatabaseLockAsync(lockTimeout ?? _defaultLockTimeout, cancellationToken).ConfigureAwait(false);
await using var _ = dbLock.ConfigureAwait(false);
if (!await _historyRepository.ExistsAsync(cancellationToken).ConfigureAwait(false))
@@ -137,15 +177,35 @@ public virtual async Task MigrateAsync(
await _historyRepository.CreateAsync(cancellationToken).ConfigureAwait(false);
}
- var commandLists = GetMigrationCommandLists(
- await _historyRepository.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false),
- targetMigration);
+ PopulateMigrations(
+ (await _historyRepository.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false)).Select(t => t.MigrationId),
+ targetMigration,
+ out var migratorData);
+ foreach (var plugin in _plugins)
+ {
+ await plugin.MigratingAsync(_currentContext.Context, migratorData, cancellationToken).ConfigureAwait(false);
+ }
+
+ var commandLists = GetMigrationCommandLists(migratorData);
foreach (var commandList in commandLists)
{
await _migrationCommandExecutor.ExecuteNonQueryAsync(commandList(), _connection, cancellationToken)
.ConfigureAwait(false);
}
+
+ foreach (var plugin in _plugins)
+ {
+ await plugin.MigratedAsync(_currentContext.Context, migratorData, cancellationToken).ConfigureAwait(false);
+ }
+
+ if (seed != null)
+ {
+ var transaction = await _connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
+ await using var __ = transaction.ConfigureAwait(false);
+ await seed(_currentContext.Context, migratorData, cancellationToken).ConfigureAwait(false);
+ await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
+ }
}
finally
{
@@ -153,16 +213,11 @@ await _migrationCommandExecutor.ExecuteNonQueryAsync(commandList(), _connection,
}
}
- private IEnumerable>> GetMigrationCommandLists(
- IReadOnlyList appliedMigrationEntries,
- string? targetMigration = null)
+ private IEnumerable>> GetMigrationCommandLists(IMigratorData parameters)
{
- PopulateMigrations(
- appliedMigrationEntries.Select(t => t.MigrationId),
- targetMigration,
- out var migrationsToApply,
- out var migrationsToRevert,
- out var actualTargetMigration);
+ var migrationsToApply = parameters.AppliedMigrations;
+ var migrationsToRevert = parameters.RevertedMigrations;
+ var actualTargetMigration = parameters.TargetMigration;
for (var i = 0; i < migrationsToRevert.Count; i++)
{
@@ -173,11 +228,17 @@ private IEnumerable>> GetMigrationCommandLi
{
_logger.MigrationReverting(this, migration);
- return GenerateDownSql(
+ var commands = GenerateDownSql(
migration,
index != migrationsToRevert.Count - 1
? migrationsToRevert[index + 1]
: actualTargetMigration);
+ if (migration.DownOperations.Count > 1
+ && commands.FirstOrDefault(c => c.TransactionSuppressed) is MigrationCommand nonTransactionalCommand)
+ {
+ _logger.NonTransactionalMigrationOperationWarning(this, migration, nonTransactionalCommand);
+ }
+ return commands;
};
}
@@ -187,7 +248,13 @@ private IEnumerable>> GetMigrationCommandLi
{
_logger.MigrationApplying(this, migration);
- return GenerateUpSql(migration);
+ var commands = GenerateUpSql(migration);
+ if (migration.UpOperations.Count > 1
+ && commands.FirstOrDefault(c => c.TransactionSuppressed) is MigrationCommand nonTransactionalCommand)
+ {
+ _logger.NonTransactionalMigrationOperationWarning(this, migration, nonTransactionalCommand);
+ }
+ return commands;
};
}
@@ -206,9 +273,7 @@ private IEnumerable>> GetMigrationCommandLi
protected virtual void PopulateMigrations(
IEnumerable appliedMigrationEntries,
string? targetMigration,
- out IReadOnlyList migrationsToApply,
- out IReadOnlyList migrationsToRevert,
- out Migration? actualTargetMigration)
+ out IMigratorData parameters)
{
var appliedMigrations = new Dictionary();
var unappliedMigrations = new Dictionary();
@@ -230,6 +295,9 @@ protected virtual void PopulateMigrations(
}
}
+ IReadOnlyList migrationsToApply;
+ IReadOnlyList migrationsToRevert;
+ Migration? actualTargetMigration = null;
if (string.IsNullOrEmpty(targetMigration))
{
migrationsToApply = unappliedMigrations
@@ -237,7 +305,6 @@ protected virtual void PopulateMigrations(
.Select(p => _migrationsAssembly.CreateMigration(p.Value, _activeProvider))
.ToList();
migrationsToRevert = [];
- actualTargetMigration = null;
}
else if (targetMigration == Migration.InitialDatabase)
{
@@ -246,7 +313,6 @@ protected virtual void PopulateMigrations(
.OrderByDescending(m => m.Key)
.Select(p => _migrationsAssembly.CreateMigration(p.Value, _activeProvider))
.ToList();
- actualTargetMigration = null;
}
else
{
@@ -266,6 +332,8 @@ protected virtual void PopulateMigrations(
.Select(p => _migrationsAssembly.CreateMigration(p.Value, _activeProvider))
.SingleOrDefault();
}
+
+ parameters = new MigratorData(migrationsToApply, migrationsToRevert, actualTargetMigration);
}
///
@@ -298,12 +366,7 @@ public virtual string GenerateScript(
.Select(t => t.Key);
}
- PopulateMigrations(
- appliedMigrations,
- toMigration,
- out var migrationsToApply,
- out var migrationsToRevert,
- out var actualTargetMigration);
+ PopulateMigrations(appliedMigrations, toMigration, out var migratorData);
var builder = new IndentedStringBuilder();
@@ -318,6 +381,10 @@ public virtual string GenerateScript(
var idempotencyEnd = idempotent
? _historyRepository.GetEndIfScript()
: null;
+ var migrationsToApply = migratorData.AppliedMigrations;
+ var migrationsToRevert = migratorData.RevertedMigrations;
+ var actualTargetMigration = migratorData.TargetMigration;
+ var transactionStarted = false;
for (var i = 0; i < migrationsToRevert.Count; i++)
{
var migration = migrationsToRevert[i];
@@ -331,7 +398,9 @@ public virtual string GenerateScript(
? _historyRepository.GetBeginIfExistsScript(migration.GetId())
: null;
- GenerateSqlScript(GenerateDownSql(migration, previousMigration, options), builder, _sqlGenerationHelper, noTransactions, idempotencyCondition, idempotencyEnd);
+ GenerateSqlScript(
+ GenerateDownSql(migration, previousMigration, options),
+ builder, _sqlGenerationHelper, ref transactionStarted, noTransactions, idempotencyCondition, idempotencyEnd);
}
foreach (var migration in migrationsToApply)
@@ -342,7 +411,16 @@ public virtual string GenerateScript(
? _historyRepository.GetBeginIfNotExistsScript(migration.GetId())
: null;
- GenerateSqlScript(GenerateUpSql(migration, options), builder, _sqlGenerationHelper, noTransactions, idempotencyCondition, idempotencyEnd);
+ GenerateSqlScript(
+ GenerateUpSql(migration, options),
+ builder, _sqlGenerationHelper, ref transactionStarted, noTransactions, idempotencyCondition, idempotencyEnd);
+ }
+
+ if (!noTransactions && transactionStarted)
+ {
+ builder
+ .AppendLine(_sqlGenerationHelper.CommitTransactionStatement)
+ .Append(_sqlGenerationHelper.BatchTerminator);
}
return builder.ToString();
@@ -352,11 +430,11 @@ private static void GenerateSqlScript(
IEnumerable commands,
IndentedStringBuilder builder,
ISqlGenerationHelper sqlGenerationHelper,
+ ref bool transactionStarted,
bool noTransactions = false,
string? idempotencyCondition = null,
string? idempotencyEnd = null)
{
- var transactionStarted = false;
foreach (var command in commands)
{
if (!noTransactions)
@@ -396,13 +474,6 @@ private static void GenerateSqlScript(
builder.Append(sqlGenerationHelper.BatchTerminator);
}
-
- if (!noTransactions && transactionStarted)
- {
- builder
- .AppendLine(sqlGenerationHelper.CommitTransactionStatement)
- .Append(sqlGenerationHelper.BatchTerminator);
- }
}
///
@@ -445,6 +516,19 @@ protected virtual IReadOnlyList GenerateDownSql(
.ToList();
}
- private IModel FinalizeModel(IModel model)
- => _modelRuntimeInitializer.Initialize(model, designTime: true, validationLogger: null);
+ private IModel? FinalizeModel(IModel? model)
+ => model == null
+ ? null
+ : _modelRuntimeInitializer.Initialize(model);
+
+ ///
+ /// 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 bool HasPendingModelChanges()
+ => _migrationsModelDiffer.HasDifferences(
+ FinalizeModel(_migrationsAssembly.ModelSnapshot?.Model)?.GetRelationalModel(),
+ _designTimeModel.Model.GetRelationalModel());
}
diff --git a/src/EFCore.Relational/Migrations/Internal/MigratorData.cs b/src/EFCore.Relational/Migrations/Internal/MigratorData.cs
new file mode 100644
index 00000000000..97c47555d2f
--- /dev/null
+++ b/src/EFCore.Relational/Migrations/Internal/MigratorData.cs
@@ -0,0 +1,41 @@
+// 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.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 MigratorData(
+ IReadOnlyList appliedMigrations,
+ IReadOnlyList revertedMigrations,
+ Migration? targetMigration)
+ : IMigratorData
+{
+ ///
+ /// 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 IReadOnlyList AppliedMigrations { get; } = appliedMigrations;
+
+ ///
+ /// 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 IReadOnlyList RevertedMigrations { get; } = revertedMigrations;
+
+ ///
+ /// 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 Migration? TargetMigration { get; } = targetMigration;
+}
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
index b174634a215..76cb6123a68 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
+++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
@@ -1178,7 +1178,7 @@ public static string JsonNodeMustBeHandledByProviderSpecificVisitor
=> GetString("JsonNodeMustBeHandledByProviderSpecificVisitor");
///
- /// Using parameter to access the element of a JSON collection '{entityTypeName}' is not supported when using '{asNoTrackingWithIdentityResolution}'. Use constant, or project the entire JSON entity collection instead.
+ /// Using a parameter to access the element of a JSON collection '{entityTypeName}' is not supported when using '{asNoTrackingWithIdentityResolution}'. Use a constant, or project the entire JSON entity collection instead.
///
public static string JsonProjectingCollectionElementAccessedUsingParmeterNoTrackingWithIdentityResolution(object? entityTypeName, object? asNoTrackingWithIdentityResolution)
=> string.Format(
@@ -1188,10 +1188,10 @@ public static string JsonProjectingCollectionElementAccessedUsingParmeterNoTrack
///
/// When using '{asNoTrackingWithIdentityResolution}' entities mapped to JSON must be projected in a particular order. Project entire collection of entities '{entityTypeName}' before its individual elements.
///
- public static string JsonProjectingEntitiesIncorrectOrderNoTrackingWithIdentityResolution(object? entityTypeName, object? asNoTrackingWithIdentityResolution)
+ public static string JsonProjectingEntitiesIncorrectOrderNoTrackingWithIdentityResolution(object? asNoTrackingWithIdentityResolution, object? entityTypeName)
=> string.Format(
- GetString("JsonProjectingEntitiesIncorrectOrderNoTrackingWithIdentityResolution", nameof(entityTypeName), nameof(asNoTrackingWithIdentityResolution)),
- entityTypeName, asNoTrackingWithIdentityResolution);
+ GetString("JsonProjectingEntitiesIncorrectOrderNoTrackingWithIdentityResolution", nameof(asNoTrackingWithIdentityResolution), nameof(entityTypeName)),
+ asNoTrackingWithIdentityResolution, entityTypeName);
///
/// Projecting queryable operations on JSON collection is not supported for '{asNoTrackingWithIdentityResolution}'.
@@ -1386,7 +1386,7 @@ public static string NoActiveTransaction
=> GetString("NoActiveTransaction");
///
- /// No alias is defined on table: '{table}'
+ /// No alias is defined on table: '{table}'.
///
public static string NoAliasOnTable(object? table)
=> string.Format(
@@ -2036,7 +2036,7 @@ public static string TransactionAssociatedWithDifferentConnection
=> GetString("TransactionAssociatedWithDifferentConnection");
///
- /// User transaction is not supported with a TransactionSuppressed migrations.
+ /// User transaction is not supported with a TransactionSuppressed migrations or a retrying execution strategy.
///
public static string TransactionSuppressedMigrationInUserTransaction
=> GetString("TransactionSuppressedMigrationInUserTransaction");
@@ -2106,7 +2106,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(
@@ -3522,6 +3522,31 @@ public static EventDefinition LogNoMigrationsFound(IDiagnosticsLogger lo
return (EventDefinition)definition;
}
+ ///
+ /// The migration operation '{operation}' from migration '{migration}' cannot be executed in a transaction. If the app is terminated or an unrecoverable error occurs while this operation is being executed then the migration will be left in a partially applied state and would need to be reverted manually before it can be applied again. Create a separate migration that contains just this operation.
+ ///
+ public static EventDefinition LogNonTransactionalMigrationOperationWarning(IDiagnosticsLogger logger)
+ {
+ var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogNonTransactionalMigrationOperationWarning;
+ if (definition == null)
+ {
+ definition = NonCapturingLazyInitializer.EnsureInitialized(
+ ref ((RelationalLoggingDefinitions)logger.Definitions).LogNonTransactionalMigrationOperationWarning,
+ logger,
+ static logger => new EventDefinition(
+ logger.Options,
+ RelationalEventId.NonTransactionalMigrationOperationWarning,
+ LogLevel.Error,
+ "RelationalEventId.NonTransactionalMigrationOperationWarning",
+ level => LoggerMessage.Define(
+ level,
+ RelationalEventId.NonTransactionalMigrationOperationWarning,
+ _resourceManager.GetString("LogNonTransactionalMigrationOperationWarning")!)));
+ }
+
+ return (EventDefinition)definition;
+ }
+
///
/// Opened connection to database '{database}' on server '{server}'.
///
@@ -3647,6 +3672,31 @@ public static EventDefinition LogOptionalDependentWithoutIdentifyingProp
return (EventDefinition)definition;
}
+ ///
+ /// The model for context '{contextType}' has pending changes. Add a new migration before updating the database.
+ ///
+ public static EventDefinition LogPendingModelChanges(IDiagnosticsLogger logger)
+ {
+ var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogPendingModelChanges;
+ if (definition == null)
+ {
+ definition = NonCapturingLazyInitializer.EnsureInitialized(
+ ref ((RelationalLoggingDefinitions)logger.Definitions).LogPendingModelChanges,
+ logger,
+ static logger => new EventDefinition(
+ logger.Options,
+ RelationalEventId.PendingModelChangesWarning,
+ LogLevel.Error,
+ "RelationalEventId.PendingModelChangesWarning",
+ level => LoggerMessage.Define(
+ level,
+ RelationalEventId.PendingModelChangesWarning,
+ _resourceManager.GetString("LogPendingModelChanges")!)));
+ }
+
+ return (EventDefinition)definition;
+ }
+
///
/// Possible unintended use of method 'Equals' for arguments '{left}' and '{right}' of different types in a query. This comparison will always return false.
///
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx
index 1e7a548235a..f94806c31ba 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.resx
+++ b/src/EFCore.Relational/Properties/RelationalStrings.resx
@@ -563,7 +563,7 @@
This node should be handled by provider-specific sql generator.
- Using parameter to access the element of a JSON collection '{entityTypeName}' is not supported for '{asNoTrackingWithIdentityResolution}'. Use constant, or project the entire JSON entity collection instead.
+ Using a parameter to access the element of a JSON collection '{entityTypeName}' is not supported when using '{asNoTrackingWithIdentityResolution}'. Use a constant, or project the entire JSON entity collection instead.When using '{asNoTrackingWithIdentityResolution}' entities mapped to JSON must be projected in a particular order. Project entire collection of entities '{entityTypeName}' before its individual elements.
@@ -797,6 +797,10 @@
No migrations were found in assembly '{migrationsAssembly}'.Debug RelationalEventId.MigrationsNotFound string
+
+ The migration operation '{operation}' from migration '{migration}' cannot be executed in a transaction. If the app is terminated or an unrecoverable error occurs while this operation is being executed then the migration will be left in a partially applied state and would need to be reverted manually before it can be applied again. Create a separate migration that contains just this operation.
+ Error RelationalEventId.NonTransactionalMigrationOperationWarning string string
+
Opened connection to database '{database}' on server '{server}'.Debug RelationalEventId.ConnectionOpened string string
@@ -817,6 +821,10 @@
The entity type '{entityType}' is an optional dependent using table sharing without any required non shared property that could be used to identify whether the entity exists. If all nullable properties contain a null value in database then an object instance won't be created in the query. Add a required property to create instances with null values for other properties or mark the incoming navigation as required to always create an instance.Warning RelationalEventId.OptionalDependentWithoutIdentifyingPropertyWarning string
+
+ The model for context '{contextType}' has pending changes. Add a new migration before updating the database.
+ Error RelationalEventId.PendingModelChangesWarning string
+
Possible unintended use of method 'Equals' for arguments '{left}' and '{right}' of different types in a query. This comparison will always return false.Warning RelationalEventId.QueryPossibleUnintendedUseOfEqualsWarning string string
@@ -1029,12 +1037,12 @@
Cannot create a 'SelectExpression' with a custom 'TableExpressionBase' since the result type '{entityType}' is part of a hierarchy and does not contain a discriminator property.
-
- Set operations over different entity or complex types are not supported ('{type1}' and '{type2}').
-
SelectExpression.Update() is not supported while the expression is in mutable state.
+
+ Set operations over different entity or complex types are not supported ('{type1}' and '{type2}').
+
Unable to translate set operation after client projection has been applied. Consider moving the set operation before the last 'Select' call.
@@ -1201,7 +1209,7 @@
The specified transaction is not associated with the current connection. Only transactions associated with the current connection may be used.
- User transaction is not supported with a TransactionSuppressed migrations.
+ User transaction is not supported with a TransactionSuppressed migrations or a retrying execution strategy.Trigger '{trigger}' for table '{triggerTable}' is defined on entity type '{entityType}', which is mapped to table '{entityTable}'. See https://aka.ms/efcore-docs-triggers for more information on triggers.
diff --git a/src/EFCore.Relational/Query/RelationalAggregateMethodCallTranslatorProviderDependencies.cs b/src/EFCore.Relational/Query/RelationalAggregateMethodCallTranslatorProviderDependencies.cs
index 4474a11dd3e..6ebec3ad2c9 100644
--- a/src/EFCore.Relational/Query/RelationalAggregateMethodCallTranslatorProviderDependencies.cs
+++ b/src/EFCore.Relational/Query/RelationalAggregateMethodCallTranslatorProviderDependencies.cs
@@ -56,7 +56,7 @@ public RelationalAggregateMethodCallTranslatorProviderDependencies(
}
///
- /// The expression factory..
+ /// The expression factory.
///
public ISqlExpressionFactory SqlExpressionFactory { get; init; }
diff --git a/src/EFCore.Relational/Query/RelationalMemberTranslatorProviderDependencies.cs b/src/EFCore.Relational/Query/RelationalMemberTranslatorProviderDependencies.cs
index 6d4c628edc6..3a126739f30 100644
--- a/src/EFCore.Relational/Query/RelationalMemberTranslatorProviderDependencies.cs
+++ b/src/EFCore.Relational/Query/RelationalMemberTranslatorProviderDependencies.cs
@@ -54,7 +54,7 @@ public RelationalMemberTranslatorProviderDependencies(
}
///
- /// The expression factory..
+ /// The expression factory.
///
public ISqlExpressionFactory SqlExpressionFactory { get; init; }
diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs
index dbff4b7a514..1a297374ef1 100644
--- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs
+++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs
@@ -1738,7 +1738,7 @@ protected virtual void Rename(
}
///
- /// Generates a transfer from one schema to another..
+ /// Generates a transfer from one schema to another.
///
/// The schema to transfer to.
/// The schema to transfer from.
diff --git a/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs b/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs
index c109fcdded7..20190cc57aa 100644
--- a/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs
+++ b/src/EFCore.Sqlite.Core/Extensions/SqliteServiceCollectionExtensions.cs
@@ -117,9 +117,8 @@ public static IServiceCollection AddEntityFrameworkSqlite(this IServiceCollectio
.TryAdd()
.TryAdd()
.TryAddProviderSpecificServices(
- b => b.TryAddScoped());
-
- builder.TryAddCoreServices();
+ b => b.TryAddScoped())
+ .TryAddCoreServices();
return serviceCollection;
}
diff --git a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs
index 3dbcaed6b29..8ffa2935bce 100644
--- a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs
+++ b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs
@@ -1033,7 +1033,7 @@ public static Task SumAsync(
/// A to observe while waiting for the task to complete.
///
/// A task that represents the asynchronous operation.
- /// The task result contains the sum of the projected values..
+ /// The task result contains the sum of the projected values.
///
///
/// or is .
@@ -1069,7 +1069,7 @@ public static Task SumAsync(
/// A to observe while waiting for the task to complete.
///
/// A task that represents the asynchronous operation.
- /// The task result contains the sum of the projected values..
+ /// The task result contains the sum of the projected values.
///
///
/// or is .
@@ -1157,7 +1157,7 @@ public static Task SumAsync(
/// A to observe while waiting for the task to complete.
///
/// A task that represents the asynchronous operation.
- /// The task result contains the sum of the projected values..
+ /// The task result contains the sum of the projected values.
///
///
/// or is .
@@ -1192,7 +1192,7 @@ public static Task SumAsync(
/// A to observe while waiting for the task to complete.
///
/// A task that represents the asynchronous operation.
- /// The task result contains the sum of the projected values..
+ /// The task result contains the sum of the projected values.
///
///
/// or is .
@@ -1280,7 +1280,7 @@ public static Task SumAsync(
/// A to observe while waiting for the task to complete.
///
/// A task that represents the asynchronous operation.
- /// The task result contains the sum of the projected values..
+ /// The task result contains the sum of the projected values.
///
///
/// or is .
@@ -1316,7 +1316,7 @@ public static Task SumAsync(
/// A to observe while waiting for the task to complete.
///
/// A task that represents the asynchronous operation.
- /// The task result contains the sum of the projected values..
+ /// The task result contains the sum of the projected values.
///
///
/// or is .
@@ -1404,7 +1404,7 @@ public static Task SumAsync(
/// A to observe while waiting for the task to complete.
///
/// A task that represents the asynchronous operation.
- /// The task result contains the sum of the projected values..
+ /// The task result contains the sum of the projected values.
///
///
/// or is .
@@ -1440,7 +1440,7 @@ public static Task SumAsync(
/// A to observe while waiting for the task to complete.
///
/// A task that represents the asynchronous operation.
- /// The task result contains the sum of the projected values..
+ /// The task result contains the sum of the projected values.
///
///
/// or is .
@@ -1528,7 +1528,7 @@ public static Task SumAsync(
/// A to observe while waiting for the task to complete.
///
/// A task that represents the asynchronous operation.
- /// The task result contains the sum of the projected values..
+ /// The task result contains the sum of the projected values.
///
///
/// or is .
@@ -1564,7 +1564,7 @@ public static Task SumAsync(
/// A to observe while waiting for the task to complete.
///
/// A task that represents the asynchronous operation.
- /// The task result contains the sum of the projected values..
+ /// The task result contains the sum of the projected values.
///
///
/// or is .
diff --git a/src/EFCore/Extensions/EntityFrameworkServiceCollectionExtensions.cs b/src/EFCore/Extensions/EntityFrameworkServiceCollectionExtensions.cs
index 1708ac969f7..ec8c8bd78f2 100644
--- a/src/EFCore/Extensions/EntityFrameworkServiceCollectionExtensions.cs
+++ b/src/EFCore/Extensions/EntityFrameworkServiceCollectionExtensions.cs
@@ -1041,7 +1041,7 @@ public static IServiceCollection AddPooledDbContextFactory
///
///
/// ,
- /// ,
+ /// ,
/// or
///
/// must also be called for the specified configuration to take effect.
@@ -1077,7 +1077,7 @@ public static IServiceCollection ConfigureDbContext
///
///
/// ,
- /// ,
+ /// ,
/// or
///
/// must also be called for the specified configuration to take effect.
diff --git a/src/EFCore/Metadata/ITypeBase.cs b/src/EFCore/Metadata/ITypeBase.cs
index f1a76022bc8..45aa87e5bc3 100644
--- a/src/EFCore/Metadata/ITypeBase.cs
+++ b/src/EFCore/Metadata/ITypeBase.cs
@@ -195,7 +195,7 @@ public interface ITypeBase : IReadOnlyTypeBase, IAnnotatable
new IPropertyBase? FindMember(string name);
///
- /// Gets the members with the given name on this type, base types or derived types..
+ /// Gets the members with the given name on this type, base types or derived types.
///
/// Type members.
new IEnumerable FindMembersInHierarchy(string name);
diff --git a/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs b/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs
index 99ae637bde2..7cab26ed8f3 100644
--- a/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs
+++ b/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs
@@ -91,8 +91,7 @@ var migrationAssembly
services.GetRequiredService()),
idGenerator,
new MigrationsCodeGeneratorSelector(
- new[]
- {
+ [
new CSharpMigrationsGenerator(
new MigrationsCodeGeneratorDependencies(
sqlServerTypeMappingSource,
@@ -105,7 +104,7 @@ var migrationAssembly
new CSharpSnapshotGenerator(
new CSharpSnapshotGeneratorDependencies(
code, sqlServerTypeMappingSource, sqlServerAnnotationCodeGenerator))))
- }),
+ ]),
historyRepository,
reporter,
new MockProvider(),
@@ -123,7 +122,10 @@ var migrationAssembly
services.GetRequiredService(),
services.GetRequiredService>(),
services.GetRequiredService(),
- services.GetRequiredService())));
+ services.GetRequiredService(),
+ services.GetServices(),
+ services.GetRequiredService(),
+ services.GetRequiredService())));
}
// ReSharper disable once UnusedTypeParameter
diff --git a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsInfrastructureTestBase.cs b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsInfrastructureTestBase.cs
index a75f37bd012..8aac3a0b618 100644
--- a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsInfrastructureTestBase.cs
+++ b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsInfrastructureTestBase.cs
@@ -3,7 +3,7 @@
// ReSharper disable InconsistentNaming
-using Microsoft.EntityFrameworkCore.TestUtilities;
+using Microsoft.EntityFrameworkCore.Diagnostics.Internal;
namespace Microsoft.EntityFrameworkCore.Migrations;
@@ -18,6 +18,7 @@ protected MigrationsInfrastructureTestBase(TFixture fixture)
{
Fixture = fixture;
Fixture.TestStore.CloseConnection();
+ Fixture.TestSqlLoggerFactory.Clear();
}
protected string Sql { get; private set; }
@@ -70,7 +71,12 @@ public virtual void Can_apply_all_migrations()
GiveMeSomeTime(db);
- db.Database.Migrate();
+ MigrationsInfrastructureFixtureBase.MigratorPlugin.ResetCounts();
+ db.Database.Migrate((c, d) =>
+ {
+ c.Add(new MigrationsInfrastructureFixtureBase.Foo { Id = 1, Bar = 10, Description = "Test" });
+ c.SaveChanges();
+ });
var history = db.GetService();
Assert.Collection(
@@ -82,6 +88,47 @@ public virtual void Can_apply_all_migrations()
x => Assert.Equal("00000000000005_Migration5", x.MigrationId),
x => Assert.Equal("00000000000006_Migration6", x.MigrationId),
x => Assert.Equal("00000000000007_Migration7", x.MigrationId));
+
+ Assert.NotNull(db.Find(1));
+
+ Assert.Equal(1, MigrationsInfrastructureFixtureBase.MigratorPlugin.MigratingCallCount);
+ Assert.Equal(1, MigrationsInfrastructureFixtureBase.MigratorPlugin.MigratedCallCount);
+ Assert.Equal(0, MigrationsInfrastructureFixtureBase.MigratorPlugin.MigratingAsyncCallCount);
+ Assert.Equal(0, MigrationsInfrastructureFixtureBase.MigratorPlugin.MigratedAsyncCallCount);
+ }
+
+ [ConditionalFact]
+ public virtual async Task Can_apply_all_migrations_async()
+ {
+ using var db = Fixture.CreateContext();
+ await db.Database.EnsureDeletedAsync();
+
+ await GiveMeSomeTimeAsync(db);
+
+ MigrationsInfrastructureFixtureBase.MigratorPlugin.ResetCounts();
+ await db.Database.MigrateAsync(async (c, d, ct) =>
+ {
+ c.Add(new MigrationsInfrastructureFixtureBase.Foo { Id = 1, Bar = 10, Description = "Test" });
+ await c.SaveChangesAsync(ct);
+ });
+
+ var history = db.GetService();
+ Assert.Collection(
+ await history.GetAppliedMigrationsAsync(),
+ x => Assert.Equal("00000000000001_Migration1", x.MigrationId),
+ x => Assert.Equal("00000000000002_Migration2", x.MigrationId),
+ x => Assert.Equal("00000000000003_Migration3", x.MigrationId),
+ x => Assert.Equal("00000000000004_Migration4", x.MigrationId),
+ x => Assert.Equal("00000000000005_Migration5", x.MigrationId),
+ x => Assert.Equal("00000000000006_Migration6", x.MigrationId),
+ x => Assert.Equal("00000000000007_Migration7", x.MigrationId));
+
+ Assert.NotNull(await db.FindAsync(1));
+
+ Assert.Equal(0, MigrationsInfrastructureFixtureBase.MigratorPlugin.MigratingCallCount);
+ Assert.Equal(0, MigrationsInfrastructureFixtureBase.MigratorPlugin.MigratedCallCount);
+ Assert.Equal(1, MigrationsInfrastructureFixtureBase.MigratorPlugin.MigratingAsyncCallCount);
+ Assert.Equal(1, MigrationsInfrastructureFixtureBase.MigratorPlugin.MigratedAsyncCallCount);
}
[ConditionalFact]
@@ -92,8 +139,7 @@ public virtual void Can_apply_range_of_migrations()
GiveMeSomeTime(db);
- var migrator = db.GetService();
- migrator.Migrate("Migration6");
+ db.Database.Migrate(null, "Migration6");
var history = db.GetService();
Assert.Collection(
@@ -121,6 +167,9 @@ public virtual void Can_apply_one_migration()
Assert.Collection(
history.GetAppliedMigrations(),
x => Assert.Equal("00000000000001_Migration1", x.MigrationId));
+
+ Assert.Equal(LogLevel.Error,
+ Fixture.TestSqlLoggerFactory.Log.Single(l => l.Id == RelationalEventId.PendingModelChangesWarning).Level);
}
[ConditionalFact]
@@ -160,28 +209,6 @@ public virtual void Can_revert_one_migrations()
x => Assert.Equal("00000000000004_Migration4", x.MigrationId));
}
- [ConditionalFact]
- public virtual async Task Can_apply_all_migrations_async()
- {
- using var db = Fixture.CreateContext();
- await db.Database.EnsureDeletedAsync();
-
- await GiveMeSomeTimeAsync(db);
-
- await db.Database.MigrateAsync();
-
- var history = db.GetService();
- Assert.Collection(
- await history.GetAppliedMigrationsAsync(),
- x => Assert.Equal("00000000000001_Migration1", x.MigrationId),
- x => Assert.Equal("00000000000002_Migration2", x.MigrationId),
- x => Assert.Equal("00000000000003_Migration3", x.MigrationId),
- x => Assert.Equal("00000000000004_Migration4", x.MigrationId),
- x => Assert.Equal("00000000000005_Migration5", x.MigrationId),
- x => Assert.Equal("00000000000006_Migration6", x.MigrationId),
- x => Assert.Equal("00000000000007_Migration7", x.MigrationId));
- }
-
[ConditionalFact]
public virtual void Can_apply_one_migration_in_parallel()
{
@@ -444,12 +471,16 @@ public abstract class MigrationsInfrastructureFixtureBase
protected override IServiceCollection AddServices(IServiceCollection serviceCollection)
{
TestStore.UseConnectionString = true;
- return base.AddServices(serviceCollection);
+ return base.AddServices(serviceCollection)
+ .AddSingleton();
}
protected override string StoreName
=> "MigrationsTest";
+ public TestSqlLoggerFactory TestSqlLoggerFactory
+ => (TestSqlLoggerFactory)ListLoggerFactory;
+
public EmptyMigrationsContext CreateEmptyContext()
=> new(
TestStore.AddProviderOptions(
@@ -460,9 +491,6 @@ public EmptyMigrationsContext CreateEmptyContext()
.BuildServiceProvider(validateScopes: true))
.Options);
- public new virtual MigrationsContext CreateContext()
- => base.CreateContext();
-
public class EmptyMigrationsContext(DbContextOptions options) : DbContext(options);
public class MigrationsContext(DbContextOptions options) : PoolableDbContext(options)
@@ -470,12 +498,63 @@ public class MigrationsContext(DbContextOptions options) : PoolableDbContext(opt
public DbSet Foos { get; set; }
}
+ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
+ {
+ modelBuilder.Entity(b => b.ToTable("Table1"));
+ }
+
+ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
+ => base.AddOptions(builder).ConfigureWarnings(e => e
+ .Log(RelationalEventId.PendingModelChangesWarning)
+ .Log(RelationalEventId.NonTransactionalMigrationOperationWarning)
+ );
+
+ protected override bool ShouldLogCategory(string logCategory)
+ => logCategory == DbLoggerCategory.Migrations.Name;
+
public class Foo
{
public int Id { get; set; }
+ public int Bar { get; set; }
public string Description { get; set; }
}
+ public class MigratorPlugin : IMigratorPlugin
+ {
+ public static int MigratedCallCount { get; private set; }
+ public static int MigratedAsyncCallCount { get; private set; }
+ public static int MigratingCallCount { get; private set; }
+ public static int MigratingAsyncCallCount { get; private set; }
+
+ public static void ResetCounts()
+ {
+ MigratedCallCount = 0;
+ MigratedAsyncCallCount = 0;
+ MigratingCallCount = 0;
+ MigratingAsyncCallCount = 0;
+ }
+
+ public void Migrated(DbContext context, IMigratorData data)
+ {
+ MigratedCallCount++;
+ }
+
+ public Task MigratedAsync(DbContext context, IMigratorData data, CancellationToken cancellationToken)
+ {
+ MigratedAsyncCallCount++;
+ return Task.CompletedTask;
+ }
+
+ public void Migrating(DbContext context, IMigratorData data)
+ => MigratingCallCount++;
+
+ public Task MigratingAsync(DbContext context, IMigratorData data, CancellationToken cancellationToken = default)
+ {
+ MigratingAsyncCallCount++;
+ return Task.CompletedTask;
+ }
+ }
+
[DbContext(typeof(MigrationsContext))]
[Migration("00000000000001_Migration1")]
private class Migration1 : Migration
diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalEventIdTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalEventIdTest.cs
index 2c5943573bb..79caba6efc0 100644
--- a/test/EFCore.Relational.Tests/Infrastructure/RelationalEventIdTest.cs
+++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalEventIdTest.cs
@@ -6,6 +6,7 @@
using System.Transactions;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
+using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.TestUtilities.FakeProvider;
using Index = Microsoft.EntityFrameworkCore.Metadata.Internal.Index;
@@ -58,6 +59,7 @@ public void Every_eventId_has_a_logger_method_and_logs_when_level_enabled()
{ typeof(IMigrator), () => new FakeMigrator() },
{ typeof(Migration), () => new FakeMigration() },
{ typeof(IMigrationsAssembly), () => new FakeMigrationsAssembly() },
+ { typeof(MigrationCommand), () => new FakeMigrationCommand() },
{ typeof(MethodCallExpression), () => Expression.Call(constantExpression, typeof(object).GetMethod("ToString")) },
{ typeof(Expression), () => constantExpression },
{ typeof(IEntityType), () => entityType },
@@ -146,6 +148,18 @@ public string GenerateScript(
string toMigration = null,
MigrationsSqlGenerationOptions options = MigrationsSqlGenerationOptions.Default)
=> throw new NotImplementedException();
+
+ public void Migrate(string targetMigration, Action seed, TimeSpan? lockTimeout)
+ => throw new NotImplementedException();
+
+ public Task MigrateAsync(string targetMigration,
+ Func seed,
+ TimeSpan? lockTimeout,
+ CancellationToken cancellationToken = default)
+ => throw new NotImplementedException();
+
+ public bool HasPendingModelChanges()
+ => throw new NotImplementedException();
}
private class FakeMigrationsAssembly : IMigrationsAssembly
@@ -166,6 +180,45 @@ public string FindMigrationId(string nameOrId)
=> throw new NotImplementedException();
}
+ private class FakeMigrationCommand : MigrationCommand
+ {
+ public FakeMigrationCommand()
+ : base(new FakeRelationalCommand(), null, new FakeRelationalCommandDiagnosticsLogger())
+ {
+ }
+ }
+
+ private class FakeRelationalCommand : IRelationalCommand
+ {
+ public string CommandText { get; } = "";
+
+ public IReadOnlyList Parameters { get; } = [];
+
+ public DbCommand CreateDbCommand(RelationalCommandParameterObject parameterObject, Guid commandId, DbCommandMethod commandMethod)
+ => throw new NotImplementedException();
+
+ public int ExecuteNonQuery(RelationalCommandParameterObject parameterObject)
+ => throw new NotImplementedException();
+
+ public Task ExecuteNonQueryAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
+ => throw new NotImplementedException();
+
+ public RelationalDataReader ExecuteReader(RelationalCommandParameterObject parameterObject)
+ => throw new NotImplementedException();
+
+ public Task ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
+ => throw new NotImplementedException();
+
+ public object ExecuteScalar(RelationalCommandParameterObject parameterObject)
+ => throw new NotImplementedException();
+
+ public Task