diff --git a/src/EFCore.Design/Design/Internal/MigrationsOperations.cs b/src/EFCore.Design/Design/Internal/MigrationsOperations.cs index 294f1d29723..e597ec6afd5 100644 --- a/src/EFCore.Design/Design/Internal/MigrationsOperations.cs +++ b/src/EFCore.Design/Design/Internal/MigrationsOperations.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Migrations.Internal; namespace Microsoft.EntityFrameworkCore.Design.Internal; @@ -242,6 +243,26 @@ public virtual MigrationFiles RemoveMigration( return files; } + /// + /// 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 HasPendingModelChanges(string? contextType) + { + using var context = _contextOperations.CreateContext(contextType); + + var hasPendingModelChanges = context.Database.HasPendingModelChanges(); + + if (hasPendingModelChanges) + { + throw new OperationException(DesignStrings.PendingModelChanges); + } + + _reporter.WriteInformation(DesignStrings.NoPendingModelChanges); + } + private static void EnsureServices(IServiceProvider services) { var migrator = services.GetService(); diff --git a/src/EFCore.Design/Design/OperationExecutor.cs b/src/EFCore.Design/Design/OperationExecutor.cs index d08561fbfc4..046be5f92a5 100644 --- a/src/EFCore.Design/Design/OperationExecutor.cs +++ b/src/EFCore.Design/Design/OperationExecutor.cs @@ -683,6 +683,40 @@ public ScriptDbContext( private string ScriptDbContextImpl(string? contextType) => ContextOperations.ScriptDbContext(contextType); + /// + /// Represents an operation to check if there are any pending migrations. + /// + public class HasPendingModelChanges : OperationBase + { + /// + /// Initializes a new instance of the class. + /// + /// + /// The arguments supported by are: + /// contextType--The to use. + /// + /// The operation executor. + /// The . + /// The operation arguments. + public HasPendingModelChanges( + OperationExecutor executor, + IOperationResultHandler resultHandler, + IDictionary args) + : base(resultHandler) + { + Check.NotNull(executor, nameof(executor)); + Check.NotNull(args, nameof(args)); + + var contextType = (string?)args["contextType"]; + + Execute(() => executor.HasPendingModelChangesImpl(contextType)); + } + } + + private void HasPendingModelChangesImpl(string? contextType) + => MigrationsOperations.HasPendingModelChanges(contextType); + + /// /// Represents an operation. /// diff --git a/src/EFCore.Design/Properties/DesignStrings.Designer.cs b/src/EFCore.Design/Properties/DesignStrings.Designer.cs index 815834efe07..e5cd0177402 100644 --- a/src/EFCore.Design/Properties/DesignStrings.Designer.cs +++ b/src/EFCore.Design/Properties/DesignStrings.Designer.cs @@ -551,6 +551,12 @@ public static string NonRelationalProvider(object? provider) GetString("NonRelationalProvider", nameof(provider)), provider); + /// + /// No changes have been made to the model since the last migration. + /// + public static string NoPendingModelChanges + => GetString("NoPendingModelChanges"); + /// /// No referenced design-time services were found. /// @@ -591,6 +597,12 @@ public static string NotExistDatabase(object? name) GetString("NotExistDatabase", nameof(name)), name); + /// + /// Changes have been made to the model since the last migration. Add a new migration. + /// + public static string PendingModelChanges + => GetString("PendingModelChanges"); + /// /// Prefix output with level. /// diff --git a/src/EFCore.Design/Properties/DesignStrings.resx b/src/EFCore.Design/Properties/DesignStrings.resx index 5da17b71cb6..12c829d9a5b 100644 --- a/src/EFCore.Design/Properties/DesignStrings.resx +++ b/src/EFCore.Design/Properties/DesignStrings.resx @@ -332,6 +332,9 @@ Change your target project to the migrations project by using the Package Manage The provider '{provider}' is not a Relational provider and therefore cannot be used with Migrations. + + No changes have been made to the model since the last migration. + No referenced design-time services were found. @@ -350,6 +353,9 @@ Change your target project to the migrations project by using the Package Manage Database '{name}' did not exist, no action was taken. + + Changes have been made to the model since the last migration. Add a new migration. + Prefix output with level. diff --git a/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs index dd2cc1cdb8f..51d2701fbb6 100644 --- a/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs @@ -952,6 +952,39 @@ public static bool IsRelational(this DatabaseFacade databaseFacade) => ((IDatabaseFacadeDependenciesAccessor)databaseFacade) .Context.GetService().Extensions.OfType().Any(); + /// + /// Returns if the model has pending changes to be applied. + /// + /// The facade from . + /// + /// if the database model has pending changes + /// and a new migration has to be added. + /// + 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()); + } + private static IRelationalDatabaseFacadeDependencies GetFacadeDependencies(DatabaseFacade databaseFacade) { var dependencies = ((IDatabaseFacadeDependenciesAccessor)databaseFacade).Dependencies; diff --git a/src/dotnet-ef/Properties/Resources.Designer.cs b/src/dotnet-ef/Properties/Resources.Designer.cs index 25392da7117..6721009a20e 100644 --- a/src/dotnet-ef/Properties/Resources.Designer.cs +++ b/src/dotnet-ef/Properties/Resources.Designer.cs @@ -241,6 +241,12 @@ public static string MigrationsBundleRuntimeDescription public static string MigrationsDescription => GetString("MigrationsDescription"); + /// + /// Checks if any changes have been made to the model since the last migration. + /// + public static string MigrationsHasPendingModelChangesDescription + => GetString("MigrationsHasPendingModelChangesDescription"); + /// /// Lists available migrations. /// @@ -505,3 +511,4 @@ private static string GetString(string name, params string[] formatterNames) } } } + diff --git a/src/dotnet-ef/Properties/Resources.resx b/src/dotnet-ef/Properties/Resources.resx index fd52fa85898..8bfe4b57cab 100644 --- a/src/dotnet-ef/Properties/Resources.resx +++ b/src/dotnet-ef/Properties/Resources.resx @@ -228,6 +228,9 @@ Commands to manage migrations. + + Checks if any changes have been made to the model since the last migration. + Lists available migrations. @@ -345,4 +348,4 @@ Writing '{file}'... - + \ No newline at end of file diff --git a/src/ef/Commands/MigrationsCommand.cs b/src/ef/Commands/MigrationsCommand.cs index d6f44f09129..674b8c3e549 100644 --- a/src/ef/Commands/MigrationsCommand.cs +++ b/src/ef/Commands/MigrationsCommand.cs @@ -14,6 +14,7 @@ public override void Configure(CommandLineApplication command) command.Command("add", new MigrationsAddCommand().Configure); command.Command("bundle", new MigrationsBundleCommand().Configure); + command.Command("has-pending-model-changes", new MigrationsHasPendingModelChangesCommand().Configure); command.Command("list", new MigrationsListCommand().Configure); command.Command("remove", new MigrationsRemoveCommand().Configure); command.Command("script", new MigrationsScriptCommand().Configure); diff --git a/src/ef/Commands/MigrationsHasPendingModelChangesCommand.Configure.cs b/src/ef/Commands/MigrationsHasPendingModelChangesCommand.Configure.cs new file mode 100644 index 00000000000..d5b025d320a --- /dev/null +++ b/src/ef/Commands/MigrationsHasPendingModelChangesCommand.Configure.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.DotNet.Cli.CommandLine; +using Microsoft.EntityFrameworkCore.Tools.Properties; + +namespace Microsoft.EntityFrameworkCore.Tools.Commands; + +internal partial class MigrationsHasPendingModelChangesCommand : ContextCommandBase +{ + public override void Configure(CommandLineApplication command) + { + command.Description = Resources.MigrationsHasPendingModelChangesDescription; + + base.Configure(command); + } +} diff --git a/src/ef/Commands/MigrationsHasPendingModelChangesCommand.cs b/src/ef/Commands/MigrationsHasPendingModelChangesCommand.cs new file mode 100644 index 00000000000..bdbd2db8b6a --- /dev/null +++ b/src/ef/Commands/MigrationsHasPendingModelChangesCommand.cs @@ -0,0 +1,16 @@ +// 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.Tools.Commands; + +internal partial class MigrationsHasPendingModelChangesCommand +{ + protected override int Execute(string[] args) + { + using var executor = CreateExecutor(args); + + executor.HasPendingModelChanges(Context!.Value()); + + return base.Execute(args); + } +} diff --git a/src/ef/IOperationExecutor.cs b/src/ef/IOperationExecutor.cs index 90f3e322e9d..e77caaab7c7 100644 --- a/src/ef/IOperationExecutor.cs +++ b/src/ef/IOperationExecutor.cs @@ -37,4 +37,5 @@ IDictionary ScaffoldContext( string ScriptMigration(string? fromMigration, string? toMigration, bool idempotent, bool noTransactions, string? contextType); string ScriptDbContext(string? contextType); + void HasPendingModelChanges(string? contextType); } diff --git a/src/ef/OperationExecutorBase.cs b/src/ef/OperationExecutorBase.cs index 73b1e74d6b6..8e578bebc8f 100644 --- a/src/ef/OperationExecutorBase.cs +++ b/src/ef/OperationExecutorBase.cs @@ -206,4 +206,9 @@ public string ScriptDbContext(string? contextType) => InvokeOperation( "ScriptDbContext", new Dictionary { ["contextType"] = contextType }); + + public void HasPendingModelChanges(string? contextType) + => InvokeOperation( + "HasPendingModelChanges", + new Dictionary { ["contextType"] = contextType }); } diff --git a/src/ef/Properties/Resources.Designer.cs b/src/ef/Properties/Resources.Designer.cs index b96dc343edf..9ae329692c9 100644 --- a/src/ef/Properties/Resources.Designer.cs +++ b/src/ef/Properties/Resources.Designer.cs @@ -317,6 +317,12 @@ public static string MigrationsBundleRuntimeDescription public static string MigrationsDescription => GetString("MigrationsDescription"); + /// + /// Checks if any changes have been made to the model since the last migration. + /// + public static string MigrationsHasPendingModelChangesDescription + => GetString("MigrationsHasPendingModelChangesDescription"); + /// /// Lists available migrations. /// @@ -647,3 +653,4 @@ private static string GetString(string name, params string[] formatterNames) } } } + diff --git a/src/ef/Properties/Resources.resx b/src/ef/Properties/Resources.resx index 11ef663ef9e..ec632fe97ef 100644 --- a/src/ef/Properties/Resources.resx +++ b/src/ef/Properties/Resources.resx @@ -258,6 +258,9 @@ Commands to manage migrations. + + Checks if any changes have been made to the model since the last migration. + Lists available migrations. @@ -402,4 +405,4 @@ Writing '{file}'... - + \ No newline at end of file diff --git a/test/EFCore.Relational.Tests/RelationalDatabaseFacadeExtensionsTest.cs b/test/EFCore.Relational.Tests/RelationalDatabaseFacadeExtensionsTest.cs index 228fc1c45c2..5f1a269d466 100644 --- a/test/EFCore.Relational.Tests/RelationalDatabaseFacadeExtensionsTest.cs +++ b/test/EFCore.Relational.Tests/RelationalDatabaseFacadeExtensionsTest.cs @@ -220,7 +220,7 @@ public void GetMigrations_works() private class FakeIMigrationsAssembly : IMigrationsAssembly { public IReadOnlyDictionary Migrations { get; set; } - public ModelSnapshot ModelSnapshot { get; } + public ModelSnapshot ModelSnapshot { get; set; } public Assembly Assembly { get; } public string FindMigrationId(string nameOrId) @@ -249,6 +249,86 @@ public async Task GetAppliedMigrations_works(bool async) : context.Database.GetAppliedMigrations()); } + [ConditionalFact] + public void HasPendingModelChanges_has_no_migrations_has_dbcontext_changes_returns_true() + { + // This project has NO existing migrations right now but does have information in the DbContext + var migrationsAssembly = new FakeIMigrationsAssembly + { + ModelSnapshot = null, + Migrations = new Dictionary(), + }; + + var testHelper = FakeRelationalTestHelpers.Instance; + + var contextOptions = testHelper.CreateOptions( + testHelper.CreateServiceProvider(new ServiceCollection().AddSingleton(migrationsAssembly))); + + var testContext = new TestDbContext(contextOptions); + + Assert.True(testContext.Database.HasPendingModelChanges()); + } + + [ConditionalFact] + public void HasPendingModelChanges_has_migrations_and_no_new_context_changes_returns_false() + { + var fakeModelSnapshot = new FakeModelSnapshot(builder => + { + builder.Entity("Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensionsTests.TestDbContext.Simple", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("default_int_mapping"); + + b.HasKey("Id"); + + b.ToTable("Simples"); + }); + }); + var migrationsAssembly = new FakeIMigrationsAssembly + { + ModelSnapshot = fakeModelSnapshot, + Migrations = new Dictionary(), + }; + + var testHelper = FakeRelationalTestHelpers.Instance; + + var contextOptions = testHelper.CreateOptions( + testHelper.CreateServiceProvider(new ServiceCollection().AddSingleton(migrationsAssembly))); + + var testContext = new TestDbContext(contextOptions); + + Assert.False(testContext.Database.HasPendingModelChanges()); + } + + private class TestDbContext : DbContext + { + public TestDbContext(DbContextOptions options) : base(options) + { } + public DbSet Simples { get; set; } + + public class Simple + { + public int Id { get; set; } + } + + } + + private class FakeModelSnapshot : ModelSnapshot + { + private readonly Action _buildModel; + + public FakeModelSnapshot(Action buildModel) + { + _buildModel = buildModel; + } + protected override void BuildModel(ModelBuilder modelBuilder) + { + _buildModel(modelBuilder); + } + } + + private class FakeHistoryRepository : IHistoryRepository { public List AppliedMigrations { get; set; }