Examples

Code reviews

Repository, pull request, review and lock - the library's reference domain.

A lock test needs a valid chain of related rows before it can check the lock itself. This example shows how to keep that repeated branch in a helper while leaving numbering and time predictable.

Mirrors a real code-review app: the seed is an organization and an owner, and the graph is CodeRepository → PullRequestRecord → CodeReviewRecord → ActiveCodeReviewLock. Source: tests/Zeeq.Biome.Tests/CodeReviews.

The blueprints

CodeReviews/Domain.cs
// Scope is declared on the sequence: Under(repo) and
// blueprint variants share its progression.
internal static readonly Sequence<int> Numbers = Sequence
    .Of(1, n => n + 1)
    .Per<CodeRepository>();

internal static readonly Blueprint<CodeReviewSeed, CodeRepository> Repository =
    Blueprint.For<CodeReviewSeed, CodeRepository>(ctx =>
        new()
        {
            Id = ctx.NewId("repo"),
            Organization = ctx.Seed.Organization,
            CreatedAt = ctx.Now,
        }
    );

// Leave Number unset; derive it first, then Title and Branch from its final value.
internal static readonly ChildBlueprint<
    CodeReviewSeed,
    CodeRepository,
    PullRequestRecord
> PullRequest = Blueprint
    .ChildOf<CodeReviewSeed, CodeRepository, PullRequestRecord>(
        (repo, ctx) => new() { Id = ctx.NewId("pr"), Repository = repo }
    )
    .Derive(pr => pr.Number, Numbers)
    .Derive(pr => pr.Title, (pr, _) => $"PR {pr.Number}")
    .Derive(pr => pr.Branch, (pr, _) => $"feature/{pr.Number}");

// Wire the typed PR parent and walk recorded ancestors to the repository.
internal static readonly ChildBlueprint<
    CodeReviewSeed,
    PullRequestRecord,
    CodeReviewRecord
> Review = Blueprint.ChildOf<
    CodeReviewSeed,
    PullRequestRecord,
    CodeReviewRecord
>(
    (pr, ctx) =>
        new()
        {
            Id = ctx.NewId("review"),
            PullRequest = pr,
            Repository = ctx.Ancestor<CodeRepository>(),
            CreatedAt = ctx.Now,
        }
);

A locked review in one line

The scenario helper builds the four-level default chain. The test asserts on the lock and the row order in the store.

var store = new RecordingStore();
var biome = TestBiome.Start(out var seed, store);

await biome.AddLockedReview(out var activeLock).BuildAsync();

// The leaf references its ancestors; seed rows precede the four created rows.
await Assert
    .That(activeLock.Repository)
    .IsSameReferenceAs(activeLock.Review.PullRequest.Repository);
await Assert.That(store.Entities.Count).IsEqualTo(6);
The setup shrinks from 26 lines to 3, but the more useful change is where the relationships are defined. The lock, review, and pull request need the same repository instance. Keeping that wiring in blueprints avoids repeating it in each test.

Numbers resume across calls and variants

The same Numbers sequence is shared by the closed variant, and .Under(repos) fans out across both repositories.

CodeReviewTests.cs
var biome = TestBiome.Start(out _);
var closed = Blueprints.PullRequest.With(PullRequests.Closed);

await biome
    .AddCodeRepositories(out var repos, 2)
    .AddPullRequests(out var first, 2)
    .Under(repos)
    .Add(closed, out var second, count: 2)
    .Under(repos[0])
    .AddPullRequest(out var fifth)
    .BuildAsync();

// first:  1, 2, 1, 2    (numbering restarts per repository)
// second: 3, 4, 3, 4    (the variant resumes the same sequence)
// fifth:  5, Title "PR 5", Branch "feature/5"

Explicit values are reserved first

Explicit numbers are skipped by the sequence, even when they come later in the same step:

CodeReviewTests.cs
await TestBiome
    .Start(out _)
    .AddCodeRepository(out var repo)
    .AddPullRequests(
        out var prs,
        _ => { },
        pr => pr.Number = 1,
        pr => pr.Number = 3,
        _ => { }
    )
    .Under(repo)
    .AddPullRequest(out var next)
    .BuildAsync();
// prs: 2, 1, 3, 4   next: 5
Set explicit numbers in the same step as the defaults, or an earlier one. A later step can't retroactively fix numbers that are already assigned.

Commas, plus, and transients

CodeReviewTests.cs
await TestBiome
    .Start(out _)
    .AddCodeRepository(out _)
    .AddPullRequests(
        out var prs,
        PullRequests.Closed,
        PullRequests.Closed + PullRequests.Merged
    ) // one PR with both traits
    .BuildAsync();
// prs.Length == 2; only prs[1].Merged is true

Transient inputs stay inspectable but are not saved:

CodeReviewTests.cs
var store = new RecordingStore();
var biome = TestBiome.Start(out _, store);

biome.AddCodeRepository(out _).AddPullRequests(out var prs, 2);
// marks the most recent Add step, whichever builder you call it on
await biome.Transient().BuildAsync();

// biome.All<PullRequestRecord>().Count == 2, but the store holds none

Time

A shared FakeTimeProvider is read live, and every source of now is rounded to 1 µs:

CodeReviewTests.cs
var instant = new DateTimeOffset(2026, 10, 11, 12, 0, 0, TimeSpan.Zero).AddTicks(
    17
);
var time = new FakeTimeProvider(instant);
var biome = TestBiome.Start(out _, time: time);

biome.AddCodeRepository(out var first);
time.Advance(TimeSpan.FromHours(1));
await biome
    .AddOne(
        Blueprints.Repository,
        out var second,
        (repo, ctx) => repo.CreatedAt = ctx.Ago(TimeSpan.FromMinutes(5))
    )
    .AddPullRequest(out _)
    .AddCodeReview(out _)
    .AddActiveLock(out var activeLock)
    .BuildAsync();

// first.CreatedAt   == instant.AddTicks(-7)                      (rounded to 1 µs)
// second.CreatedAt  == instant.AddTicks(-7).AddMinutes(55)
// activeLock.ExpiresAt == instant.AddTicks(-7).AddMinutes(65)
Ago and FromNow derive from the rounded now, so a value read back from Postgres equals the value in memory.