Guides

EF Core store

Save a biome through a DbContext with one SaveChangesAsync.

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:

TestBiome.cs
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();
A tracked entity can make a read return the object from setup without exercising a database round trip. When the test is meant to check persisted rows, call _db.ChangeTracker.Clear() after BuildAsync().

What the store does

  1. db.AddRange(entities) in the order Biome supplies.
  2. 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.

Reviews/Blueprints.cs
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}");
EfCoreStoreTests.cs
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
EF Core discovers entities reachable through navigations. A transient entity referenced by a saved row is therefore saved with it.

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);
}

Rows seen by the database

Seed rows, aggregate parts and BeforeSave rows are all saved. Transient entities are not (unless referenced, as above).

EfCoreStoreTests.cs
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:

MyStore.cs
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;
    }
}