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 ExecuteScalarAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken) + => throw new NotImplementedException(); + + public void PopulateFrom(IRelationalCommandTemplate commandTemplate) + => throw new NotImplementedException(); + } + private class FakeRelationalConnection : IRelationalConnection { public string ConnectionString { get; set; } diff --git a/test/EFCore.Relational.Tests/Migrations/MigrationCommandExecutorTest.cs b/test/EFCore.Relational.Tests/Migrations/MigrationCommandExecutorTest.cs index 57ef0609153..f428f1e65c9 100644 --- a/test/EFCore.Relational.Tests/Migrations/MigrationCommandExecutorTest.cs +++ b/test/EFCore.Relational.Tests/Migrations/MigrationCommandExecutorTest.cs @@ -22,7 +22,7 @@ public async Task Executes_migration_commands_in_same_transaction(bool async) new(CreateRelationalCommand(), null, logger), new(CreateRelationalCommand(), null, logger) }; - var migrationCommandExecutor = new MigrationCommandExecutor(); + var migrationCommandExecutor = CreateMigrationCommandExecutor(); if (async) { @@ -63,7 +63,7 @@ public async Task Executes_migration_commands_in_user_transaction(bool async) new(CreateRelationalCommand(), null, logger), new(CreateRelationalCommand(), null, logger) }; - var migrationCommandExecutor = new MigrationCommandExecutor(); + var migrationCommandExecutor = CreateMigrationCommandExecutor(); IDbContextTransaction tx; using (tx = fakeConnection.BeginTransaction()) @@ -113,7 +113,7 @@ public async Task Executes_transaction_suppressed_migration_commands_in_user_tra new(CreateRelationalCommand(), null, logger), new(CreateRelationalCommand(), null, logger, transactionSuppressed: true) }; - var migrationCommandExecutor = new MigrationCommandExecutor(); + var migrationCommandExecutor = CreateMigrationCommandExecutor(); IDbContextTransaction tx; using (tx = fakeConnection.BeginTransaction()) @@ -122,17 +122,15 @@ public async Task Executes_transaction_suppressed_migration_commands_in_user_tra { Assert.Equal( RelationalStrings.TransactionSuppressedMigrationInUserTransaction, - (await Assert.ThrowsAsync( - async () - => await migrationCommandExecutor.ExecuteNonQueryAsync(commandList, fakeConnection))).Message); + (await Assert.ThrowsAsync(async () + => await migrationCommandExecutor.ExecuteNonQueryAsync(commandList, fakeConnection))).Message); } else { Assert.Equal( RelationalStrings.TransactionSuppressedMigrationInUserTransaction, - Assert.Throws( - () - => migrationCommandExecutor.ExecuteNonQuery(commandList, fakeConnection)).Message); + Assert.Throws(() + => migrationCommandExecutor.ExecuteNonQuery(commandList, fakeConnection)).Message); } tx.Rollback(); @@ -166,7 +164,7 @@ public async Task Executes_migration_commands_with_transaction_suppressed_outsid new(CreateRelationalCommand(), null, logger, transactionSuppressed: true) }; - var migrationCommandExecutor = new MigrationCommandExecutor(); + var migrationCommandExecutor = CreateMigrationCommandExecutor(); if (async) { @@ -201,7 +199,7 @@ public async Task Ends_transaction_when_transaction_is_suppressed(bool async) new(CreateRelationalCommand(), null, logger), new(CreateRelationalCommand(), null, logger, transactionSuppressed: true) }; - var migrationCommandExecutor = new MigrationCommandExecutor(); + var migrationCommandExecutor = CreateMigrationCommandExecutor(); if (async) { @@ -241,7 +239,7 @@ public async Task Begins_new_transaction_when_transaction_nolonger_suppressed(bo new(CreateRelationalCommand(), null, logger, transactionSuppressed: true), new(CreateRelationalCommand(), null, logger) }; - var migrationCommandExecutor = new MigrationCommandExecutor(); + var migrationCommandExecutor = CreateMigrationCommandExecutor(); if (async) { @@ -283,7 +281,7 @@ public async Task Executes_commands_in_order_regardless_of_transaction_suppressi new(CreateRelationalCommand(commandText: "Third"), null, logger) }; - var migrationCommandExecutor = new MigrationCommandExecutor(); + var migrationCommandExecutor = CreateMigrationCommandExecutor(); if (async) { @@ -353,19 +351,17 @@ public async Task Disposes_transaction_on_exception(bool async) var commandList = new List { new(CreateRelationalCommand(), null, logger) }; - var migrationCommandExecutor = new MigrationCommandExecutor(); + var migrationCommandExecutor = CreateMigrationCommandExecutor(); if (async) { - await Assert.ThrowsAsync( - async () - => await migrationCommandExecutor.ExecuteNonQueryAsync(commandList, fakeConnection)); + await Assert.ThrowsAsync(async () + => await migrationCommandExecutor.ExecuteNonQueryAsync(commandList, fakeConnection)); } else { - Assert.Throws( - () - => migrationCommandExecutor.ExecuteNonQuery(commandList, fakeConnection)); + Assert.Throws(() + => migrationCommandExecutor.ExecuteNonQuery(commandList, fakeConnection)); } Assert.Equal(1, fakeDbConnection.OpenCount); @@ -377,6 +373,9 @@ await Assert.ThrowsAsync( Assert.Equal(0, fakeDbConnection.DbTransactions[0].RollbackCount); } + private static IMigrationCommandExecutor CreateMigrationCommandExecutor() + => FakeRelationalTestHelpers.Instance.CreateContextServices().GetRequiredService(); + private const string ConnectionString = "Fake Connection String"; private static FakeRelationalConnection CreateConnection(IDbContextOptions options = null) diff --git a/test/EFCore.Relational.Tests/TestUtilities/FakeProvider/FakeRelationalOptionsExtension.cs b/test/EFCore.Relational.Tests/TestUtilities/FakeProvider/FakeRelationalOptionsExtension.cs index 940391a6704..c23460f4ea7 100644 --- a/test/EFCore.Relational.Tests/TestUtilities/FakeProvider/FakeRelationalOptionsExtension.cs +++ b/test/EFCore.Relational.Tests/TestUtilities/FakeProvider/FakeRelationalOptionsExtension.cs @@ -27,7 +27,7 @@ public override void ApplyServices(IServiceCollection services) public static IServiceCollection AddEntityFrameworkRelationalDatabase(IServiceCollection serviceCollection) { - var builder = new EntityFrameworkRelationalServicesBuilder(serviceCollection) + new EntityFrameworkRelationalServicesBuilder(serviceCollection) .TryAdd() .TryAdd>() .TryAdd() @@ -38,9 +38,8 @@ public static IServiceCollection AddEntityFrameworkRelationalDatabase(IServiceCo .TryAdd(_ => null) .TryAdd() .TryAdd() - .TryAdd(); - - builder.TryAddCoreServices(); + .TryAdd() + .TryAddCoreServices(); return serviceCollection; } diff --git a/test/EFCore.Specification.Tests/DataAnnotationTestBase.cs b/test/EFCore.Specification.Tests/DataAnnotationTestBase.cs index a6561f78f0c..acbdcf13877 100644 --- a/test/EFCore.Specification.Tests/DataAnnotationTestBase.cs +++ b/test/EFCore.Specification.Tests/DataAnnotationTestBase.cs @@ -2192,7 +2192,7 @@ public virtual void InversePropertyAttribute_pointing_to_same_nav_on_base_causes + $" {nameof(MultipleAnswersInverse)}.{nameof(MultipleAnswersInverse.Answers)}", nameof(PartialAnswerInverse.Answer)), "CoreEventId.MultipleInversePropertiesSameTargetWarning"), - Assert.Throws(() => modelBuilder.FinalizeModel()).Message); + Assert.Throws(modelBuilder.FinalizeModel).Message); } [ConditionalFact] diff --git a/test/EFCore.Specification.Tests/TestUtilities/ListLoggerFactory.cs b/test/EFCore.Specification.Tests/TestUtilities/ListLoggerFactory.cs index 79dc7e6a916..e1ed535fa12 100644 --- a/test/EFCore.Specification.Tests/TestUtilities/ListLoggerFactory.cs +++ b/test/EFCore.Specification.Tests/TestUtilities/ListLoggerFactory.cs @@ -48,12 +48,7 @@ public virtual ILogger CreateLogger(string name) } private void CheckDisposed() - { - if (_disposed) - { - throw new ObjectDisposedException(nameof(ListLoggerFactory)); - } - } + => ObjectDisposedException.ThrowIf(_disposed, typeof(ListLoggerFactory)); public void AddProvider(ILoggerProvider provider) => CheckDisposed(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsInfrastructureSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsInfrastructureSqlServerTest.cs index c3f6e105bdd..bd54bca902e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsInfrastructureSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsInfrastructureSqlServerTest.cs @@ -3,8 +3,10 @@ using Identity30.Data; using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; using Microsoft.EntityFrameworkCore.TestModels.AspNetIdentity; +using static Microsoft.EntityFrameworkCore.DbLoggerCategory; #nullable disable @@ -16,10 +18,21 @@ public class MigrationsInfrastructureSqlServerTest(MigrationsInfrastructureSqlSe : MigrationsInfrastructureTestBase(fixture) { public override void Can_apply_all_migrations() // Issue #32826 - => Assert.Throws(() => base.Can_apply_all_migrations()); + => Assert.Throws(base.Can_apply_all_migrations); public override Task Can_apply_all_migrations_async() // Issue #32826 - => Assert.ThrowsAsync(() => base.Can_apply_all_migrations_async()); + => Assert.ThrowsAsync(base.Can_apply_all_migrations_async); + + public override void Can_apply_range_of_migrations() + { + base.Can_apply_range_of_migrations(); + + var sql = @"CREATE DATABASE TransactionSuppressed; +"; + Assert.Equal(RelationalResources.LogNonTransactionalMigrationOperationWarning(new TestLogger()) + .GenerateMessage(sql, "Migration3"), + Fixture.TestSqlLoggerFactory.Log.Single(l => l.Id == RelationalEventId.NonTransactionalMigrationOperationWarning).Message); + } public override void Can_generate_migration_from_initial_database_to_initial() { @@ -96,12 +109,6 @@ INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) VALUES (N'00000000000001_Migration1', N'7.0.0-test'); GO -COMMIT; -GO - -BEGIN TRANSACTION; -GO - EXEC sp_rename N'[Table1].[Foo]', N'Bar', 'COLUMN'; GO @@ -123,12 +130,6 @@ INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) VALUES (N'00000000000003_Migration3', N'7.0.0-test'); -GO - -COMMIT; -GO - -BEGIN TRANSACTION; GO CREATE PROCEDURE [dbo].[GotoReproduction] @@ -157,12 +158,6 @@ INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) VALUES (N'00000000000004_Migration4', N'7.0.0-test'); GO -COMMIT; -GO - -BEGIN TRANSACTION; -GO - INSERT INTO Table1 (Id, Bar, Description) VALUES (-1, 3, 'Value With Empty Lines') @@ -172,12 +167,6 @@ INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) VALUES (N'00000000000005_Migration5', N'7.0.0-test'); GO -COMMIT; -GO - -BEGIN TRANSACTION; -GO - INSERT INTO Table1 (Id, Bar, Description) VALUES (-2, 4, 'GO Value With @@ -188,12 +177,6 @@ INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) VALUES (N'00000000000006_Migration6', N'7.0.0-test'); GO -COMMIT; -GO - -BEGIN TRANSACTION; -GO - INSERT INTO Table1 (Id, Bar, Description) VALUES (-3, 5, 'GO Value With @@ -420,12 +403,6 @@ INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) END; GO -COMMIT; -GO - -BEGIN TRANSACTION; -GO - IF NOT EXISTS ( SELECT * FROM [__EFMigrationsHistory] WHERE [MigrationId] = N'00000000000002_Migration2' @@ -479,12 +456,6 @@ INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) END; GO -COMMIT; -GO - -BEGIN TRANSACTION; -GO - IF NOT EXISTS ( SELECT * FROM [__EFMigrationsHistory] WHERE [MigrationId] = N'00000000000004_Migration4' @@ -523,12 +494,6 @@ INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) END; GO -COMMIT; -GO - -BEGIN TRANSACTION; -GO - IF NOT EXISTS ( SELECT * FROM [__EFMigrationsHistory] WHERE [MigrationId] = N'00000000000005_Migration5' @@ -550,12 +515,6 @@ INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) END; GO -COMMIT; -GO - -BEGIN TRANSACTION; -GO - IF NOT EXISTS ( SELECT * FROM [__EFMigrationsHistory] WHERE [MigrationId] = N'00000000000006_Migration6' @@ -578,12 +537,6 @@ INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) END; GO -COMMIT; -GO - -BEGIN TRANSACTION; -GO - IF NOT EXISTS ( SELECT * FROM [__EFMigrationsHistory] WHERE [MigrationId] = N'00000000000007_Migration7' @@ -847,12 +800,6 @@ DELETE FROM [__EFMigrationsHistory] WHERE [MigrationId] = N'00000000000002_Migration2'; GO -COMMIT; -GO - -BEGIN TRANSACTION; -GO - DROP TABLE [Table1]; GO @@ -897,12 +844,6 @@ DELETE FROM [__EFMigrationsHistory] END; GO -COMMIT; -GO - -BEGIN TRANSACTION; -GO - IF EXISTS ( SELECT * FROM [__EFMigrationsHistory] WHERE [MigrationId] = N'00000000000001_Migration1' @@ -989,22 +930,244 @@ public override void Can_get_active_provider() } [ConditionalFact] - public async Task Empty_Migration_Creates_Database() + public void Throws_for_pending_model_changes() { using var context = new BloggingContext( Fixture.TestStore.AddProviderOptions( new DbContextOptionsBuilder().EnableServiceProviderCaching(false)).Options); + + Assert.Equal( + CoreStrings.WarningAsErrorTemplate( + RelationalEventId.PendingModelChangesWarning.ToString(), + RelationalResources.LogPendingModelChanges(new TestLogger()) + .GenerateMessage(nameof(BloggingContext)), + "RelationalEventId.PendingModelChangesWarning"), + (Assert.Throws(context.Database.Migrate)).Message); + } + + [ConditionalFact] + public async Task Throws_for_pending_model_changes_async() + { + using var context = new BloggingContext( + Fixture.TestStore.AddProviderOptions( + new DbContextOptionsBuilder().EnableServiceProviderCaching(false)).Options); + + Assert.Equal( + CoreStrings.WarningAsErrorTemplate( + RelationalEventId.PendingModelChangesWarning.ToString(), + RelationalResources.LogPendingModelChanges(new TestLogger()) + .GenerateMessage(nameof(BloggingContext)), + "RelationalEventId.PendingModelChangesWarning"), + (await Assert.ThrowsAsync(() => context.Database.MigrateAsync())).Message); + } + + [ConditionalFact] + public async Task Empty_Migration_Creates_Database() + { + using var context = new BloggingContext( + Fixture.TestStore.AddProviderOptions( + new DbContextOptionsBuilder().EnableServiceProviderCaching(false)) + .ConfigureWarnings(e => e.Log(RelationalEventId.PendingModelChangesWarning)).Options); + + context.Database.EnsureDeleted(); + GiveMeSomeTime(context); + var creator = (SqlServerDatabaseCreator)context.GetService(); creator.RetryTimeout = TimeSpan.FromMinutes(10); - await context.Database.MigrateAsync(); + await context.Database.MigrateAsync(null, "Empty"); Assert.True(creator.Exists()); } - private class BloggingContext(DbContextOptions options) : DbContext(options) + [ConditionalFact] + public void Non_transactional_migration_is_retried() { + using var context = new BloggingContext( + Fixture.TestStore.AddProviderOptions( + new DbContextOptionsBuilder().EnableServiceProviderCaching(false)) + .ConfigureWarnings(e => e.Log(RelationalEventId.PendingModelChangesWarning)) + .UseLoggerFactory(Fixture.TestSqlLoggerFactory).Options); + + context.Database.EnsureDeleted(); + GiveMeSomeTime(context); + + Fixture.TestSqlLoggerFactory.Clear(); + var creator = (SqlServerDatabaseCreator)context.GetService(); + creator.RetryTimeout = TimeSpan.FromMinutes(10); + + context.Database.Migrate(); + + Assert.Equal( + """ +CREATE DATABASE [MigrationsTest]; + +IF SERVERPROPERTY('EngineEdition') <> 5 +BEGIN + ALTER DATABASE [MigrationsTest] SET READ_COMMITTED_SNAPSHOT ON; +END; + +SELECT 1 + +@LockTimeout='?' (DbType = Double) + +DECLARE @result int; +EXEC @result = sp_getapplock @Resource = '__EFMigrationsLock', @LockOwner = 'Session', @LockMode = 'Exclusive', @LockTimeout = @LockTimeout; +SELECT @result + +SELECT OBJECT_ID(N'[__EFMigrationsHistory]'); + +CREATE TABLE [__EFMigrationsHistory] ( + [MigrationId] nvarchar(150) NOT NULL, + [ProductVersion] nvarchar(32) NOT NULL, + CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId]) +); + +SELECT OBJECT_ID(N'[__EFMigrationsHistory]'); + +SELECT [MigrationId], [ProductVersion] +FROM [__EFMigrationsHistory] +ORDER BY [MigrationId]; + +INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) +VALUES (N'00000000000000_Empty', N'9.0.0-dev'); + +--Before + +IF OBJECT_ID(N'Blogs', N'U') IS NULL +BEGIN + CREATE TABLE [Blogs] ( + [Id] int NOT NULL, + [Name] nvarchar(max) NOT NULL, + CONSTRAINT [PK_Blogs] PRIMARY KEY ([Id]) + ); + + THROW 65536, 'Test', 0; +END + +IF OBJECT_ID(N'Blogs', N'U') IS NULL +BEGIN + CREATE TABLE [Blogs] ( + [Id] int NOT NULL, + [Name] nvarchar(max) NOT NULL, + CONSTRAINT [PK_Blogs] PRIMARY KEY ([Id]) + ); + + THROW 65536, 'Test', 0; +END + +INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) +VALUES (N'00000000000001_Migration1', N'9.0.0-dev'); + +--After + +INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) +VALUES (N'00000000000002_Migration2', N'9.0.0-dev'); + +DECLARE @result int; +EXEC @result = sp_releaseapplock @Resource = '__EFMigrationsLock', @LockOwner = 'Session'; +SELECT @result +""", + Fixture.TestSqlLoggerFactory.Sql, + ignoreLineEndingDifferences: true); + } + + [ConditionalFact] + public async Task Non_transactional_migration_is_retried_async() + { + using var context = new BloggingContext( + Fixture.TestStore.AddProviderOptions( + new DbContextOptionsBuilder().EnableServiceProviderCaching(false)) + .ConfigureWarnings(e => e.Log(RelationalEventId.PendingModelChangesWarning)) + .UseLoggerFactory(Fixture.TestSqlLoggerFactory).Options); + + context.Database.EnsureDeleted(); + GiveMeSomeTime(context); + + Fixture.TestSqlLoggerFactory.Clear(); + + var creator = (SqlServerDatabaseCreator)context.GetService(); + creator.RetryTimeout = TimeSpan.FromMinutes(10); + + await context.Database.MigrateAsync(); + + Assert.Equal( + """ +CREATE DATABASE [MigrationsTest]; + +IF SERVERPROPERTY('EngineEdition') <> 5 +BEGIN + ALTER DATABASE [MigrationsTest] SET READ_COMMITTED_SNAPSHOT ON; +END; + +SELECT 1 + +@LockTimeout='?' (DbType = Double) + +DECLARE @result int; +EXEC @result = sp_getapplock @Resource = '__EFMigrationsLock', @LockOwner = 'Session', @LockMode = 'Exclusive', @LockTimeout = @LockTimeout; +SELECT @result + +SELECT OBJECT_ID(N'[__EFMigrationsHistory]'); + +CREATE TABLE [__EFMigrationsHistory] ( + [MigrationId] nvarchar(150) NOT NULL, + [ProductVersion] nvarchar(32) NOT NULL, + CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId]) +); + +SELECT OBJECT_ID(N'[__EFMigrationsHistory]'); + +SELECT [MigrationId], [ProductVersion] +FROM [__EFMigrationsHistory] +ORDER BY [MigrationId]; + +INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) +VALUES (N'00000000000000_Empty', N'9.0.0-dev'); + +--Before + +IF OBJECT_ID(N'Blogs', N'U') IS NULL +BEGIN + CREATE TABLE [Blogs] ( + [Id] int NOT NULL, + [Name] nvarchar(max) NOT NULL, + CONSTRAINT [PK_Blogs] PRIMARY KEY ([Id]) + ); + + THROW 65536, 'Test', 0; +END + +IF OBJECT_ID(N'Blogs', N'U') IS NULL +BEGIN + CREATE TABLE [Blogs] ( + [Id] int NOT NULL, + [Name] nvarchar(max) NOT NULL, + CONSTRAINT [PK_Blogs] PRIMARY KEY ([Id]) + ); + + THROW 65536, 'Test', 0; +END + +INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) +VALUES (N'00000000000001_Migration1', N'9.0.0-dev'); + +--After + +INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) +VALUES (N'00000000000002_Migration2', N'9.0.0-dev'); + +DECLARE @result int; +EXEC @result = sp_releaseapplock @Resource = '__EFMigrationsLock', @LockOwner = 'Session'; +SELECT @result +""", + Fixture.TestSqlLoggerFactory.Sql, + ignoreLineEndingDifferences: true); + } + + private class BloggingContext(DbContextOptions options) : DbContext(options) + { // ReSharper disable once UnusedMember.Local public DbSet Blogs { get; set; } @@ -1021,13 +1184,53 @@ public class Blog [DbContext(typeof(BloggingContext))] [Migration("00000000000000_Empty")] - public class EmptyMigration : Migration + private class EmptyMigration : Migration { protected override void Up(MigrationBuilder migrationBuilder) { } } + [DbContext(typeof(BloggingContext))] + [Migration("00000000000001_Migration1")] + private class BloggingMigration1 : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("--Before", suppressTransaction: true); + migrationBuilder.Sql(""" +IF OBJECT_ID(N'Blogs', N'U') IS NULL +BEGIN + CREATE TABLE [Blogs] ( + [Id] int NOT NULL, + [Name] nvarchar(max) NOT NULL, + CONSTRAINT [PK_Blogs] PRIMARY KEY ([Id]) + ); + + THROW 65536, 'Test', 0; +END +""", suppressTransaction: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } + + [DbContext(typeof(BloggingContext))] + [Migration("00000000000002_Migration2")] + private class BloggingMigration2 : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("--After"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } + public override void Can_diff_against_2_2_model() { using var context = new ModelSnapshot22.BloggingContext(); @@ -1880,6 +2083,9 @@ public override MigrationsContext CreateContext() .Options; return new MigrationsContext(options); } + + protected override bool ShouldLogCategory(string logCategory) + => base.ShouldLogCategory(logCategory); } } } diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestSqlServerRetryingExecutionStrategy.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestSqlServerRetryingExecutionStrategy.cs index 3c9b11c92da..ec517ff802f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestSqlServerRetryingExecutionStrategy.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestSqlServerRetryingExecutionStrategy.cs @@ -14,7 +14,8 @@ public class TestSqlServerRetryingExecutionStrategy : SqlServerRetryingExecution -1, // Physical connection is not usable -2, // Timeout 42008, // Mirroring (Only when a database is deleted and another one is created in fast succession) - 42019 // CREATE DATABASE operation failed + 42019, // CREATE DATABASE operation failed + 65536, // Used for testing ]; public TestSqlServerRetryingExecutionStrategy() diff --git a/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsInfrastructureSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsInfrastructureSqliteTest.cs index 54e0caea3a0..26799421608 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsInfrastructureSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsInfrastructureSqliteTest.cs @@ -68,33 +68,17 @@ public override void Can_generate_up_scripts() INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") VALUES ('00000000000001_Migration1', '7.0.0-test'); -COMMIT; - -BEGIN TRANSACTION; - ALTER TABLE "Table1" RENAME COLUMN "Foo" TO "Bar"; INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") VALUES ('00000000000002_Migration2', '7.0.0-test'); -COMMIT; - -BEGIN TRANSACTION; - INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") VALUES ('00000000000003_Migration3', '7.0.0-test'); -COMMIT; - -BEGIN TRANSACTION; - INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") VALUES ('00000000000004_Migration4', '7.0.0-test'); -COMMIT; - -BEGIN TRANSACTION; - INSERT INTO Table1 (Id, Bar, Description) VALUES (-1, 3, 'Value With Empty Lines') @@ -102,10 +86,6 @@ INSERT INTO Table1 (Id, Bar, Description) VALUES (-1, 3, 'Value With INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") VALUES ('00000000000005_Migration5', '7.0.0-test'); -COMMIT; - -BEGIN TRANSACTION; - INSERT INTO Table1 (Id, Bar, Description) VALUES (-2, 4, 'GO Value With @@ -114,10 +94,6 @@ Value With INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") VALUES ('00000000000006_Migration6', '7.0.0-test'); -COMMIT; - -BEGIN TRANSACTION; - INSERT INTO Table1 (Id, Bar, Description) VALUES (-3, 5, 'GO Value With @@ -261,10 +237,6 @@ public override void Can_generate_down_scripts() DELETE FROM "__EFMigrationsHistory" WHERE "MigrationId" = '00000000000002_Migration2'; -COMMIT; - -BEGIN TRANSACTION; - DROP TABLE "Table1"; DELETE FROM "__EFMigrationsHistory"