Parent/child chains and .Under
A child blueprint takes its parent
A review needs to belong to the pull request this test created. Looking up the most recent pull request becomes unreliable once the test adds siblings. A child blueprint receives its parent directly, keeping the relationship clear:
public static partial class Blueprints
{
public static readonly ChildBlueprint<
ZeeqSeed,
PullRequestRecord,
CodeReviewRecord
> Review = Blueprint.ChildOf<ZeeqSeed, PullRequestRecord, CodeReviewRecord>(
(pr, ctx) =>
new()
{
Id = ctx.NewId("review"),
OrganizationId = pr.OrganizationId,
PullRequestRecordId = pr.Id,
RepositoryId = ctx.Ancestor<CodeRepository>().Id, // grandparent
Status = CodeReviewStatus.Pending,
CreatedAtUtc = ctx.Now,
}
);
public static readonly ChildBlueprint<
ZeeqSeed,
CodeReviewRecord,
ActiveCodeReviewLock
> ActiveLock = Blueprint.ChildOf<
ZeeqSeed,
CodeReviewRecord,
ActiveCodeReviewLock
>(
(review, ctx) =>
new()
{
OrganizationId = review.OrganizationId,
RepositoryId = review.RepositoryId,
PullRequestRecordId = review.PullRequestRecordId,
CodeReviewRecordId = review.Id,
AcquiredAtUtc = ctx.Now,
ExpiresAtUtc = ctx.FromNow(TimeSpan.FromMinutes(5)),
}
);
}
The typed parent is the lambda's first argument. Reach further up with ctx.Ancestor<T>(), which walks the recorded parent links and throws a clear error if no ancestor of that type exists.
Every Add re-anchors
The builder always has a current anchor: the seed, at the start, then the output of the latest Add. A child Add attaches to the anchor, so a chain reads top to bottom.
await TestBiome
.Start(out _)
.AddOne(Blueprints.Repository, out var repo)
.AddOne(Blueprints.PullRequest, out var pr) // parent: repo
.AddOne(Blueprints.Review, out var review) // parent: pr
.AddOne(Blueprints.ActiveLock, out var activeLock) // parent: review
.BuildAsync();
// Foreign keys wired by hand, in dependency order, with timestamps repeated.
var now = DateTimeOffset.UtcNow;
now = new DateTimeOffset(now.Ticks - now.Ticks % 10, TimeSpan.Zero);
var repo = new CodeRepository
{
Id = $"repo_{Guid.CreateVersion7():N}",
OrganizationId = seed.Organization.Id,
OwnerQualifiedName = "zeeq-ai/example",
Enabled = true,
CreatedAtUtc = now,
};
var pr = new PullRequestRecord
{
Id = $"pr_{Guid.CreateVersion7():N}",
OrganizationId = repo.OrganizationId,
RepositoryId = repo.Id,
PullRequestNumber = 1,
Title = "Pull request 1",
Branch = "feature/1",
State = PullRequestState.Open,
CreatedAtUtc = now,
};
var review = new CodeReviewRecord
{
Id = $"review_{Guid.CreateVersion7():N}",
OrganizationId = pr.OrganizationId,
PullRequestRecordId = pr.Id,
RepositoryId = repo.Id,
Status = CodeReviewStatus.Pending,
CreatedAtUtc = now,
};
var activeLock = new ActiveCodeReviewLock
{
OrganizationId = review.OrganizationId,
RepositoryId = review.RepositoryId,
PullRequestRecordId = review.PullRequestRecordId,
CodeReviewRecordId = review.Id,
AcquiredAtUtc = now,
ExpiresAtUtc = now.AddMinutes(5),
};
_db.AddRange(seed.Owner, seed.Organization, repo, pr, review, activeLock);
await _db.SaveChangesAsync();
.Under: siblings and other branches
A chain follows one branch at a time. To add a sibling or start another branch, return to a parent you already built with .Under(x). It adds no entities; the next Add attaches to x.
await TestBiome
.Start(out _)
.AddCodeRepository(out var repo)
.AddPullRequest(out var open)
.Under(repo) // back to the repository
.AddPullRequest(out var closed, PullRequests.Closed)
.AddCodeReview(out var review) // reviews `closed`
.BuildAsync();
.Under accepts a single entity or an array: .Under(repos) fans out across all of them.
await TestBiome
.Start(out _)
.AddCodeRepository(out var repo)
.AddPullRequest(out var open) // #1
.AddCodeReview(out var review) // reviews open
.Under(repo)
.AddPullRequest(out var closed, PullRequests.Closed) // #2, sibling of `open`
.Under(open)
.AddCodeReview(out var otherReview) // a second review of `open`
.BuildAsync();
.Under takes Add outputs and the seed rows you listed in SeedEntities (for example .Under(seed.Owner)). A hand-built object, or an untracked part of an aggregate, can't be a parent, and Biome throws ArgumentException naming the offending index.
Anchors and sibling blueprints
Siblings that share a parent are separate branches. The library's ProjectManagement tests attach checklist items and comments to the same cards by jumping back with .Under(cards):
await biome
.AddWorkspaces(out var workspaces, 2)
.AddProjects(out _, 2)
.AddBoards(out _, 1)
.AddColumns(out var columns, 2)
.AddCards(out var cards, 3)
.AddChecklistItems(out var checklist, 4)
// Comments attach to cards, so jump back from
// checklist leaves to the saved card output.
.Under(cards)
.AddComments(out var comments, 2)
.BuildAsync();
out var x when you intend to .Under(x) later. Keep the starting builder (var biome = TestBiome.Start(...)) when you also want biome.All<T>() and biome.Last<T>() to inspect entities a helper created.Errors you may see
| Message | Cause |
|---|---|
A singular child AddOne requires exactly one parent | The previous step produced several (or zero) parents. Use the plural Add or .Under(repos[0]). |
No ancestor of type X exists… | A blueprint walks up to X, but the chain skipped X. |
Parent at index N … is not owned by this biome session | .Under was given an object Biome didn't build or select as seed. |
This biome session is faulted because an earlier Add failed | An earlier Add threw. Fix that error; the session cannot continue. |