Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
633165e
Add runtime migration creation and application
thromel Dec 23, 2025
ed69364
Add CLI functional tests for CreateAndApplyMigration operation
thromel Dec 23, 2025
872ffdc
Add SQLite integration tests for runtime migrations and fix service w…
thromel Dec 23, 2025
2609bea
Add SQL Server integration tests for runtime migrations
thromel Dec 23, 2025
52999ad
Fix CI build: Add DatabaseUpdateAddDescription to dotnet-ef Resources
thromel Dec 23, 2025
62c8e9e
Fix lazy initialization of DynamicMigrationsAssembly
thromel Dec 23, 2025
4a84f33
Trigger CI re-run
thromel Dec 24, 2025
31ded81
Fix dry run test: remove query to non-existent database
thromel Dec 25, 2025
9164772
Fix IsCI detection for Helix test environment
thromel Dec 25, 2025
84eec3f
Add comprehensive tests for runtime migrations
thromel Dec 25, 2025
4316282
Add RevertMigration support for runtime migrations
thromel Dec 25, 2025
8e7cad7
Add comprehensive tests for runtime migrations
thromel Dec 25, 2025
50dde25
Trigger CI re-run
thromel Dec 25, 2025
234868e
Add migration name validation to CreateAndApplyMigration
thromel Dec 25, 2025
b78e506
Address PR review feedback for runtime migrations
thromel Dec 31, 2025
848777b
Add missing using directive for Resources in DatabaseUpdateCommand
thromel Dec 31, 2025
9303834
Address PR review feedback: remove IRuntimeMigrationService
thromel Jan 3, 2026
953c86c
Add runtime migration tests to DesignTimeTestBase
thromel Jan 3, 2026
9c8d771
Add missing runtime migration tests to DesignTimeTestBase
thromel Jan 3, 2026
b6a523b
Add comprehensive tests for runtime migrations feature
thromel Jan 3, 2026
6f7ae79
Remove unrelated AddMigration tests from MigrationsOperationsTest
thromel Jan 3, 2026
f17dddc
Fix CI test failures for runtime migrations
thromel Jan 4, 2026
ac1deb1
Add rigorous schema verification tests for runtime migrations
thromel Jan 5, 2026
6b0a93d
Add RuntimeMigrationSqlServerTest for CI compliance
thromel Jan 5, 2026
ad289d2
Fix SQLite file locking issue on Windows
thromel Jan 6, 2026
13f5377
Address PR review comments for runtime migrations
thromel Jan 6, 2026
3b99d66
Address remaining PR review comments for runtime migrations
thromel Jan 6, 2026
4c48e09
Move Validate() to Configure.cs for dotnet-ef inclusion
thromel Jan 6, 2026
f0163c7
Fix SQL Server runtime migration test cleanup
thromel Jan 6, 2026
01bfe2f
Fix connection state handling in CleanDatabase
thromel Jan 6, 2026
355c7b7
Fix critical issues in runtime migrations
thromel Jan 6, 2026
6e5e59f
Don't delete snapshot file on AddAndApplyMigration failure
thromel Jan 6, 2026
cd324fd
Address PR review comments for runtime migrations
thromel Jan 8, 2026
5bf8a0b
Fix CI test failures
thromel Jan 8, 2026
8129933
Fix remaining First() calls in Migration_preserves_existing_data test
thromel Jan 8, 2026
e21555c
Fix connection state in Migration_down_reverses_up test
thromel Jan 8, 2026
e9b3d54
Remove lock from AddMigrations per reviewer feedback
thromel Jan 9, 2026
a5e5331
Address reviewer feedback on test patterns
thromel Jan 9, 2026
e7b6329
Address remaining reviewer feedback
thromel Jan 9, 2026
72a3c0f
Add createTables parameter to TestStore.CleanAsync for runtime migrat…
thromel Jan 11, 2026
a5b2659
Address Copilot review feedback
thromel Jan 11, 2026
9a62a7b
Use existing MissingConditionalOption resource for --json validation
thromel Jan 11, 2026
1d82f83
Fix GnomeContext to use valid SQLite connection string
thromel Jan 11, 2026
6d64bb1
Fix GnomeContext to use externally opened SQLite connection
thromel Jan 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.Design.Internal;
using Microsoft.EntityFrameworkCore.Migrations.Design;
using Microsoft.EntityFrameworkCore.Migrations.Design.Internal;
using Microsoft.EntityFrameworkCore.Migrations.Internal;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Scaffolding.Internal;
Expand Down Expand Up @@ -65,7 +67,8 @@ public static IServiceCollection AddEntityFrameworkDesignTimeServices(
.TryAddScoped<IReverseEngineerScaffolder, ReverseEngineerScaffolder>()
.TryAddScoped<MigrationsScaffolderDependencies, MigrationsScaffolderDependencies>()
.TryAddScoped<IMigrationsScaffolder, MigrationsScaffolder>()
.TryAddScoped<ISnapshotModelProcessor, SnapshotModelProcessor>());
.TryAddScoped<ISnapshotModelProcessor, SnapshotModelProcessor>()
.TryAddSingleton<IMigrationCompiler, CSharpMigrationCompiler>());

var loggerFactory = new LoggerFactory(
[new OperationLoggerProvider(reporter)], new LoggerFilterOptions { MinLevel = LogLevel.Debug });
Expand Down Expand Up @@ -100,6 +103,7 @@ public static IServiceCollection AddDbContextDesignTimeServices(
.TryAdd(_ => context.GetService<IMigrationsModelDiffer>())
.TryAdd(_ => context.GetService<IMigrator>())
.TryAdd(_ => context.GetService<IDesignTimeModel>().Model);

return services;
}
}
135 changes: 110 additions & 25 deletions src/EFCore.Design/Design/Internal/MigrationsOperations.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Migrations.Design;
using Microsoft.EntityFrameworkCore.Migrations.Design.Internal;

namespace Microsoft.EntityFrameworkCore.Design.Internal;

Expand Down Expand Up @@ -70,31 +73,8 @@ public virtual MigrationFiles AddMigration(
string? @namespace,
bool dryRun)
{
var invalidPathChars = Path.GetInvalidFileNameChars();
if (name.Any(c => invalidPathChars.Contains(c)))
{
throw new OperationException(
DesignStrings.BadMigrationName(name, string.Join("','", invalidPathChars)));
}

if (outputDir != null)
{
outputDir = Path.GetFullPath(Path.Combine(_projectDir, outputDir));
}

var subNamespace = SubnamespaceFromOutputPath(outputDir);

using var context = _contextOperations.CreateContext(contextType);
var contextClassName = context.GetType().Name;
if (string.Equals(name, contextClassName, StringComparison.Ordinal))
{
throw new OperationException(
DesignStrings.ConflictingContextAndMigrationName(name));
}

var services = _servicesBuilder.Build(context);
EnsureServices(services);
EnsureMigrationsAssembly(services);
var (resolvedOutputDir, subNamespace, services) = PrepareForMigration(name, outputDir, context);

using var scope = services.CreateScope();
var scaffolder = scope.ServiceProvider.GetRequiredService<IMigrationsScaffolder>();
Expand All @@ -103,7 +83,7 @@ public virtual MigrationFiles AddMigration(
// TODO: Honor _nullable (issue #18950)
? scaffolder.ScaffoldMigration(name, _rootNamespace ?? string.Empty, subNamespace, _language, dryRun)
: scaffolder.ScaffoldMigration(name, null, @namespace, _language, dryRun);
return scaffolder.Save(_projectDir, migration, outputDir, dryRun);
return scaffolder.Save(_projectDir, migration, resolvedOutputDir, dryRun);
}

// if outputDir is a subfolder of projectDir, then use each subfolder as a sub-namespace
Expand Down Expand Up @@ -272,6 +252,111 @@ public virtual void HasPendingModelChanges(string? contextType)
_reporter.WriteInformation(DesignStrings.NoPendingModelChanges);
}

/// <summary>
/// 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.
/// </summary>
[RequiresDynamicCode("Runtime migration compilation requires dynamic code generation.")]
public virtual MigrationFiles AddAndApplyMigration(
string name,
string? outputDir,
string? contextType,
string? @namespace,
string? connectionString)
{
using var context = _contextOperations.CreateContext(contextType);
var (resolvedOutputDir, subNamespace, services) = PrepareForMigration(name, outputDir, context);

if (connectionString != null)
{
context.Database.SetConnectionString(connectionString);
}

using var scope = services.CreateScope();
var migrator = scope.ServiceProvider.GetRequiredService<IMigrator>();
var scaffolder = scope.ServiceProvider.GetRequiredService<IMigrationsScaffolder>();
var compiler = scope.ServiceProvider.GetRequiredService<IMigrationCompiler>();
var migrationsAssembly = scope.ServiceProvider.GetRequiredService<IMigrationsAssembly>();

if (!migrator.HasPendingModelChanges())
{
_reporter.WriteInformation(DesignStrings.NoPendingModelChanges);
migrator.Migrate(null);
_reporter.WriteInformation(DesignStrings.Done);
Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When there are no pending model changes, the method returns an empty MigrationFiles object (line 298). However, the calling code in DatabaseUpdateCommand will attempt to output JSON from this empty object when --json is specified. This will result in null values for all file paths in the JSON output, which may be confusing to users. Consider returning null or a special indicator, or documenting this behavior clearly.

Suggested change
_reporter.WriteInformation(DesignStrings.Done);
_reporter.WriteInformation(DesignStrings.Done);
// Note: Returning an empty MigrationFiles instance here indicates that no migration was created.
// DatabaseUpdateCommand will serialize this instance when --json is specified, which results in null
// values for all file path properties in the JSON output.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit a5b2659 - added a comment documenting that an empty MigrationFiles is returned when there are no pending changes, and that JSON serialization will produce null values for all file paths.

// Return empty MigrationFiles to indicate no migration was created.
// When serialized to JSON (with --json), all file path properties will be null.
return new MigrationFiles();
}

_reporter.WriteInformation(DesignStrings.CreatingAndApplyingMigration(name));

var migration =
string.IsNullOrEmpty(@namespace)
? scaffolder.ScaffoldMigration(name, _rootNamespace ?? string.Empty, subNamespace, _language, dryRun: true)
: scaffolder.ScaffoldMigration(name, null, @namespace, _language, dryRun: true);

MigrationFiles? files = null;
try
{
files = scaffolder.Save(_projectDir, migration, resolvedOutputDir, dryRun: false);

var compiledAssembly = compiler.CompileMigration(migration, context.GetType());
migrationsAssembly.AddMigrations(compiledAssembly);

migrator.Migrate(migration.MigrationId);

_reporter.WriteInformation(DesignStrings.MigrationCreatedAndApplied(migration.MigrationId));

return files;
}
catch (Exception ex)
{
throw new OperationException(
DesignStrings.AddAndApplyMigrationFailed(name, ex.Message), ex);
}
}
Comment on lines +300 to +319
Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling in AddAndApplyMigration does not clean up the saved migration files on failure, even though the PR description mentions "Error handling with cleanup" as a robustness feature. If compilation or migration application fails after line 311, the files remain on disk which can cause issues. The catch block should delete the files that were successfully saved to prevent orphaned migrations.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an intentional design decision. Files are kept on disk after failure to aid debugging - developers can inspect the generated migration code to understand what went wrong. Deleting files on failure would make troubleshooting more difficult. This follows the pattern of dotnet ef migrations add which also keeps files on failure.


/// <summary>
/// Prepares common resources for migration operations.
/// </summary>
private (string? outputDir, string? subNamespace, IServiceProvider services)
PrepareForMigration(string name, string? outputDir, DbContext context)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new OperationException(DesignStrings.MigrationNameRequired);
}
Comment on lines +327 to +330
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, then remove this check since it's already checked before this call


var invalidPathChars = Path.GetInvalidFileNameChars();
if (name.Any(c => invalidPathChars.Contains(c)))
{
throw new OperationException(
DesignStrings.BadMigrationName(name, string.Join("','", invalidPathChars)));
}

if (outputDir != null)
{
outputDir = Path.GetFullPath(Path.Combine(_projectDir, outputDir));
}

var subNamespace = SubnamespaceFromOutputPath(outputDir);

var contextClassName = context.GetType().Name;
if (string.Equals(name, contextClassName, StringComparison.Ordinal))
{
throw new OperationException(
DesignStrings.ConflictingContextAndMigrationName(name));
}

var services = _servicesBuilder.Build(context);
EnsureServices(services);
EnsureMigrationsAssembly(services);

return (outputDir, subNamespace, services);
}

private static void EnsureServices(IServiceProvider services)
{
var migrator = services.GetService<IMigrator>();
Expand Down
58 changes: 58 additions & 0 deletions src/EFCore.Design/Design/OperationExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,64 @@ private void UpdateDatabaseImpl(
string? contextType)
=> MigrationsOperations.UpdateDatabase(targetMigration, connectionString, contextType);

/// <summary>
/// Represents an operation to add and apply a new migration in one step.
/// </summary>
public class AddAndApplyMigration : OperationBase
{
/// <summary>
/// Initializes a new instance of the <see cref="AddAndApplyMigration" /> class.
/// </summary>
/// <remarks>
/// <para>The arguments supported by <paramref name="args" /> are:</para>
/// <para><c>name</c>--The name of the migration.</para>
/// <para><c>outputDir</c>--The directory to put files in. Paths are relative to the project directory.</para>
/// <para><c>contextType</c>--The <see cref="DbContext" /> to use.</para>
/// <para><c>namespace</c>--The namespace to use for the migration.</para>
/// <para>
/// <c>connectionString</c>--The connection string to the database. Defaults to the one specified in
/// <see cref="O:Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions.AddDbContext" /> or
/// <see cref="DbContext.OnConfiguring" />.
/// </para>
/// </remarks>
/// <param name="executor">The operation executor.</param>
/// <param name="resultHandler">The <see cref="IOperationResultHandler" />.</param>
/// <param name="args">The operation arguments.</param>
public AddAndApplyMigration(
OperationExecutor executor,
IOperationResultHandler resultHandler,
IDictionary args)
: base(resultHandler)
{
Check.NotNull(executor);
Check.NotNull(args);

var name = (string)args["name"]!;
var outputDir = (string?)args["outputDir"];
var contextType = (string?)args["contextType"];
var @namespace = (string?)args["namespace"];
var connectionString = (string?)args["connectionString"];

Execute(() => executor.AddAndApplyMigrationImpl(name, outputDir, contextType, @namespace, connectionString));
}
}

private IDictionary AddAndApplyMigrationImpl(
string name,
string? outputDir,
string? contextType,
string? @namespace,
string? connectionString)
{
var files = MigrationsOperations.AddAndApplyMigration(name, outputDir, contextType, @namespace, connectionString);
return new Hashtable
{
["MigrationFile"] = files.MigrationFile,
["MetadataFile"] = files.MetadataFile,
["SnapshotFile"] = files.SnapshotFile
};
}

/// <summary>
/// Represents an operation to generate a SQL script from migrations.
/// </summary>
Expand Down
Loading
Loading