Skip to content

Conversation

@thromel
Copy link
Contributor

@thromel thromel commented Dec 23, 2025

Summary

Implements #37342: Allow to create a migration and apply it 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.

New Features

CLI:

dotnet ef database update --add MigrationName [--output-dir <DIR>] [--namespace <NS>]

Programmatic API:

var result = context.Database.CreateAndApplyMigration("MigrationName");
// or with options
var result = context.Database.CreateAndApplyMigration("MigrationName", new RuntimeMigrationOptions
{
    PersistToDisk = true,
    OutputDirectory = "Migrations",
    DryRun = false
});

Architecture

Component Purpose
IMigrationCompiler / CSharpMigrationCompiler Roslyn-based compilation of scaffolded migrations
IDynamicMigrationsAssembly / DynamicMigrationsAssembly Decorator that merges runtime-compiled migrations with static ones
IRuntimeMigrationService / RuntimeMigrationService Orchestrates scaffold → compile → register → apply flow
RuntimeMigrationOptions Configuration for persistence, output directory, namespace, dry-run
RuntimeMigrationResult Contains migration ID, applied status, SQL commands, file paths

Limitations

  • Requires dynamic code generation (incompatible with NativeAOT) - marked with [RequiresDynamicCode]
  • C# only (no VB.NET/F# support initially)

Test plan

  • Unit tests for CSharpMigrationCompiler (5 tests)
  • Unit tests for DynamicMigrationsAssembly (12 tests)
  • All existing EFCore.Design.Tests pass (1210 tests)
  • All existing EFCore.Relational.Tests pass (1386 tests)
  • Integration tests with actual database (SQLite: 8 tests, SQL Server: 7 tests)
  • CLI functional tests (2 tests)

Fixes #37342

Implements dotnet#37342: Allow to create a migration and apply it 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.

## New Features

### CLI
```bash
dotnet ef database update --add MigrationName [--output-dir <DIR>] [--namespace <NS>]
```

### Programmatic API
```csharp
var result = context.Database.CreateAndApplyMigration("MigrationName");
// or with options
var result = context.Database.CreateAndApplyMigration("MigrationName", new RuntimeMigrationOptions
{
    PersistToDisk = true,
    OutputDirectory = "Migrations",
    DryRun = false
});
```

## Architecture

- `IMigrationCompiler` / `CSharpMigrationCompiler`: Roslyn-based compilation of scaffolded migrations
- `IDynamicMigrationsAssembly` / `DynamicMigrationsAssembly`: Decorator that merges runtime-compiled migrations with static ones
- `IRuntimeMigrationService` / `RuntimeMigrationService`: Orchestrates scaffold -> compile -> register -> apply flow
- `RuntimeMigrationOptions`: Configuration for persistence, output directory, namespace, dry-run
- `RuntimeMigrationResult`: Contains migration ID, applied status, SQL commands, file paths

## Limitations

- Requires dynamic code generation (incompatible with NativeAOT)
- C# only (no VB.NET/F# support initially)
- Test error handling when no model changes detected
- Test error handling for invalid context type
- Uses OperationExecutor pattern consistent with AddMigration tests
…iring

Integration tests:
- Can_create_and_apply_initial_migration
- Can_create_and_apply_initial_migration_async
- Can_create_migration_with_dry_run
- CreateAndApplyMigration_generates_valid_sql_commands
- CreateAndApplyMigration_throws_when_no_pending_changes
- Can_create_migration_with_custom_namespace
- Can_check_for_pending_model_changes
- Applied_migration_appears_in_migration_history

Service wiring fixes:
- Create DynamicMigrationsAssembly in AddDbContextDesignTimeServices to properly wrap context's IMigrationsAssembly
- Register DynamicMigrationsAssembly as both IDynamicMigrationsAssembly and IMigrationsAssembly
- Add IDesignTimeModel registration from context

RuntimeMigrationService changes:
- Apply migrations directly instead of using IMigrator.Migrate() to avoid migration lookup issues
- Add ApplyMigration and ApplyMigrationAsync methods that execute commands directly
- Inject additional services: IMigrationCommandExecutor, IRelationalConnection, IRawSqlCommandBuilder, IRelationalCommandDiagnosticsLogger
Add RuntimeMigrationSqlServerTest.cs with 7 integration tests for SQL Server:
- Can_create_and_apply_initial_migration
- Can_create_and_apply_initial_migration_async
- Can_create_migration_with_dry_run
- CreateAndApplyMigration_generates_valid_sql_commands
- Can_check_for_pending_model_changes
- Applied_migration_appears_in_migration_history
- Can_create_migration_with_custom_namespace

Tests are marked with [SqlServerCondition] and will be skipped on
platforms without SQL Server configured.
The dotnet-ef project includes *.Configure.cs files from the ef project,
but uses its own Resources class. When DatabaseUpdateCommand.Configure.cs
references Resources.DatabaseUpdateAddDescription, it needs to be present
in both the ef and dotnet-ef Resources.
The previous implementation called context.GetService<IMigrationsAssembly>()
immediately during service registration. This failed for contexts that don't
have a database provider configured (like some test contexts used in
DbContextOperationsTest).

Changed to lazy initialization so the migrations assembly is only retrieved
when actually needed, matching the original pattern for other services.
@thromel thromel marked this pull request as ready for review December 24, 2025 06:22
@thromel thromel requested a review from a team as a code owner December 24, 2025 06:22
@thromel thromel marked this pull request as draft December 25, 2025 02:03
In dry run mode after EnsureDeleted(), the database doesn't exist,
so we can't query INFORMATION_SCHEMA.TABLES. The result.Applied=false
assertion is sufficient to verify dry run behavior.
@thromel thromel force-pushed the feature/runtime-migrations branch from 62018f1 to 5c41f2f Compare December 25, 2025 03:51
In Helix distributed testing, PIPELINE_WORKSPACE is not set.
Add HELIX_CORRELATION_PAYLOAD check so tests marked with
[SqlServerCondition(IsNotCI)] are properly skipped in Helix.
@thromel
Copy link
Contributor Author

thromel commented Dec 25, 2025

Note on SQL Server Integration Tests

The RuntimeMigrationSqlServerTest tests are marked with [SqlServerCondition(SqlServerCondition.IsNotAzureSql | SqlServerCondition.IsNotCI)] and are skipped in CI. This follows the same pattern used by MigrationsInfrastructureSqlServerTest.

Why these tests are skipped in CI:

  • They require creating fresh databases dynamically for each test to properly test the migration flow from scratch
  • The Helix CI environment has SQL Server available but with limited permissions configured for shared/pre-configured databases
  • Tests that use SqlServerTestStore.CreateInitializedAsync with dynamic database names don't work in the CI SQL Server setup

Test coverage is still maintained:

  • Core runtime migration logic is covered by unit tests in EFCore.Design.Tests (which run in CI)
  • The SQL Server integration tests run locally for developers with SQL Server configured
  • SQLite integration tests in EFCore.Sqlite.FunctionalTests also validate the end-to-end flow

This is consistent with how other complex migration infrastructure tests handle CI limitations.

New SQLite integration tests:
- Can_apply_multiple_sequential_migrations: Apply 2 migrations in sequence
- Can_create_migration_with_indexes_and_foreign_keys: Complex model with FK/indexes
- Generated_migration_file_has_correct_structure: Verify file contents
- Down_migration_reverses_changes: Verify rollback works

New CLI/OperationExecutor tests:
- CreateAndApplyMigration_errors_for_bad_names: Invalid migration name handling
- CreateAndApplyMigration_errors_for_empty_name: Empty name validation
- Add RevertMigration and RevertMigrationAsync methods to
  IRuntimeMigrationService interface
- Implement revert functionality in RuntimeMigrationService that:
  - Tracks applied dynamic migrations
  - Executes Down operations to undo changes
  - Removes migration from history table
- Add error handling for missing/not-found migrations
- Fix tests to properly verify actual rollback behavior instead of
  just checking generated code
- Add tests for error cases and async revert
@thromel thromel marked this pull request as ready for review December 25, 2025 08:13
@thromel thromel marked this pull request as draft December 25, 2025 16:43
- Add partial failure tests (table conflict, external interference)
- Add error recovery tests (revert after manual drop)
- Add dry run safety verification
- Add migration tracking and result verification tests
- Add edge case tests for special characters and null paths
- Total: 26 SQLite functional tests, 20 design tests (46 total)
Testing for transient CI failures.
Add validation for invalid file name characters and context name
conflicts to the CreateAndApplyMigration operation, matching the
existing validation in the AddMigration operation. This fixes the
Windows CI test failure where tests for bad migration names were
expecting proper error handling.
@thromel thromel marked this pull request as ready for review December 25, 2025 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow to create a migration and apply it without recompiling

2 participants