Skip to content

Commit 4cc987d

Browse files
fix(tests): serialize schema creation per test factory
Use a per-factory SemaphoreSlim for EnsureCreatedHostedService so concurrent host startup for the same WebApplicationFactory cannot race while creating the in-memory SQLite schema. Remove Pooling=True from the in-memory SQLite connection string to keep database lifetime governed by the explicit keep-alive connection.
1 parent 761b9d5 commit 4cc987d

1 file changed

Lines changed: 22 additions & 7 deletions

File tree

EssentialCSharp.Web.Tests/WebApplicationFactory.cs

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ public sealed class WebApplicationFactory : TestWebApplicationFactory<Program>
1717
// One GUID per factory instance (field, not computed property) ensures each factory
1818
// gets its own isolated in-memory database and keeps a stable connection string.
1919
private readonly string _sqlConnectionString =
20-
$"DataSource=file:{Guid.NewGuid():N}?mode=memory&cache=shared;Pooling=True";
20+
$"DataSource=file:{Guid.NewGuid():N}?mode=memory&cache=shared";
21+
22+
private readonly SemaphoreSlim _schemaInitializationGate = new(1, 1);
2123

2224
// Kept open for the factory lifetime so the shared-cache in-memory database is not dropped
2325
// when per-scope connections are disposed between requests.
@@ -79,7 +81,10 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
7981
nameof(EnsureCreatedHostedService));
8082

8183
ServiceDescriptor ensureCreatedDescriptor =
82-
ServiceDescriptor.Singleton<IHostedService, EnsureCreatedHostedService>();
84+
ServiceDescriptor.Singleton<IHostedService>(
85+
serviceProvider => new EnsureCreatedHostedService(
86+
serviceProvider,
87+
_schemaInitializationGate));
8388
services.Insert(0, ensureCreatedDescriptor);
8489

8590
// Replace IListingSourceCodeService with one backed by TestData
@@ -124,14 +129,24 @@ private static void RemoveSingleOrNone(
124129
}
125130
}
126131

127-
private sealed class EnsureCreatedHostedService(IServiceProvider serviceProvider) : IHostedService
132+
private sealed class EnsureCreatedHostedService(
133+
IServiceProvider serviceProvider,
134+
SemaphoreSlim schemaInitializationGate) : IHostedService
128135
{
129136
public async Task StartAsync(CancellationToken cancellationToken)
130137
{
131-
using IServiceScope scope = serviceProvider.CreateScope();
132-
EssentialCSharpWebContext dbContext =
133-
scope.ServiceProvider.GetRequiredService<EssentialCSharpWebContext>();
134-
await dbContext.Database.EnsureCreatedAsync(cancellationToken);
138+
await schemaInitializationGate.WaitAsync(cancellationToken);
139+
try
140+
{
141+
using IServiceScope scope = serviceProvider.CreateScope();
142+
EssentialCSharpWebContext dbContext =
143+
scope.ServiceProvider.GetRequiredService<EssentialCSharpWebContext>();
144+
await dbContext.Database.EnsureCreatedAsync(cancellationToken);
145+
}
146+
finally
147+
{
148+
schemaInitializationGate.Release();
149+
}
135150
}
136151

137152
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;

0 commit comments

Comments
 (0)