Patterns for integration testing Aspire-hosted .NET services with WebApplicationFactory, Testcontainers, and SQLite
When writing integration tests for .NET Aspire services that use AddNpgsqlDbContext and AddRedisClient (or similar Aspire component methods). Applies to any service using Aspire 13.x with PostgreSQL + Redis.
WebApplicationFactory<Program> to host the service under test — avoids Aspire AppHost orchestration in testsAddNpgsqlDbContext, AddRedisClient) validate connection strings at registration time — provide fake connection strings via builder.UseSetting("ConnectionStrings:name", "...") in ConfigureWebHostFullName containing "EntityFramework", "Npgsql", "DbContext") then re-registering with AddDbContext<T>(o => o.UseSqlite(...))Cache=Shared requires a keep-alive SqliteConnection that stays open for the entire fixture lifetimeEnsureCreatedAsync() during fixture initialization, before the host startsISubscriber.OnMessage callbacks is brittleIAsyncLifetime on the factory class for container lifecycle (note: xUnit IAsyncLifetime uses Task, not ValueTask)IClassFixture<BridgeApiFactory> to share the factory across tests in a class — avoids container churn// Keep-alive connection pattern for SQLite in-memory
private SqliteConnection _keepAlive = null!;
_keepAlive = new SqliteConnection(connStr);
await _keepAlive.OpenAsync();
// ... in DisposeAsync: await _keepAlive.DisposeAsync();
// Removing Aspire-registered services
var toRemove = services.Where(d => {
var st = d.ServiceType.FullName ?? "";
return st.Contains("EntityFramework", OrdinalIgnoreCase)
|| st.Contains("Npgsql", OrdinalIgnoreCase)
|| st.Contains("DbContext", OrdinalIgnoreCase);
}).ToList();
foreach (var d in toRemove) services.Remove(d);
// Provider-agnostic Max query (works on both Npgsql and SQLite)
var maxIndex = await db.Items.Select(x => (int?)x.Index).MaxAsync();
var nextIndex = (maxIndex ?? -1) + 1;
DefaultIfEmpty(value).MaxAsync() — SQLite provider can't translate itBuildServiceProvider() inside ConfigureServices to call EnsureCreated() — it creates a separate DI container with stale registrations