diff --git a/src/EFCore.Relational/Diagnostics/RelationalEventId.cs b/src/EFCore.Relational/Diagnostics/RelationalEventId.cs index c8f84558c2a..a0d76413ed8 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalEventId.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalEventId.cs @@ -77,6 +77,7 @@ private enum Id MigrationsNotFound, MigrationAttributeMissingWarning, ColumnOrderIgnoredWarning, + PendingModelChangesWarning, // Query events QueryClientEvaluationWarning = CoreEventId.RelationalBaseId + 500, @@ -721,6 +722,19 @@ private static EventId MakeMigrationsId(Id id) /// public static readonly EventId ColumnOrderIgnoredWarning = MakeMigrationsId(Id.ColumnOrderIgnoredWarning); + /// + /// Column order was ignored. + /// + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId PendingModelChangesWarning = MakeMigrationsId(Id.PendingModelChangesWarning); + 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..03682b6f468 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs @@ -2309,6 +2309,40 @@ 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 PendingModelChanges( + this IDiagnosticsLogger diagnostics, + Type contextType) + { + var definition = RelationalResources.LogPendingModelChanges(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics, contextType); + } + + 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); + } + /// /// Logs for the event. /// diff --git a/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs b/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs index eb2e5ca3386..680483a0d58 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs @@ -646,6 +646,15 @@ 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 diff --git a/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs index c2cd5273beb..053b406f91e 100644 --- a/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs @@ -116,6 +116,35 @@ 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 optional seed method to run after migrating the database. It will be invoked even if no migrations were applied. + /// + /// + /// The target migration to migrate the database to, or to migrate to the latest. + /// + /// + /// + /// 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) + => databaseFacade.GetRelationalService().Migrate(targetMigration, seed); + /// /// Asynchronously applies any pending migrations for the context to the database. Will create the database /// if it does not already exist. @@ -142,6 +171,40 @@ 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 optional seed method to run after migrating the database. It will be invoked even if no migrations were applied. + /// + /// + /// The target migration to migrate the database to, or to migrate to the latest. + /// + /// 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, + CancellationToken cancellationToken = default) + => databaseFacade.GetRelationalService().MigrateAsync(targetMigration, seed, cancellationToken); + /// /// Executes the given SQL against the database and returns the number of rows affected. /// @@ -974,29 +1037,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..da72feae3c7 100644 --- a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs +++ b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs @@ -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..b15687c3319 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs @@ -439,7 +439,8 @@ 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)); /// /// Information/metadata for a . diff --git a/src/EFCore.Relational/Migrations/IMigrator.cs b/src/EFCore.Relational/Migrations/IMigrator.cs index ac36e8184cc..e6b6984774b 100644 --- a/src/EFCore.Relational/Migrations/IMigrator.cs +++ b/src/EFCore.Relational/Migrations/IMigrator.cs @@ -32,9 +32,12 @@ public interface IMigrator /// /// 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. + /// [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); /// /// Migrates the database to either a specified target migration or up to the latest @@ -46,6 +49,9 @@ public interface IMigrator /// /// 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. + /// /// A to observe while waiting for the task to complete. /// A task that represents the asynchronous operation /// If the is canceled. @@ -53,6 +59,7 @@ public interface IMigrator [RequiresDynamicCode("Migrations operations are not supported with NativeAOT")] Task MigrateAsync( string? targetMigration = null, + Func? seed = null, CancellationToken cancellationToken = default); /// @@ -78,4 +85,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..073542af014 --- /dev/null +++ b/src/EFCore.Relational/Migrations/IMigratorPlugin.cs @@ -0,0 +1,47 @@ +// 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 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 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/Migrator.cs b/src/EFCore.Relational/Migrations/Internal/Migrator.cs index 6263bec3a9b..2f765dd4ada 100644 --- a/src/EFCore.Relational/Migrations/Internal/Migrator.cs +++ b/src/EFCore.Relational/Migrations/Internal/Migrator.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.Diagnostics.Internal; + namespace Microsoft.EntityFrameworkCore.Migrations.Internal; /// @@ -23,6 +25,9 @@ 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; /// @@ -44,7 +49,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 +66,9 @@ public Migrator( _modelRuntimeInitializer = modelRuntimeInitializer; _logger = logger; _commandLogger = commandLogger; + _plugins = plugins; + _migrationsModelDiffer = migrationsModelDiffer; + _designTimeModel = designTimeModel; _activeProvider = databaseProvider.Name; } @@ -75,8 +86,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. /// - public virtual void Migrate(string? targetMigration = null) + public virtual void Migrate(string? targetMigration, Action? seed) { + if (RelationalResources.LogPendingModelChanges(_logger).WarningBehavior != WarningBehavior.Ignore + && HasPendingModelChanges()) + { + _logger.PendingModelChanges(_currentContext.Context.GetType()); + } + _logger.MigrateUsingConnection(this, _connection); if (!_databaseCreator.Exists()) @@ -95,12 +112,28 @@ public virtual void Migrate(string? targetMigration = null) _historyRepository.Create(); } - var commandLists = GetMigrationCommandLists(_historyRepository.GetAppliedMigrations(), targetMigration); + PopulateMigrations( + _historyRepository.GetAppliedMigrations().Select(t => t.MigrationId), + targetMigration, + out var 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 +148,16 @@ 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, CancellationToken cancellationToken = default) { + if (RelationalResources.LogPendingModelChanges(_logger).WarningBehavior != WarningBehavior.Ignore + && HasPendingModelChanges()) + { + _logger.PendingModelChanges(_currentContext.Context.GetType()); + } + _logger.MigrateUsingConnection(this, _connection); if (!await _databaseCreator.ExistsAsync(cancellationToken).ConfigureAwait(false)) @@ -137,15 +177,30 @@ 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); + 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 +208,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++) { @@ -206,9 +256,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 +278,9 @@ protected virtual void PopulateMigrations( } } + IReadOnlyList migrationsToApply; + IReadOnlyList migrationsToRevert; + Migration? actualTargetMigration = null; if (string.IsNullOrEmpty(targetMigration)) { migrationsToApply = unappliedMigrations @@ -237,7 +288,6 @@ protected virtual void PopulateMigrations( .Select(p => _migrationsAssembly.CreateMigration(p.Value, _activeProvider)) .ToList(); migrationsToRevert = []; - actualTargetMigration = null; } else if (targetMigration == Migration.InitialDatabase) { @@ -246,7 +296,6 @@ protected virtual void PopulateMigrations( .OrderByDescending(m => m.Key) .Select(p => _migrationsAssembly.CreateMigration(p.Value, _activeProvider)) .ToList(); - actualTargetMigration = null; } else { @@ -266,6 +315,8 @@ protected virtual void PopulateMigrations( .Select(p => _migrationsAssembly.CreateMigration(p.Value, _activeProvider)) .SingleOrDefault(); } + + parameters = new MigratorData(migrationsToApply, migrationsToRevert, actualTargetMigration); } /// @@ -298,12 +349,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 +364,9 @@ public virtual string GenerateScript( var idempotencyEnd = idempotent ? _historyRepository.GetEndIfScript() : null; + var migrationsToApply = migratorData.AppliedMigrations; + var migrationsToRevert = migratorData.RevertedMigrations; + var actualTargetMigration = migratorData.TargetMigration; for (var i = 0; i < migrationsToRevert.Count; i++) { var migration = migrationsToRevert[i]; @@ -445,6 +494,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..d94c8e9877f 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 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. /// 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( @@ -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( @@ -3647,6 +3647,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..f974ba7aa2c 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -817,6 +817,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 Type + 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 +1033,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. 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/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/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..8faf0632fc3 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs @@ -123,7 +123,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..9a19326d73a 100644 --- a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsInfrastructureTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsInfrastructureTestBase.cs @@ -4,6 +4,7 @@ // ReSharper disable InconsistentNaming using Microsoft.EntityFrameworkCore.TestUtilities; +using static Microsoft.EntityFrameworkCore.Migrations.MigrationsInfrastructureFixtureBase; namespace Microsoft.EntityFrameworkCore.Migrations; @@ -18,6 +19,7 @@ protected MigrationsInfrastructureTestBase(TFixture fixture) { Fixture = fixture; Fixture.TestStore.CloseConnection(); + Fixture.TestSqlLoggerFactory.Clear(); } protected string Sql { get; private set; } @@ -70,7 +72,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 +89,43 @@ 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.MigratedCallCount); + 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.MigratedCallCount); + Assert.Equal(1, MigrationsInfrastructureFixtureBase.MigratorPlugin.MigratedAsyncCallCount); } [ConditionalFact] @@ -92,8 +136,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 +164,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 +206,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 +468,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 +488,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 +495,45 @@ 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)); + + 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 void ResetCounts() + { + MigratedCallCount = 0; + MigratedAsyncCallCount = 0; + } + + public void Migrated(DbContext context, IMigratorData data) + => MigratedCallCount++; + + public Task MigratedAsync(DbContext context, IMigratorData data, CancellationToken cancellationToken) + { + MigratedAsyncCallCount++; + 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..2bca0105019 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalEventIdTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalEventIdTest.cs @@ -146,6 +146,9 @@ public string GenerateScript( string toMigration = null, MigrationsSqlGenerationOptions options = MigrationsSqlGenerationOptions.Default) => throw new NotImplementedException(); + public void Migrate(string targetMigration = null, Action seed = null) => throw new NotImplementedException(); + public Task MigrateAsync(string targetMigration = null, Func seed = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public bool HasPendingModelChanges() => throw new NotImplementedException(); } private class FakeMigrationsAssembly : IMigrationsAssembly 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..03a4e7c57b3 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsInfrastructureSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsInfrastructureSqlServerTest.cs @@ -3,6 +3,7 @@ using Identity30.Data; using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; using Microsoft.EntityFrameworkCore.TestModels.AspNetIdentity; @@ -16,10 +17,10 @@ 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_generate_migration_from_initial_database_to_initial() { @@ -989,11 +990,44 @@ 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(typeof(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(typeof(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); var creator = (SqlServerDatabaseCreator)context.GetService(); creator.RetryTimeout = TimeSpan.FromMinutes(10); @@ -1004,7 +1038,6 @@ public async Task Empty_Migration_Creates_Database() private class BloggingContext(DbContextOptions options) : DbContext(options) { - // ReSharper disable once UnusedMember.Local public DbSet Blogs { get; set; }