-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Add runtime migration creation and application #37415
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
a15611a to
9a35a9b
Compare
62018f1 to
5c41f2f
Compare
Note on SQL Server Integration TestsThe Why these tests are skipped in CI:
Test coverage is still maintained:
This is consistent with how other complex migration infrastructure tests handle CI limitations. |
test/EFCore.SqlServer.FunctionalTests/Migrations/RuntimeMigrationSqlServerTest.cs
Outdated
Show resolved
Hide resolved
src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs
Outdated
Show resolved
Hide resolved
src/EFCore.Design/Migrations/Design/IDynamicMigrationsAssembly.cs
Outdated
Show resolved
Hide resolved
src/EFCore.Design/Migrations/Design/IDynamicMigrationsAssembly.cs
Outdated
Show resolved
Hide resolved
|
Thank you for the thorough review @AndriySvyryd! I've addressed all your feedback:
All tests pass locally (SQLite: 26 tests, SQL Server: 7 tests, CSharpMigrationCompiler: 4 tests, MigrationsOperations: 2 tests). |
src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs
Outdated
Show resolved
Hide resolved
This comment was marked as outdated.
This comment was marked as outdated.
test/EFCore.Design.Tests/Design/Internal/MigrationsOperationsTest.cs
Outdated
Show resolved
Hide resolved
Properly restore connection state after cleaning database tables. The connection is closed after cleanup only if it wasn't already open before, preventing "connection was not closed" errors in tests that expect to open the connection themselves.
1. AddAndApplyMigration error handling: - Add try-catch around scaffold-compile-apply chain - Clean up saved files on failure to prevent orphans - Add TryDeleteFile helper for best-effort cleanup - Add AddAndApplyMigrationFailed resource string 2. Context disposal in PrepareForMigration: - Wrap context usage in try-catch - Dispose context if validation or service building fails - Prevents context leaks on validation exceptions 3. Thread safety in MigrationsAssembly: - Add lock protection around _additionalAssemblies, _migrations, and _modelSnapshot - Protect Migrations property getter, ModelSnapshot property getter, and AddMigrations method - Prevents race conditions in multi-threaded scenarios
The snapshot file may have overwritten an existing snapshot during Save(). Deleting it on failure would leave the project without a snapshot, breaking future migrations. Only delete migration and metadata files which are always newly created.
- Remove file deletion on failure (keep files for debugging) - Inline validation methods into PrepareForMigration - Remove DisableParallelization from test classes - Refactor tests to use SharedStoreFixtureBase pattern - Use NonCapturingLazyInitializer for MigrationsAssembly.Migrations - Convert to using declarations to reduce nesting - Make CleanDatabase virtual for provider overrides - Fix thread safety with lock-based ModelSnapshot caching
f3a91b0 to
cd324fd
Compare
This comment was marked as off-topic.
This comment was marked as off-topic.
- Move migration name validation before context creation in AddMigration and AddAndApplyMigration to ensure proper error messages when name is empty - Use Single() instead of First() in Migration_preserves_existing_data test to avoid FirstWithoutOrderByAndFilterWarning
Replace First() with Single() to avoid FirstWithoutOrderByAndFilterWarning
Close connection before migrator.Migrate("0") call and reopen after,
since the migrator manages its own connection state internally.
I agree that it's very unlikely, keep NonCapturingLazyInitializer and remove the lock from AddMigrations |
test/EFCore.Relational.Specification.Tests/RuntimeMigrationTestBase.cs
Outdated
Show resolved
Hide resolved
| public void Can_scaffold_migration() | ||
| { | ||
| using var context = CreateContext(); | ||
| CleanDatabase(context); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should be able to call Fixture.ReseedAsync() instead.
You can also call it from IAsyncLifetime.InitializeAsync to avoid repeating the call in each method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I forgot that ReseedAsync also creates tables. Change InitializeAsync to
{
using var context = CreateContext();
return Fixture.TestStore.CleanAsync(context, createTables: false);
}Then add the bool createTables = true parameter to TestStore.CleanAsync, SqlServerDatabaseFacadeExtensions.EnsureClean, SqliteDatabaseFacadeTestExtensions.EnsureClean and RelationalDatabaseCleaner.Clean
| => "RuntimeMigration"; | ||
|
|
||
| protected override bool UsePooling | ||
| => false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you have to specify false here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Re: UsePooling => false in RuntimeMigrationFixtureBase
Connection pooling is disabled because these tests dynamically alter the database schema (creating/dropping tables, applying/reverting migrations). With pooling enabled, pooled connections might hold stale schema information or cached state that conflicts with the schema changes made during migration operations.
However, if you think pooling should work fine for these tests, I can remove the override and test it.
This property controls DbContext pooling, not connection pooling, so leaving the default value (true) should be fine for these tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see now, if you use context pooling then the tests will get a context instance that still contains the runtime migrations assembly from the previous test, making it fail in most cases. So in this case, disabling DbContext pooling is the right choice
Per review: race condition is very unlikely since AddMigrations is only called during design-time operations, not concurrently with Migrations access.
|
Re: Connection pooling is disabled because these tests dynamically alter the database schema (creating/dropping tables, applying/reverting migrations). With pooling enabled, pooled connections might hold stale schema information or cached state that conflicts with the schema changes made during migration operations. However, if you think pooling should work fine for these tests, I can remove the override and test it. |
- Implement IAsyncLifetime and call Fixture.ReseedAsync() in InitializeAsync instead of manually calling CleanDatabase in each test - Use context.Database.OpenConnection/CloseConnection instead of direct connection.Open/Close calls - Move database cleanup logic to fixture's CleanAsync override - Add GetTableNamesAsync to fixtures for async cleanup
This comment was marked as resolved.
This comment was marked as resolved.
- Simplify CSharpMigrationCompiler.GetMetadataReferences to use cached references plus context assembly, removing explicit Assembly.Load calls - Remove duplicate name validation from AddMigration/AddAndApplyMigration since PrepareForMigration already validates - Remove UsePooling override (controls DbContext pooling, not connection pooling)
|
Addressed the remaining unresolved comments:
Commit: e7b6329 |
| if (string.IsNullOrWhiteSpace(name)) | ||
| { | ||
| throw new OperationException(DesignStrings.MigrationNameRequired); | ||
| } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements runtime migration creation and application to support scenarios like .NET Aspire and containerized applications where recompiling is not possible. It extends the existing dotnet ef database update and Update-Database commands with a new --add option that scaffolds, compiles (using Roslyn), registers, and applies a migration in one atomic operation.
Changes:
- Adds
IMigrationCompilerinterface andCSharpMigrationCompilerimplementation for runtime Roslyn-based compilation of scaffolded migrations - Extends
IMigrationsAssemblywithAddMigrations(Assembly)method to register dynamically compiled migrations - Adds
AddAndApplyMigrationoperation toMigrationsOperationsthat orchestrates the scaffold → compile → register → apply workflow - Updates CLI and PowerShell commands to support
--add,--output-dir,--namespace, and--jsonoptions with appropriate validation
Reviewed changes
Copilot reviewed 26 out of 29 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
src/EFCore.Design/Migrations/Design/IMigrationCompiler.cs |
New internal interface for runtime migration compilation |
src/EFCore.Design/Migrations/Design/CSharpMigrationCompiler.cs |
Roslyn-based implementation with assembly reference caching |
src/EFCore.Relational/Migrations/IMigrationsAssembly.cs |
Adds AddMigrations method to public interface |
src/EFCore.Relational/Migrations/Internal/MigrationsAssembly.cs |
Implements dynamic migration registration with thread-safety concerns |
src/EFCore.Design/Design/Internal/MigrationsOperations.cs |
Core AddAndApplyMigration operation with error handling |
src/EFCore.Design/Design/OperationExecutor.cs |
Operation executor for AddAndApplyMigration command |
src/ef/Commands/DatabaseUpdateCommand*.cs |
CLI command extensions with validation logic |
src/EFCore.Tools/tools/EntityFrameworkCore.psm1 |
PowerShell Update-Database function enhancements |
test/EFCore.Relational.Specification.Tests/RuntimeMigrationTestBase.cs |
Comprehensive test base with 20+ test scenarios |
test/EFCore.*.FunctionalTests/RuntimeMigration*Test.cs |
Provider-specific test implementations |
Resource files (*.resx, *.Designer.cs) |
New localized strings for errors and messages |
Files not reviewed (3)
- src/EFCore.Design/Properties/DesignStrings.Designer.cs: Language not supported
- src/dotnet-ef/Properties/Resources.Designer.cs: Language not supported
- src/ef/Properties/Resources.Designer.cs: Language not supported
| 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); | ||
| } | ||
| } |
Copilot
AI
Jan 11, 2026
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| if (_namespace!.HasValue()) | ||
| { | ||
| throw new CommandException(Resources.NamespaceRequiresAdd); | ||
| } |
Copilot
AI
Jan 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The validation logic does not check if the --json option is used without --add. According to the PR description, the --json option requires --add to be specified, similar to --output-dir and --namespace. This validation should be added to ensure consistent behavior.
| } | |
| } | |
| if (_json!.HasValue()) | |
| { | |
| throw new CommandException(Resources.JsonRequiresAdd); | |
| } |
There was a problem hiding this comment.
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 validation that --json requires --add to be specified, consistent with --output-dir and --namespace.
| .TryAdd(_ => context.GetService<IMigrationsModelDiffer>()) | ||
| .TryAdd(_ => context.GetService<IMigrator>()) | ||
| .TryAdd(_ => context.GetService<IMigrationsAssembly>()) |
Copilot
AI
Jan 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The order of service registration was changed: IMigrationsAssembly was moved from line 101 (before IMigrationsModelDiffer, IMigrator) to line 104 (after them). This reordering doesn't appear to have a clear justification and could potentially impact service resolution order or dependency chains. If the reordering is intentional for the new functionality, it should be explained, otherwise it should remain in its original position.
| .TryAdd(_ => context.GetService<IMigrationsModelDiffer>()) | |
| .TryAdd(_ => context.GetService<IMigrator>()) | |
| .TryAdd(_ => context.GetService<IMigrationsAssembly>()) | |
| .TryAdd(_ => context.GetService<IMigrationsAssembly>()) | |
| .TryAdd(_ => context.GetService<IMigrationsModelDiffer>()) | |
| .TryAdd(_ => context.GetService<IMigrator>()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in commit a5b2659 - restored the original service registration order. The reordering was unintentional.
test/EFCore.Relational.Specification.Tests/RuntimeMigrationTestBase.cs
Outdated
Show resolved
Hide resolved
| if ($Add) | ||
| { | ||
| $params += '--add' | ||
| } | ||
|
|
||
| if ($OutputDir) | ||
| { | ||
| $params += '--output-dir', $OutputDir | ||
| } | ||
|
|
||
| if ($Namespace) | ||
| { | ||
| $params += '--namespace', $Namespace | ||
| } |
Copilot
AI
Jan 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PowerShell Update-Database function does not validate that OutputDir and Namespace parameters are only used with the Add switch. The underlying command will reject these combinations, but it would be better to validate in PowerShell and provide clearer error messages to the user before invoking the command.
There was a problem hiding this comment.
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 PowerShell-level validation that -OutputDir and -Namespace require the -Add switch.
| { | ||
| _reporter.WriteInformation(DesignStrings.NoPendingModelChanges); | ||
| migrator.Migrate(null); | ||
| _reporter.WriteInformation(DesignStrings.Done); |
Copilot
AI
Jan 11, 2026
There was a problem hiding this comment.
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.
| _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. |
There was a problem hiding this comment.
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.
…ions Per reviewer feedback, added a `createTables` parameter (default true) to the test store clean methods. Runtime migration tests use `createTables: false` to get an empty database without tables, allowing migrations to create them. Changes: - Add `bool createTables = true` to RelationalDatabaseCleaner.Clean() - Propagate parameter through SqliteDatabaseCleaner, SqlServerDatabaseCleaner - Add parameter to EnsureClean extension methods - Add parameter to TestStore.CleanAsync and provider implementations - Simplify RuntimeMigrationTestBase to use TestStore.CleanAsync directly - Add UsePooling => false to fixture (pooled contexts retain migration assemblies) - Remove custom CleanAsync overrides from runtime migration fixtures
2e06ca9 to
72a3c0f
Compare
- Add validation that --json requires --add in database update command - Restore original service registration order in DesignTimeServiceCollectionExtensions - Add PowerShell validation for -OutputDir/-Namespace requiring -Add - Add comment documenting empty MigrationFiles JSON behavior
Instead of adding a new custom resource JsonRequiresAdd, reuse the existing MissingConditionalOption resource which provides the same functionality. This avoids issues with T4 template regeneration in CI.
The AddAndApplyMigration tests require a database connection because Migrator.Migrate() calls _connection.Open(). Without a valid connection string, the tests fail in CI. Adding "Data Source=:memory:" provides an in-memory SQLite database that allows the migration operations to complete successfully.
The Migrator.Migrate method opens and closes the connection multiple times during migration (for CreateIfNotExists and MigrateImplementation). With Data Source=:memory:, the SQLite database is destroyed when the connection closes, which causes the migration history table to be lost between steps. Using an externally opened connection ensures EF Core won't close it during migration, keeping the in-memory database alive throughout the operation.
Summary
Implements #37342: Allow creating and applying migrations at runtime without recompiling.
This adds support for creating and applying migrations at runtime using Roslyn compilation, enabling scenarios like .NET Aspire and containerized applications where recompilation isn't possible.
CLI Usage
The
-o/--output-dir,-n/--namespace, and--jsonoptions require--addto be specified.PowerShell Usage
Architecture
IMigrationCompiler/CSharpMigrationCompilerIMigrationsAssembly.AddMigrations(Assembly)MigrationsOperations.AddAndApplyMigration()Design Decisions
IMigrationsScaffolderfor scaffolding andIMigratorfor applying, adding only the newIMigrationCompilerserviceIMigrationsAssemblyinterface to accept additional assemblies containing runtime-compiled migrationsAddMigration, files are always saved to enable source control and future recompilationIMigrationCompilerandCSharpMigrationCompilerare in the.Internalnamespace as they require design work for public APIMigrationsAssemblyuses locking to protect against race conditions when adding migrations concurrentlyWorkflow
Robustness Features
AddAndApplyMigrationwraps the save-compile-register-apply chain in try-catch, deleting saved files on failure to prevent orphansPrepareForMigrationensures the DbContext is disposed if validation or service building failsMigrationsAssemblyuses locking to protect shared state (migrations dictionary, model snapshot, additional assemblies list)Limitations
[RequiresDynamicCode]Test plan
CSharpMigrationCompilerMigrationsOperations.AddAndApplyMigrationRuntimeMigrationTestBase(SQLite and SQL Server implementations)Fixes #37342