From 24b31645e89f8474c1e2b4cb98085d162bb24aa0 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Mon, 25 Nov 2024 17:48:57 -0800 Subject: [PATCH] Add more specific messages when pending model changes are detected. Fixes #35133 --- .../Diagnostics/RelationalEventId.cs | 14 ++ .../Diagnostics/RelationalLoggerExtensions.cs | 71 +++++++ .../RelationalLoggingDefinitions.cs | 20 +- .../Migrations/Internal/Migrator.cs | 85 +++++---- .../Properties/RelationalStrings.Designer.cs | 62 ++++++- .../Properties/RelationalStrings.resx | 16 +- src/EFCore/Infrastructure/ModelSource.cs | 23 ++- ...rpMigrationsGeneratorTest.ModelSnapshot.cs | 5 +- .../MigrationsInfrastructureTestBase.cs | 18 +- .../MigrationsInfrastructureSqlServerTest.cs | 175 +++++++++++++++++- 10 files changed, 424 insertions(+), 65 deletions(-) diff --git a/src/EFCore.Relational/Diagnostics/RelationalEventId.cs b/src/EFCore.Relational/Diagnostics/RelationalEventId.cs index c05c73c46a3..0b1f48c5275 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalEventId.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalEventId.cs @@ -81,6 +81,7 @@ private enum Id NonTransactionalMigrationOperationWarning, AcquiringMigrationLock, MigrationsUserTransactionWarning, + ModelSnapshotNotFound, // Query events QueryClientEvaluationWarning = CoreEventId.RelationalBaseId + 500, @@ -778,6 +779,19 @@ private static EventId MakeMigrationsId(Id id) /// public static readonly EventId MigrationsUserTransactionWarning = MakeMigrationsId(Id.MigrationsUserTransactionWarning); + /// + /// Model snapshot was not found. + /// + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId ModelSnapshotNotFound = MakeMigrationsId(Id.ModelSnapshotNotFound); + 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 177b7e90bba..acecc79ca01 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs @@ -2343,6 +2343,77 @@ private static string PendingModelChanges(EventDefinitionBase definition, EventD return d.GenerateMessage(p.ContextType.ShortDisplayName()); } + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The type being used. + public static void NonDeterministicModel( + this IDiagnosticsLogger diagnostics, + Type contextType) + { + var definition = RelationalResources.LogNonDeterministicModel(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, + NonDeterministicModel, + contextType); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + } + } + + private static string NonDeterministicModel(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 migrator. + /// The assembly in which migrations are stored. + public static void ModelSnapshotNotFound( + this IDiagnosticsLogger diagnostics, + IMigrator migrator, + IMigrationsAssembly migrationsAssembly) + { + var definition = RelationalResources.LogNoModelSnapshotFound(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics, migrationsAssembly.Assembly.GetName().Name!); + } + + if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = new MigrationAssemblyEventData( + definition, + ModelSnapshotNotFound, + migrator, + migrationsAssembly); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + } + } + + private static string ModelSnapshotNotFound(EventDefinitionBase definition, EventData payload) + { + var d = (EventDefinition)definition; + var p = (MigrationAssemblyEventData)payload; + return d.GenerateMessage(p.MigrationsAssembly.Assembly.GetName().Name!); + } + /// /// Logs for the event. /// diff --git a/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs b/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs index 762172c8944..9634f027bf7 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs @@ -374,7 +374,7 @@ public abstract class RelationalLoggingDefinitions : LoggingDefinitions /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] - public EventDefinitionBase? LogMigrationsUserTransactionWarning; + public EventDefinitionBase? LogMigrationsUserTransaction; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -673,6 +673,24 @@ public abstract class RelationalLoggingDefinitions : LoggingDefinitions [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? LogNonDeterministicModel; + + /// + /// 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? LogNoModelSnapshotFound; + /// /// 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/Migrations/Internal/Migrator.cs b/src/EFCore.Relational/Migrations/Internal/Migrator.cs index 4380d89fd82..ea758d2c634 100644 --- a/src/EFCore.Relational/Migrations/Internal/Migrator.cs +++ b/src/EFCore.Relational/Migrations/Internal/Migrator.cs @@ -3,6 +3,7 @@ using System.Transactions; using Microsoft.EntityFrameworkCore.Diagnostics.Internal; +using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace Microsoft.EntityFrameworkCore.Migrations.Internal; @@ -93,24 +94,7 @@ public Migrator( public virtual void Migrate(string? targetMigration) { var useTransaction = _connection.CurrentTransaction is null; - if (!useTransaction - && _executionStrategy.RetriesOnFailure) - { - throw new NotSupportedException(RelationalStrings.TransactionSuppressedMigrationInUserTransaction); - } - - if (RelationalResources.LogPendingModelChanges(_logger).WarningBehavior != WarningBehavior.Ignore - && HasPendingModelChanges()) - { - _logger.PendingModelChangesWarning(_currentContext.Context.GetType()); - } - - if (!useTransaction) - { - _logger.MigrationsUserTransactionWarning(); - } - - _logger.MigrateUsingConnection(this, _connection); + ValidateMigrations(useTransaction); using var transactionScope = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled); @@ -235,24 +219,7 @@ public virtual async Task MigrateAsync( CancellationToken cancellationToken = default) { var useTransaction = _connection.CurrentTransaction is null; - if (!useTransaction - && _executionStrategy.RetriesOnFailure) - { - throw new NotSupportedException(RelationalStrings.TransactionSuppressedMigrationInUserTransaction); - } - - if (RelationalResources.LogPendingModelChanges(_logger).WarningBehavior != WarningBehavior.Ignore - && HasPendingModelChanges()) - { - _logger.PendingModelChangesWarning(_currentContext.Context.GetType()); - } - - if (!useTransaction) - { - _logger.MigrationsUserTransactionWarning(); - } - - _logger.MigrateUsingConnection(this, _connection); + ValidateMigrations(useTransaction); using var transactionScope = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled); @@ -382,6 +349,48 @@ await _migrationCommandExecutor.ExecuteNonQueryAsync( } } + private void ValidateMigrations(bool useTransaction) + { + if (!useTransaction + && _executionStrategy.RetriesOnFailure) + { + throw new NotSupportedException(RelationalStrings.TransactionSuppressedMigrationInUserTransaction); + } + + if (_migrationsAssembly.Migrations.Count == 0) + { + _logger.MigrationsNotFound(this, _migrationsAssembly); + } + else if (_migrationsAssembly.ModelSnapshot == null) + { + _logger.ModelSnapshotNotFound(this, _migrationsAssembly); + } + else if (RelationalResources.LogPendingModelChanges(_logger).WarningBehavior != WarningBehavior.Ignore + && HasPendingModelChanges()) + { + var modelSource = (ModelSource)_currentContext.Context.GetService(); +#pragma warning disable EF1001 // Internal EF Core API usage. + var newDesignTimeModel = modelSource.CreateModel( + _currentContext.Context, _currentContext.Context.GetService(), designTime: true); +#pragma warning restore EF1001 // Internal EF Core API usage. + if (_migrationsModelDiffer.HasDifferences(newDesignTimeModel.GetRelationalModel(), _designTimeModel.Model.GetRelationalModel())) + { + _logger.NonDeterministicModel(_currentContext.Context.GetType()); + } + else + { + _logger.PendingModelChangesWarning(_currentContext.Context.GetType()); + } + } + + if (!useTransaction) + { + _logger.MigrationsUserTransactionWarning(); + } + + _logger.MigrateUsingConnection(this, _connection); + } + private IEnumerable<(string, Func>)> GetMigrationCommandLists(MigratorData parameters) { var migrationsToApply = parameters.AppliedMigrations; @@ -449,10 +458,6 @@ protected virtual void PopulateMigrations( var appliedMigrations = new Dictionary(); var unappliedMigrations = new Dictionary(); var appliedMigrationEntrySet = new HashSet(appliedMigrationEntries, StringComparer.OrdinalIgnoreCase); - if (_migrationsAssembly.Migrations.Count == 0) - { - _logger.MigrationsNotFound(this, _migrationsAssembly); - } foreach (var (key, typeInfo) in _migrationsAssembly.Migrations) { diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 731c62c4ec2..5bbbd42c9d0 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -3429,11 +3429,11 @@ public static EventDefinition LogMigrationAttributeMissingWarning(IDiagn /// public static EventDefinition LogMigrationsUserTransaction(IDiagnosticsLogger logger) { - var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogMigrationsUserTransactionWarning; + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogMigrationsUserTransaction; if (definition == null) { definition = NonCapturingLazyInitializer.EnsureInitialized( - ref ((RelationalLoggingDefinitions)logger.Definitions).LogMigrationsUserTransactionWarning, + ref ((RelationalLoggingDefinitions)logger.Definitions).LogMigrationsUserTransaction, logger, static logger => new EventDefinition( logger.Options, @@ -3572,7 +3572,7 @@ public static EventDefinition LogNoMigrationsApplied(IDiagnosticsLogger logger) } /// - /// No migrations were found in assembly '{migrationsAssembly}'. + /// No migrations were found in assembly '{migrationsAssembly}'. A migration needs to be added before the database can be updated. /// public static EventDefinition LogNoMigrationsFound(IDiagnosticsLogger logger) { @@ -3585,7 +3585,7 @@ public static EventDefinition LogNoMigrationsFound(IDiagnosticsLogger lo static logger => new EventDefinition( logger.Options, RelationalEventId.MigrationsNotFound, - LogLevel.Debug, + LogLevel.Information, "RelationalEventId.MigrationsNotFound", level => LoggerMessage.Define( level, @@ -3596,6 +3596,56 @@ public static EventDefinition LogNoMigrationsFound(IDiagnosticsLogger lo return (EventDefinition)definition; } + /// + /// Model snapshot was not found in assembly '{migrationsAssembly}'. Skipping pending model changes check. + /// + public static EventDefinition LogNoModelSnapshotFound(IDiagnosticsLogger logger) + { + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogNoModelSnapshotFound; + if (definition == null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((RelationalLoggingDefinitions)logger.Definitions).LogNoModelSnapshotFound, + logger, + static logger => new EventDefinition( + logger.Options, + RelationalEventId.ModelSnapshotNotFound, + LogLevel.Information, + "RelationalEventId.ModelSnapshotNotFound", + level => LoggerMessage.Define( + level, + RelationalEventId.ModelSnapshotNotFound, + _resourceManager.GetString("LogNoModelSnapshotFound")!))); + } + + return (EventDefinition)definition; + } + + /// + /// The model for context '{contextType}' changes each time it is built. This is usually caused by dynamic values used in a 'HasData' call (e.g. `new DateTime()`, `Guid.NewGuid()`). Add a new migration and examine its contents to locate the cause, and replace the dynamic call with a static, hardcoded value. See https://aka.ms/efcore-docs-pending-changes. + /// + public static EventDefinition LogNonDeterministicModel(IDiagnosticsLogger logger) + { + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogNonDeterministicModel; + if (definition == null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((RelationalLoggingDefinitions)logger.Definitions).LogNonDeterministicModel, + logger, + static logger => new EventDefinition( + logger.Options, + RelationalEventId.PendingModelChangesWarning, + LogLevel.Error, + "RelationalEventId.PendingModelChangesWarning", + level => LoggerMessage.Define( + level, + RelationalEventId.PendingModelChangesWarning, + _resourceManager.GetString("LogNonDeterministicModel")!))); + } + + 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. /// @@ -3610,7 +3660,7 @@ public static EventDefinition LogNonTransactionalMigrationOperat static logger => new EventDefinition( logger.Options, RelationalEventId.NonTransactionalMigrationOperationWarning, - LogLevel.Error, + LogLevel.Warning, "RelationalEventId.NonTransactionalMigrationOperationWarning", level => LoggerMessage.Define( level, @@ -3747,7 +3797,7 @@ public static EventDefinition LogOptionalDependentWithoutIdentifyingProp } /// - /// The model for context '{contextType}' has pending changes. Add a new migration before updating the database. + /// The model for context '{contextType}' has pending changes. Add a new migration before updating the database. See https://aka.ms/efcore-docs-pending-changes. /// public static EventDefinition LogPendingModelChanges(IDiagnosticsLogger logger) { diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 104daf9dafb..2382f648428 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -811,12 +811,20 @@ Information RelationalEventId.MigrationsNotApplied - No migrations were found in assembly '{migrationsAssembly}'. - Debug RelationalEventId.MigrationsNotFound string + No migrations were found in assembly '{migrationsAssembly}'. A migration needs to be added before the database can be updated. + Information RelationalEventId.MigrationsNotFound string + + + Model snapshot was not found in assembly '{migrationsAssembly}'. Skipping pending model changes check. + Information RelationalEventId.ModelSnapshotNotFound string + + + The model for context '{contextType}' changes each time it is built. This is usually caused by dynamic values used in a 'HasData' call (e.g. `new DateTime()`, `Guid.NewGuid()`). Add a new migration and examine its contents to locate the cause, and replace the dynamic call with a static, hardcoded value. See https://aka.ms/efcore-docs-pending-changes. + Error RelationalEventId.PendingModelChangesWarning 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 + Warning RelationalEventId.NonTransactionalMigrationOperationWarning string string Opened connection to database '{database}' on server '{server}'. @@ -839,7 +847,7 @@ Warning RelationalEventId.OptionalDependentWithoutIdentifyingPropertyWarning string - The model for context '{contextType}' has pending changes. Add a new migration before updating the database. + The model for context '{contextType}' has pending changes. Add a new migration before updating the database. See https://aka.ms/efcore-docs-pending-changes. Error RelationalEventId.PendingModelChangesWarning string diff --git a/src/EFCore/Infrastructure/ModelSource.cs b/src/EFCore/Infrastructure/ModelSource.cs index d1113d8bd27..e69a0e42e3a 100644 --- a/src/EFCore/Infrastructure/ModelSource.cs +++ b/src/EFCore/Infrastructure/ModelSource.cs @@ -65,11 +65,7 @@ public virtual IModel GetModel( { if (!cache.TryGetValue(cacheKey, out model)) { - model = CreateModel( - context, modelCreationDependencies.ConventionSetBuilder, modelCreationDependencies.ModelDependencies); - - var designTimeModel = modelCreationDependencies.ModelRuntimeInitializer.Initialize( - model, designTime: true, modelCreationDependencies.ValidationLogger); + var designTimeModel = CreateModel(context, modelCreationDependencies, designTime: true); var runtimeModel = (IModel)designTimeModel.FindRuntimeAnnotationValue(CoreAnnotationNames.ReadOnlyModel)!; @@ -88,6 +84,23 @@ public virtual IModel GetModel( return 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. + /// + [EntityFrameworkInternal] + public virtual IModel CreateModel( + DbContext context, + ModelCreationDependencies modelCreationDependencies, + bool designTime) + { + var model = CreateModel(context, modelCreationDependencies.ConventionSetBuilder, modelCreationDependencies.ModelDependencies); + return modelCreationDependencies.ModelRuntimeInitializer.Initialize( + model, designTime, modelCreationDependencies.ValidationLogger); + } + /// /// Creates the model. This method is called when the model was not found in the cache. /// diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs index d9216f9dfe5..ce667239197 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs @@ -8570,10 +8570,7 @@ protected CSharpMigrationsGenerator CreateMigrationsGenerator() var sqlServerTypeMappingSource = new SqlServerTypeMappingSource( TestServiceFactory.Instance.Create(), new RelationalTypeMappingSourceDependencies( - new IRelationalTypeMappingSourcePlugin[] - { - new SqlServerNetTopologySuiteTypeMappingSourcePlugin(NtsGeometryServices.Instance) - })); + [new SqlServerNetTopologySuiteTypeMappingSourcePlugin(NtsGeometryServices.Instance)])); var codeHelper = new CSharpHelper(sqlServerTypeMappingSource); diff --git a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsInfrastructureTestBase.cs b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsInfrastructureTestBase.cs index 25844386efa..a00f1c67989 100644 --- a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsInfrastructureTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsInfrastructureTestBase.cs @@ -154,8 +154,8 @@ public virtual void Can_apply_one_migration() x => Assert.Equal("00000000000001_Migration1", x.MigrationId)); Assert.Equal( - LogLevel.Error, - Fixture.TestSqlLoggerFactory.Log.Single(l => l.Id == RelationalEventId.PendingModelChangesWarning).Level); + LogLevel.Information, + Fixture.TestSqlLoggerFactory.Log.Single(l => l.Id == RelationalEventId.ModelSnapshotNotFound).Level); } [ConditionalFact] @@ -291,6 +291,10 @@ public virtual async Task Can_generate_no_migration_script() using var db = Fixture.CreateEmptyContext(); var migrator = db.GetService(); + await db.Database.EnsureDeletedAsync(); + await GiveMeSomeTimeAsync(db); + await db.GetService().CreateAsync(); + await SetAndExecuteSqlAsync(migrator.GenerateScript()); } @@ -300,6 +304,10 @@ public virtual async Task Can_generate_migration_from_initial_database_to_initia using var db = Fixture.CreateContext(); var migrator = db.GetService(); + await db.Database.EnsureDeletedAsync(); + await GiveMeSomeTimeAsync(db); + await db.GetService().CreateAsync(); + await SetAndExecuteSqlAsync(migrator.GenerateScript(fromMigration: Migration.InitialDatabase, toMigration: Migration.InitialDatabase)); } @@ -310,6 +318,7 @@ public virtual async Task Can_generate_up_and_down_scripts() var migrator = db.GetService(); await db.Database.EnsureDeletedAsync(); + await GiveMeSomeTimeAsync(db); await db.GetService().CreateAsync(); await SetAndExecuteSqlAsync(migrator.GenerateScript()); @@ -327,6 +336,7 @@ public virtual async Task Can_generate_up_and_down_scripts_noTransactions() var migrator = db.GetService(); await db.Database.EnsureDeletedAsync(); + await GiveMeSomeTimeAsync(db); await db.GetService().CreateAsync(); await SetAndExecuteSqlAsync(migrator.GenerateScript(options: MigrationsSqlGenerationOptions.NoTransactions)); @@ -345,6 +355,7 @@ public virtual async Task Can_generate_one_up_and_down_script() var migrator = db.GetService(); await db.Database.EnsureDeletedAsync(); + await GiveMeSomeTimeAsync(db); await db.GetService().CreateAsync(); await ExecuteSqlAsync(migrator.GenerateScript( @@ -367,6 +378,7 @@ public virtual async Task Can_generate_up_and_down_script_using_names() var migrator = db.GetService(); await db.Database.EnsureDeletedAsync(); + await GiveMeSomeTimeAsync(db); await db.GetService().CreateAsync(); await ExecuteSqlAsync(migrator.GenerateScript( @@ -389,6 +401,7 @@ public virtual async Task Can_generate_idempotent_up_and_down_scripts() var migrator = db.GetService(); await db.Database.EnsureDeletedAsync(); + await GiveMeSomeTimeAsync(db); await db.GetService().CreateAsync(); await SetAndExecuteSqlAsync(migrator.GenerateScript( @@ -409,6 +422,7 @@ public virtual async Task Can_generate_idempotent_up_and_down_scripts_noTransact var migrator = db.GetService(); await db.Database.EnsureDeletedAsync(); + await GiveMeSomeTimeAsync(db); await db.GetService().CreateAsync(); await SetAndExecuteSqlAsync(migrator.GenerateScript( diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsInfrastructureSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsInfrastructureSqlServerTest.cs index c4b8eb03ac8..491a5e86ab6 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsInfrastructureSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsInfrastructureSqlServerTest.cs @@ -8,6 +8,7 @@ using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; using Microsoft.EntityFrameworkCore.TestModels.AspNetIdentity; +using static Microsoft.EntityFrameworkCore.Migrations.MigrationsInfrastructureFixtureBase; // ReSharper disable InconsistentNaming namespace Microsoft.EntityFrameworkCore.Migrations @@ -650,12 +651,136 @@ public override void Can_get_active_provider() Assert.Equal("Microsoft.EntityFrameworkCore.SqlServer", ActiveProvider); } + [ConditionalFact] + public void Throws_when_no_migrations() + { + using var context = new DbContext( + Fixture.TestStore.AddProviderOptions( + new DbContextOptionsBuilder().EnableServiceProviderCaching(false) + .ConfigureWarnings(e => e.Throw(RelationalEventId.MigrationsNotFound))).Options); + + context.Database.EnsureDeleted(); + GiveMeSomeTime(context); + + Assert.Equal( + CoreStrings.WarningAsErrorTemplate( + RelationalEventId.MigrationsNotFound.ToString(), + RelationalResources.LogNoMigrationsFound(new TestLogger()) + .GenerateMessage(typeof(DbContext).Assembly.GetName().Name), + "RelationalEventId.MigrationsNotFound"), + (Assert.Throws(context.Database.Migrate)).Message); + } + + [ConditionalFact] + public async Task Throws_when_no_migrations_async() + { + using var context = new DbContext( + Fixture.TestStore.AddProviderOptions( + new DbContextOptionsBuilder().EnableServiceProviderCaching(false) + .ConfigureWarnings(e => e.Throw(RelationalEventId.MigrationsNotFound))).Options); + + await context.Database.EnsureDeletedAsync(); + await GiveMeSomeTimeAsync(context); + + Assert.Equal( + CoreStrings.WarningAsErrorTemplate( + RelationalEventId.MigrationsNotFound.ToString(), + RelationalResources.LogNoMigrationsFound(new TestLogger()) + .GenerateMessage(typeof(DbContext).Assembly.GetName().Name), + "RelationalEventId.MigrationsNotFound"), + (await Assert.ThrowsAsync(() => context.Database.MigrateAsync())).Message); + } + + [ConditionalFact] + public void Throws_when_no_snapshot() + { + using var context = new MigrationsContext( + Fixture.TestStore.AddProviderOptions( + new DbContextOptionsBuilder().EnableServiceProviderCaching(false) + .ConfigureWarnings(e => e.Throw(RelationalEventId.ModelSnapshotNotFound))).Options); + + context.Database.EnsureDeleted(); + GiveMeSomeTime(context); + + Assert.Equal( + CoreStrings.WarningAsErrorTemplate( + RelationalEventId.ModelSnapshotNotFound.ToString(), + RelationalResources.LogNoModelSnapshotFound(new TestLogger()) + .GenerateMessage(typeof(MigrationsContext).Assembly.GetName().Name), + "RelationalEventId.ModelSnapshotNotFound"), + (Assert.Throws(context.Database.Migrate)).Message); + } + + [ConditionalFact] + public async Task Throws_when_no_snapshot_async() + { + using var context = new MigrationsContext( + Fixture.TestStore.AddProviderOptions( + new DbContextOptionsBuilder().EnableServiceProviderCaching(false) + .ConfigureWarnings(e => e.Throw(RelationalEventId.ModelSnapshotNotFound))).Options); + + await context.Database.EnsureDeletedAsync(); + await GiveMeSomeTimeAsync(context); + + Assert.Equal( + CoreStrings.WarningAsErrorTemplate( + RelationalEventId.ModelSnapshotNotFound.ToString(), + RelationalResources.LogNoModelSnapshotFound(new TestLogger()) + .GenerateMessage(typeof(MigrationsContext).Assembly.GetName().Name), + "RelationalEventId.ModelSnapshotNotFound"), + (await Assert.ThrowsAsync(() => context.Database.MigrateAsync())).Message); + } + + [ConditionalFact] + public void Throws_for_nondeterministic_HasData() + { + using var context = new BloggingContext( + Fixture.TestStore.AddProviderOptions( + new DbContextOptionsBuilder().EnableServiceProviderCaching(false)).Options, + randomData: true); + + context.Database.EnsureDeleted(); + GiveMeSomeTime(context); + + Assert.Equal( + CoreStrings.WarningAsErrorTemplate( + RelationalEventId.PendingModelChangesWarning.ToString(), + RelationalResources.LogNonDeterministicModel(new TestLogger()) + .GenerateMessage(nameof(BloggingContext)), + "RelationalEventId.PendingModelChangesWarning"), + (Assert.Throws(context.Database.Migrate)).Message); + } + + [ConditionalFact] + public async Task Throws_for_nondeterministic_HasData_async() + { + using var context = new BloggingContext( + Fixture.TestStore.AddProviderOptions( + new DbContextOptionsBuilder().EnableServiceProviderCaching(false)).Options, + randomData: true); + + await context.Database.EnsureDeletedAsync(); + await GiveMeSomeTimeAsync(context); + + Assert.Equal( + CoreStrings.WarningAsErrorTemplate( + RelationalEventId.PendingModelChangesWarning.ToString(), + RelationalResources.LogNonDeterministicModel(new TestLogger()) + .GenerateMessage(nameof(BloggingContext)), + "RelationalEventId.PendingModelChangesWarning"), + (await Assert.ThrowsAsync(() => context.Database.MigrateAsync())).Message); + } + [ConditionalFact] public void Throws_for_pending_model_changes() { using var context = new BloggingContext( Fixture.TestStore.AddProviderOptions( - new DbContextOptionsBuilder().EnableServiceProviderCaching(false)).Options); + new DbContextOptionsBuilder().EnableServiceProviderCaching(false)).Options, + randomData: false); + + context.Database.EnsureDeleted(); + GiveMeSomeTime(context); Assert.Equal( CoreStrings.WarningAsErrorTemplate( @@ -671,7 +796,11 @@ public async Task Throws_for_pending_model_changes_async() { using var context = new BloggingContext( Fixture.TestStore.AddProviderOptions( - new DbContextOptionsBuilder().EnableServiceProviderCaching(false)).Options); + new DbContextOptionsBuilder().EnableServiceProviderCaching(false)).Options, + randomData: false); + + await context.Database.EnsureDeletedAsync(); + await GiveMeSomeTimeAsync(context); Assert.Equal( CoreStrings.WarningAsErrorTemplate( @@ -925,7 +1054,7 @@ SELECT @result ignoreLineEndingDifferences: true); } - private class BloggingContext(DbContextOptions options) : DbContext(options) + private class BloggingContext(DbContextOptions options, bool? randomData = null) : DbContext(options) { // ReSharper disable once UnusedMember.Local public DbSet Blogs { get; set; } @@ -939,6 +1068,46 @@ public class Blog public string Name { get; set; } // ReSharper restore UnusedMember.Local } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + if (randomData != null) + { + modelBuilder.Entity().HasData( + new Blog { Id = randomData.Value ? (int)new Random().NextInt64(int.MaxValue) : 1, Name = "HalfADonkey" }); + } + } + } + + [DbContext(typeof(BloggingContext))] + partial class BloggingContextSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.EntityFrameworkCore.Migrations.MigrationsInfrastructureSqlServerTest+BloggingContext+Blog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Blogs"); + }); +#pragma warning restore 612, 618 + } } [DbContext(typeof(BloggingContext))]