Skip to content

Commit 2b98f49

Browse files
committed
Add SQLite integration tests for runtime migrations and fix service wiring
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
1 parent 5166a44 commit 2b98f49

File tree

3 files changed

+466
-6
lines changed

3 files changed

+466
-6
lines changed

src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ public static IServiceCollection AddEntityFrameworkDesignTimeServices(
6868
.TryAddScoped<IMigrationsScaffolder, MigrationsScaffolder>()
6969
.TryAddScoped<ISnapshotModelProcessor, SnapshotModelProcessor>()
7070
.TryAddSingleton<IMigrationCompiler, CSharpMigrationCompiler>()
71-
.TryAddScoped<IDynamicMigrationsAssembly>(sp =>
72-
new DynamicMigrationsAssembly(sp.GetRequiredService<IMigrationsAssembly>()))
71+
// Note: IDynamicMigrationsAssembly and IMigrationsAssembly are registered in
72+
// AddDbContextDesignTimeServices to ensure proper wrapping of the context's assembly
7373
.TryAddScoped<IRuntimeMigrationService, RuntimeMigrationService>());
7474

7575
var loggerFactory = new LoggerFactory(
@@ -92,6 +92,12 @@ public static IServiceCollection AddDbContextDesignTimeServices(
9292
this IServiceCollection services,
9393
DbContext context)
9494
{
95+
// Get the context's IMigrationsAssembly to wrap in DynamicMigrationsAssembly
96+
var innerMigrationsAssembly = context.GetService<IMigrationsAssembly>();
97+
98+
// Create DynamicMigrationsAssembly wrapping the context's IMigrationsAssembly
99+
var dynamicMigrationsAssembly = new DynamicMigrationsAssembly(innerMigrationsAssembly);
100+
95101
new EntityFrameworkRelationalServicesBuilder(services)
96102
.TryAdd(context.GetService<IDatabaseProvider>())
97103
.TryAdd(_ => context.GetService<IMigrationsIdGenerator>())
@@ -101,10 +107,18 @@ public static IServiceCollection AddDbContextDesignTimeServices(
101107
.TryAdd(_ => context.GetService<ICurrentDbContext>())
102108
.TryAdd(_ => context.GetService<IDbContextOptions>())
103109
.TryAdd(_ => context.GetService<IHistoryRepository>())
104-
.TryAdd(_ => context.GetService<IMigrationsAssembly>())
105110
.TryAdd(_ => context.GetService<IMigrationsModelDiffer>())
106111
.TryAdd(_ => context.GetService<IMigrator>())
112+
.TryAdd(_ => context.GetService<IDesignTimeModel>())
107113
.TryAdd(_ => context.GetService<IDesignTimeModel>().Model);
114+
115+
// Register DynamicMigrationsAssembly as both IDynamicMigrationsAssembly and IMigrationsAssembly
116+
// Note: For runtime migration to work, the IMigrator from context needs to be updated to use
117+
// DynamicMigrationsAssembly, which isn't directly possible. The RuntimeMigrationService handles
118+
// this by registering migrations with DynamicMigrationsAssembly and calling the context's Migrator.
119+
services.AddScoped<IDynamicMigrationsAssembly>(_ => dynamicMigrationsAssembly);
120+
services.AddScoped<IMigrationsAssembly>(_ => dynamicMigrationsAssembly);
121+
108122
return services;
109123
}
110124
}

src/EFCore.Design/Migrations/Design/RuntimeMigrationService.cs

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Diagnostics.CodeAnalysis;
5+
using Microsoft.EntityFrameworkCore.Diagnostics;
56
using Microsoft.EntityFrameworkCore.Internal;
7+
using Microsoft.EntityFrameworkCore.Storage;
68

79
namespace Microsoft.EntityFrameworkCore.Migrations.Design;
810

@@ -34,6 +36,11 @@ public class RuntimeMigrationService : IRuntimeMigrationService
3436
private readonly IMigrator _migrator;
3537
private readonly IMigrationsSqlGenerator _sqlGenerator;
3638
private readonly IRelationalDatabaseCreator _databaseCreator;
39+
private readonly IMigrationCommandExecutor _commandExecutor;
40+
private readonly IRelationalConnection _connection;
41+
private readonly IHistoryRepository _historyRepository;
42+
private readonly IRawSqlCommandBuilder _rawSqlCommandBuilder;
43+
private readonly IRelationalCommandDiagnosticsLogger _commandLogger;
3744

3845
/// <summary>
3946
/// Initializes a new instance of the <see cref="RuntimeMigrationService" /> class.
@@ -45,14 +52,24 @@ public class RuntimeMigrationService : IRuntimeMigrationService
4552
/// <param name="migrator">The migrator.</param>
4653
/// <param name="sqlGenerator">The SQL generator.</param>
4754
/// <param name="databaseCreator">The database creator.</param>
55+
/// <param name="commandExecutor">The migration command executor.</param>
56+
/// <param name="connection">The relational connection.</param>
57+
/// <param name="historyRepository">The history repository.</param>
58+
/// <param name="rawSqlCommandBuilder">The raw SQL command builder.</param>
59+
/// <param name="commandLogger">The command logger.</param>
4860
public RuntimeMigrationService(
4961
ICurrentDbContext currentContext,
5062
IMigrationsScaffolder scaffolder,
5163
IMigrationCompiler compiler,
5264
IDynamicMigrationsAssembly dynamicMigrationsAssembly,
5365
IMigrator migrator,
5466
IMigrationsSqlGenerator sqlGenerator,
55-
IRelationalDatabaseCreator databaseCreator)
67+
IRelationalDatabaseCreator databaseCreator,
68+
IMigrationCommandExecutor commandExecutor,
69+
IRelationalConnection connection,
70+
IHistoryRepository historyRepository,
71+
IRawSqlCommandBuilder rawSqlCommandBuilder,
72+
IRelationalCommandDiagnosticsLogger commandLogger)
5673
{
5774
_currentContext = currentContext;
5875
_scaffolder = scaffolder;
@@ -61,6 +78,11 @@ public RuntimeMigrationService(
6178
_migrator = migrator;
6279
_sqlGenerator = sqlGenerator;
6380
_databaseCreator = databaseCreator;
81+
_commandExecutor = commandExecutor;
82+
_connection = connection;
83+
_historyRepository = historyRepository;
84+
_rawSqlCommandBuilder = rawSqlCommandBuilder;
85+
_commandLogger = commandLogger;
6486
}
6587

6688
/// <inheritdoc />
@@ -113,7 +135,7 @@ public virtual RuntimeMigrationResult CreateAndApplyMigration(
113135
var applied = false;
114136
if (!options.DryRun)
115137
{
116-
_migrator.Migrate(compiledMigration.MigrationId);
138+
ApplyMigration(compiledMigration, sqlCommands);
117139
applied = true;
118140
}
119141

@@ -177,7 +199,7 @@ public virtual async Task<RuntimeMigrationResult> CreateAndApplyMigrationAsync(
177199
var applied = false;
178200
if (!options.DryRun)
179201
{
180-
await _migrator.MigrateAsync(compiledMigration.MigrationId, cancellationToken)
202+
await ApplyMigrationAsync(compiledMigration, sqlCommands, cancellationToken)
181203
.ConfigureAwait(false);
182204
applied = true;
183205
}
@@ -191,6 +213,104 @@ await _migrator.MigrateAsync(compiledMigration.MigrationId, cancellationToken)
191213
savedFiles?.SnapshotFile);
192214
}
193215

216+
/// <summary>
217+
/// Applies a compiled migration to the database.
218+
/// </summary>
219+
/// <param name="compiledMigration">The compiled migration to apply.</param>
220+
/// <param name="sqlCommands">The pre-generated SQL commands.</param>
221+
protected virtual void ApplyMigration(CompiledMigration compiledMigration, IReadOnlyList<string> sqlCommands)
222+
{
223+
// Ensure database exists
224+
if (!_databaseCreator.Exists())
225+
{
226+
_databaseCreator.Create();
227+
}
228+
229+
_connection.Open();
230+
try
231+
{
232+
// Ensure history table exists
233+
if (!_historyRepository.Exists())
234+
{
235+
_historyRepository.Create();
236+
}
237+
238+
// Get the migration and its operations
239+
var migration = _dynamicMigrationsAssembly.CreateMigration(
240+
compiledMigration.MigrationTypeInfo,
241+
_currentContext.Context.Database.ProviderName!);
242+
243+
// Build the history insert command
244+
var insertCommand = _rawSqlCommandBuilder.Build(
245+
_historyRepository.GetInsertScript(new HistoryRow(compiledMigration.MigrationId, ProductInfo.GetVersion())));
246+
247+
// Generate migration commands and append history insert
248+
var migrationCommands = _sqlGenerator.Generate(
249+
migration.UpOperations,
250+
_currentContext.Context.Model).ToList();
251+
migrationCommands.Add(new MigrationCommand(insertCommand, _currentContext.Context, _commandLogger));
252+
253+
// Execute all commands
254+
_commandExecutor.ExecuteNonQuery(migrationCommands, _connection);
255+
}
256+
finally
257+
{
258+
_connection.Close();
259+
}
260+
}
261+
262+
/// <summary>
263+
/// Applies a compiled migration to the database asynchronously.
264+
/// </summary>
265+
/// <param name="compiledMigration">The compiled migration to apply.</param>
266+
/// <param name="sqlCommands">The pre-generated SQL commands.</param>
267+
/// <param name="cancellationToken">A token to observe while waiting for the task to complete.</param>
268+
/// <returns>A task representing the asynchronous operation.</returns>
269+
protected virtual async Task ApplyMigrationAsync(
270+
CompiledMigration compiledMigration,
271+
IReadOnlyList<string> sqlCommands,
272+
CancellationToken cancellationToken = default)
273+
{
274+
// Ensure database exists
275+
if (!await _databaseCreator.ExistsAsync(cancellationToken).ConfigureAwait(false))
276+
{
277+
await _databaseCreator.CreateAsync(cancellationToken).ConfigureAwait(false);
278+
}
279+
280+
await _connection.OpenAsync(cancellationToken).ConfigureAwait(false);
281+
try
282+
{
283+
// Ensure history table exists
284+
if (!await _historyRepository.ExistsAsync(cancellationToken).ConfigureAwait(false))
285+
{
286+
await _historyRepository.CreateAsync(cancellationToken).ConfigureAwait(false);
287+
}
288+
289+
// Get the migration and its operations
290+
var migration = _dynamicMigrationsAssembly.CreateMigration(
291+
compiledMigration.MigrationTypeInfo,
292+
_currentContext.Context.Database.ProviderName!);
293+
294+
// Build the history insert command
295+
var insertCommand = _rawSqlCommandBuilder.Build(
296+
_historyRepository.GetInsertScript(new HistoryRow(compiledMigration.MigrationId, ProductInfo.GetVersion())));
297+
298+
// Generate migration commands and append history insert
299+
var migrationCommands = _sqlGenerator.Generate(
300+
migration.UpOperations,
301+
_currentContext.Context.Model).ToList();
302+
migrationCommands.Add(new MigrationCommand(insertCommand, _currentContext.Context, _commandLogger));
303+
304+
// Execute all commands
305+
await _commandExecutor.ExecuteNonQueryAsync(migrationCommands, _connection, cancellationToken)
306+
.ConfigureAwait(false);
307+
}
308+
finally
309+
{
310+
await _connection.CloseAsync().ConfigureAwait(false);
311+
}
312+
}
313+
194314
/// <inheritdoc />
195315
public virtual bool HasPendingModelChanges()
196316
=> _migrator.HasPendingModelChanges();

0 commit comments

Comments
 (0)