EF Core store
Wire it up
When the behavior you want to test reads from a database, the graph needs to be persisted first. BuildAsync passes it to an IBiomeStore, with seed rows followed by created entities. The EF Core package supplies EfCoreStore, so you can use the DbContext your test already owns:
using Microsoft.EntityFrameworkCore;
using Zeeq.Biome.EntityFrameworkCore;
public static partial class TestBiome
{
public static BiomeBuilder<ZeeqSeed, ZeeqSeed> Start(
DbContext db,
out ZeeqSeed seed,
TimeProvider? time = null
) => Start(out seed, db.AsBiomeStore(), time); // or: new EfCoreStore(db)
}
db.AsBiomeStore() and new EfCoreStore(db) are equivalent. The store does not own the context: you create, reuse and dispose it.
await TestBiome
.Start(_db, out var seed)
.AddLockedReview(out var activeLock)
.BuildAsync(); // AddRange in order, one SaveChangesAsync
// so the code under test reads rows from the database, not tracked instances
_db.ChangeTracker.Clear();
var seed = ZeeqSeed.Generate();
/* … construct repo, pr, review, activeLock with every ID, FK and timestamp … */
_db.AddRange(seed.Owner, seed.Organization);
_db.AddRange(repo, pr, review, activeLock);
await _db.SaveChangesAsync();
_db.ChangeTracker.Clear();
_db.ChangeTracker.Clear() after BuildAsync().What the store does
db.AddRange(entities)in the order Biome supplies.- One
SaveChangesAsync, honoring the cancellation token.
EF Core sorts inserts by foreign-key dependency, so the order matters less than the keys.
Database-generated keys
A database-generated key is not available while the child blueprint runs. Set the navigation property to the parent instead of copying its current key. EF Core uses that relationship to fix up foreign keys when the generated values become available.
internal static readonly ChildBlueprint<
ReviewSeed,
CodeRepository,
PullRequestRecord
> PullRequest = Blueprint
.ChildOf<ReviewSeed, CodeRepository, PullRequestRecord>(
(repo, ctx) => new() { Repository = repo, CreatedAt = ctx.Now }
)
.Derive(pr => pr.Number, Numbers)
.Derive(pr => pr.Title, (pr, _) => $"PR {pr.Number}");
await using var db = sqlite.CreateContext();
await TestBiome
.Start(db, out var seed)
.AddCodeRepository(out var repo)
.AddPullRequests(out var prs, 2)
.AddCodeReviews(out var reviews, 1)
.BuildAsync();
await using var read = sqlite.CreateContext();
var savedPrs = await read
.PullRequests.Where(pr => pr.Repository.Id == repo.Id)
.ToListAsync();
// repo.Id > 0 now: the key was generated on save, and every FK was fixed up
SQLite and Postgres
SQLite makes it quick to check relationships, but it does not reproduce Postgres timestamp storage. The library runs the same chains on both: in-memory SQLite for relational shape, and Postgres through Testcontainers for timestamp round trips.
public sealed class SqliteDb : IAsyncInitializer, IAsyncDisposable
{
private readonly SqliteConnection connection = new("DataSource=:memory:");
public async Task InitializeAsync()
{
await connection.OpenAsync();
await using var db = CreateContext();
await db.Database.EnsureCreatedAsync();
}
internal ReviewDb CreateContext() =>
new(new DbContextOptionsBuilder<ReviewDb>().UseSqlite(connection).Options);
}
public sealed class PostgresFixture : IAsyncInitializer, IAsyncDisposable
{
private PostgreSqlContainer? container;
public async Task InitializeAsync()
{
container = new PostgreSqlBuilder("postgres:17-alpine").Build();
await container.StartAsync();
// connectionString =
// container.GetConnectionString();
// EnsureCreatedAsync() …
}
internal ReviewDb CreateContext() =>
new(
new DbContextOptionsBuilder<ReviewDb>()
.UseNpgsql(connectionString)
.Options
);
}
Rows seen by the database
Seed rows, aggregate parts and BeforeSave rows are all saved. Transient entities are not (unless referenced, as above).
await using var db = sqlite.CreateContext();
await TestBiome
.Start(db, out _)
.AddCodeRepository(out var repo)
.AddPullRequest(out var kept)
.Under(repo)
.AddPullRequest(out var draft)
.Transient()
.BuildAsync();
// draft.Id == 0: never saved; the database holds only `kept`
Other databases
Anything else implements IBiomeStore, a single method:
public sealed class MyStore : IBiomeStore
{
public ValueTask SaveAsync(
IReadOnlyList<object> entities,
CancellationToken cancellationToken
)
{
// entities: aggregates expanded, transients dropped, duplicates removed,
// BeforeSave already applied
return ValueTask.CompletedTask;
}
}