Skip to content

Commit 9a35a9b

Browse files
committed
Add SQL Server integration tests for runtime migrations
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.
1 parent 2b98f49 commit 9a35a9b

File tree

1 file changed

+345
-0
lines changed

1 file changed

+345
-0
lines changed
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.EntityFrameworkCore.Design;
5+
using Microsoft.EntityFrameworkCore.Diagnostics;
6+
using Microsoft.EntityFrameworkCore.Infrastructure;
7+
using Microsoft.EntityFrameworkCore.Migrations.Design;
8+
using Microsoft.EntityFrameworkCore.SqlServer.Design.Internal;
9+
using Microsoft.EntityFrameworkCore.Storage;
10+
using Microsoft.EntityFrameworkCore.TestUtilities;
11+
12+
namespace Microsoft.EntityFrameworkCore.Migrations;
13+
14+
[SqlServerCondition(SqlServerCondition.IsNotAzureSql | SqlServerCondition.IsNotCI)]
15+
public class RuntimeMigrationSqlServerTest : IAsyncLifetime
16+
{
17+
private SqlServerTestStore? _testStore;
18+
private string _tempDirectory = null!;
19+
20+
public async Task InitializeAsync()
21+
{
22+
_testStore = await SqlServerTestStore.CreateInitializedAsync(
23+
"RuntimeMigrationTest_" + Guid.NewGuid().ToString("N")[..8]);
24+
_tempDirectory = Path.Combine(Path.GetTempPath(), "RuntimeMigrationSqlServerTest_" + Guid.NewGuid().ToString("N"));
25+
Directory.CreateDirectory(_tempDirectory);
26+
}
27+
28+
public async Task DisposeAsync()
29+
{
30+
if (_testStore != null)
31+
{
32+
await _testStore.DisposeAsync();
33+
}
34+
35+
if (Directory.Exists(_tempDirectory))
36+
{
37+
try
38+
{
39+
Directory.Delete(_tempDirectory, recursive: true);
40+
}
41+
catch
42+
{
43+
// Ignore cleanup errors
44+
}
45+
}
46+
}
47+
48+
[ConditionalFact]
49+
public void Can_create_and_apply_initial_migration()
50+
{
51+
using var context = CreateContext();
52+
53+
// Ensure the database is clean (no tables)
54+
context.Database.EnsureDeleted();
55+
context.Database.EnsureCreated();
56+
// Drop everything again so we can test migration from scratch
57+
context.Database.EnsureDeleted();
58+
59+
using var freshContext = CreateContext();
60+
61+
// Create the design-time service provider
62+
using var serviceProvider = CreateDesignTimeServiceProvider(freshContext);
63+
using var scope = serviceProvider.CreateScope();
64+
65+
var runtimeMigrationService = scope.ServiceProvider.GetRequiredService<IRuntimeMigrationService>();
66+
67+
// Act - Create and apply migration
68+
var result = runtimeMigrationService.CreateAndApplyMigration(
69+
"InitialCreate",
70+
new RuntimeMigrationOptions
71+
{
72+
PersistToDisk = true,
73+
ProjectDirectory = _tempDirectory,
74+
OutputDirectory = "Migrations"
75+
});
76+
77+
// Assert
78+
Assert.NotNull(result);
79+
Assert.Contains("InitialCreate", result.MigrationId);
80+
Assert.True(result.Applied);
81+
Assert.NotEmpty(result.SqlCommands);
82+
Assert.NotNull(result.MigrationFilePath);
83+
Assert.True(File.Exists(result.MigrationFilePath));
84+
85+
// Verify the table was created
86+
var tableExists = _testStore!.ExecuteScalar<int>(
87+
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'TestEntities'");
88+
Assert.Equal(1, tableExists);
89+
}
90+
91+
[ConditionalFact]
92+
public async Task Can_create_and_apply_initial_migration_async()
93+
{
94+
using var context = CreateContext();
95+
96+
// Ensure the database is clean
97+
await context.Database.EnsureDeletedAsync();
98+
99+
using var freshContext = CreateContext();
100+
101+
// Create the design-time service provider
102+
using var serviceProvider = CreateDesignTimeServiceProvider(freshContext);
103+
using var scope = serviceProvider.CreateScope();
104+
105+
var runtimeMigrationService = scope.ServiceProvider.GetRequiredService<IRuntimeMigrationService>();
106+
107+
// Act - Create and apply migration
108+
var result = await runtimeMigrationService.CreateAndApplyMigrationAsync(
109+
"InitialCreateAsync",
110+
new RuntimeMigrationOptions
111+
{
112+
PersistToDisk = true,
113+
ProjectDirectory = _tempDirectory,
114+
OutputDirectory = "Migrations"
115+
});
116+
117+
// Assert
118+
Assert.NotNull(result);
119+
Assert.Contains("InitialCreateAsync", result.MigrationId);
120+
Assert.True(result.Applied);
121+
Assert.NotEmpty(result.SqlCommands);
122+
123+
// Verify the table was created
124+
var tableExists = await _testStore!.ExecuteScalarAsync<int>(
125+
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'TestEntities'");
126+
Assert.Equal(1, tableExists);
127+
}
128+
129+
[ConditionalFact]
130+
public void Can_create_migration_with_dry_run()
131+
{
132+
using var context = CreateContext();
133+
134+
// Ensure the database is clean
135+
context.Database.EnsureDeleted();
136+
137+
using var freshContext = CreateContext();
138+
139+
// Create the design-time service provider
140+
using var serviceProvider = CreateDesignTimeServiceProvider(freshContext);
141+
using var scope = serviceProvider.CreateScope();
142+
143+
var runtimeMigrationService = scope.ServiceProvider.GetRequiredService<IRuntimeMigrationService>();
144+
145+
// Act - Create migration in dry run mode
146+
var result = runtimeMigrationService.CreateAndApplyMigration(
147+
"DryRunMigration",
148+
new RuntimeMigrationOptions
149+
{
150+
PersistToDisk = false,
151+
DryRun = true
152+
});
153+
154+
// Assert
155+
Assert.NotNull(result);
156+
Assert.Contains("DryRunMigration", result.MigrationId);
157+
Assert.False(result.Applied); // Should not be applied
158+
Assert.NotEmpty(result.SqlCommands); // Should still have SQL commands
159+
Assert.Null(result.MigrationFilePath); // Should not persist to disk
160+
Assert.False(result.PersistedToDisk);
161+
162+
// Verify the table was NOT created
163+
var tableCount = _testStore!.ExecuteScalar<int>(
164+
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'TestEntities'");
165+
Assert.Equal(0, tableCount);
166+
}
167+
168+
[ConditionalFact]
169+
public void CreateAndApplyMigration_generates_valid_sql_commands()
170+
{
171+
using var context = CreateContext();
172+
173+
// Ensure the database is clean
174+
context.Database.EnsureDeleted();
175+
176+
using var freshContext = CreateContext();
177+
178+
// Create the design-time service provider
179+
using var serviceProvider = CreateDesignTimeServiceProvider(freshContext);
180+
using var scope = serviceProvider.CreateScope();
181+
182+
var runtimeMigrationService = scope.ServiceProvider.GetRequiredService<IRuntimeMigrationService>();
183+
184+
// Act
185+
var result = runtimeMigrationService.CreateAndApplyMigration(
186+
"ValidSqlMigration",
187+
new RuntimeMigrationOptions
188+
{
189+
PersistToDisk = false
190+
});
191+
192+
// Assert - SQL commands should include CREATE TABLE
193+
Assert.Contains(result.SqlCommands, sql => sql.Contains("CREATE TABLE"));
194+
Assert.Contains(result.SqlCommands, sql => sql.Contains("TestEntities"));
195+
}
196+
197+
[ConditionalFact]
198+
public void Can_check_for_pending_model_changes()
199+
{
200+
using var context = CreateContext();
201+
202+
// Ensure the database is clean
203+
context.Database.EnsureDeleted();
204+
205+
using var freshContext = CreateContext();
206+
207+
// Create the design-time service provider
208+
using var serviceProvider = CreateDesignTimeServiceProvider(freshContext);
209+
using var scope = serviceProvider.CreateScope();
210+
211+
var runtimeMigrationService = scope.ServiceProvider.GetRequiredService<IRuntimeMigrationService>();
212+
213+
// Act
214+
var hasPendingChanges = runtimeMigrationService.HasPendingModelChanges();
215+
216+
// Assert - Should have pending changes since we have no migrations yet
217+
Assert.True(hasPendingChanges);
218+
}
219+
220+
[ConditionalFact]
221+
public void Applied_migration_appears_in_migration_history()
222+
{
223+
using var context = CreateContext();
224+
225+
// Ensure the database is clean
226+
context.Database.EnsureDeleted();
227+
228+
using var freshContext = CreateContext();
229+
230+
// Create the design-time service provider
231+
using var serviceProvider = CreateDesignTimeServiceProvider(freshContext);
232+
using var scope = serviceProvider.CreateScope();
233+
234+
var runtimeMigrationService = scope.ServiceProvider.GetRequiredService<IRuntimeMigrationService>();
235+
236+
// Act
237+
var result = runtimeMigrationService.CreateAndApplyMigration(
238+
"HistoryTestMigration",
239+
new RuntimeMigrationOptions
240+
{
241+
PersistToDisk = false
242+
});
243+
244+
// Assert - Check migration history table
245+
var migrationId = _testStore!.ExecuteScalar<string>(
246+
"SELECT MigrationId FROM __EFMigrationsHistory WHERE MigrationId LIKE '%HistoryTestMigration'");
247+
Assert.NotNull(migrationId);
248+
Assert.Contains("HistoryTestMigration", migrationId);
249+
}
250+
251+
[ConditionalFact]
252+
public void Can_create_migration_with_custom_namespace()
253+
{
254+
using var context = CreateContext();
255+
256+
// Ensure the database is clean
257+
context.Database.EnsureDeleted();
258+
259+
using var freshContext = CreateContext();
260+
261+
// Create the design-time service provider
262+
using var serviceProvider = CreateDesignTimeServiceProvider(freshContext);
263+
using var scope = serviceProvider.CreateScope();
264+
265+
var runtimeMigrationService = scope.ServiceProvider.GetRequiredService<IRuntimeMigrationService>();
266+
267+
// Act - use a sub-namespace (the Namespace parameter is the sub-namespace added to root)
268+
var result = runtimeMigrationService.CreateAndApplyMigration(
269+
"CustomNamespaceMigration",
270+
new RuntimeMigrationOptions
271+
{
272+
PersistToDisk = true,
273+
ProjectDirectory = _tempDirectory,
274+
OutputDirectory = "Migrations",
275+
Namespace = "CustomMigrations"
276+
});
277+
278+
// Assert
279+
Assert.NotNull(result);
280+
Assert.True(File.Exists(result.MigrationFilePath));
281+
282+
// Verify the file was created and contains namespace declaration
283+
var migrationContent = File.ReadAllText(result.MigrationFilePath!);
284+
Assert.Contains("namespace", migrationContent);
285+
Assert.Contains("CustomMigrations", migrationContent);
286+
}
287+
288+
private TestDbContext CreateContext()
289+
{
290+
var optionsBuilder = new DbContextOptionsBuilder<TestDbContext>();
291+
_testStore!.AddProviderOptions(optionsBuilder);
292+
293+
return new TestDbContext(optionsBuilder.Options);
294+
}
295+
296+
private ServiceProvider CreateDesignTimeServiceProvider(DbContext context)
297+
{
298+
var serviceCollection = new ServiceCollection()
299+
.AddEntityFrameworkDesignTimeServices()
300+
.AddDbContextDesignTimeServices(context);
301+
302+
// Add SQL Server design-time services
303+
new SqlServerDesignTimeServices().ConfigureDesignTimeServices(serviceCollection);
304+
305+
// Add additional required services for RuntimeMigrationService
306+
serviceCollection.AddScoped(_ => context.GetService<IMigrationsSqlGenerator>());
307+
serviceCollection.AddScoped(_ => context.GetService<IRelationalDatabaseCreator>());
308+
serviceCollection.AddScoped(_ => context.GetService<IMigrationCommandExecutor>());
309+
serviceCollection.AddScoped(_ => context.GetService<IRelationalConnection>());
310+
serviceCollection.AddScoped(_ => context.GetService<IRawSqlCommandBuilder>());
311+
serviceCollection.AddScoped(_ => context.GetService<IRelationalCommandDiagnosticsLogger>());
312+
313+
return serviceCollection.BuildServiceProvider(validateScopes: true);
314+
}
315+
316+
// Test DbContext with entities - no existing migrations/snapshot
317+
public class TestDbContext : DbContext
318+
{
319+
public TestDbContext(DbContextOptions<TestDbContext> options)
320+
: base(options)
321+
{
322+
}
323+
324+
public DbSet<TestEntity> TestEntities { get; set; } = null!;
325+
326+
protected override void OnModelCreating(ModelBuilder modelBuilder)
327+
{
328+
modelBuilder.Entity<TestEntity>(entity =>
329+
{
330+
entity.HasKey(e => e.Id);
331+
entity.Property(e => e.Name).HasMaxLength(100);
332+
entity.Property(e => e.Description).HasMaxLength(500);
333+
});
334+
}
335+
}
336+
337+
public class TestEntity
338+
{
339+
public int Id { get; set; }
340+
public string Name { get; set; } = string.Empty;
341+
public string? Description { get; set; }
342+
public DateTime CreatedAt { get; set; }
343+
}
344+
}
345+

0 commit comments

Comments
 (0)