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; }