Guides
Fan-out
Child counts and customizers multiply per parent, and come back as one flat array.
Per parent, in parent order
A test may need the same shape under several parents: three pull requests in each of two repositories, for example. Instead of writing a loop, add children while the anchor holds those parents. Counts and customizers apply to each parent, and the result is one flat array in parent order.
FanOutTests.cs
await TestBiome
.Start(out _)
.Add(Blueprints.Repository, out var repos, 2)
// 3 per repo → 6: prs[0..2] → repos[0], prs[3..5] → repos[1]
.Add(Blueprints.PullRequest, out var prs, 3)
.BuildAsync();
Two parents × three customizers is six children, not three. The count is per parent.
Customizers per parent
Each customizer creates one entity per parent. From the library's Membership tests: two users, two alias customizers, four aliases.
MembershipTests.cs
await TestBiome
.Start(out _)
// Create user parents so alias customizers can fan out under each one.
.AddUsers(out var users, 2)
// Apply each customization PER user; Derive normalizes only unset values.
.AddAliases(
out var aliases,
a => a.Value = "Alice",
a =>
{
a.Value = "Bob";
a.NormalizedValue = "manual";
}
)
.BuildAsync();
// aliases.Length == 4; aliases[0].User == users[0]; aliases[2].User == users[1]
// aliases[0].NormalizedValue == "ALICE" (derived)
// aliases[3].NormalizedValue == "manual" (kept)
await TestBiome
.Start(out _)
.AddCodeRepositories(out var repos, 2)
// 2 per repo → 4
.AddPullRequests(out var prs, PullRequests.Draft, PullRequests.Merged)
.BuildAsync();
// prs[0], prs[1] → repos[0]: #1 draft, #2 merged;
// prs[2], prs[3] → repos[1]: #1, #2
// The loops, numbering and foreign keys are yours to get right.
var repos = new[] { NewRepository(), NewRepository() };
var prs = new List<PullRequestRecord>();
foreach (var repo in repos)
{
var number = 1; // restart per repository, by hand
foreach (
var configure in new Action<PullRequestRecord>[]
{
p => p.IsDraft = true,
p => p.State = PullRequestState.Merged,
}
)
{
// builds Id, FK, title, branch, timestamps
var pr = NewPullRequest(repo, number++);
configure(pr);
prs.Add(pr);
}
}
_db.AddRange(seed.Owner, seed.Organization);
_db.AddRange(repos);
_db.AddRange(prs);
await _db.SaveChangesAsync();
Parent-aware customizers
A child customizer in the two-parameter form receives a ChildContext:
| Member | Meaning |
|---|---|
ctx.Parent | This child's parent, typed. |
ctx.ParentIndex | The parent's position in the anchor array. |
ctx.Index | The child's position under that parent. |
Parent-aware
await TestBiome
.Start(out _)
.AddUsers(
out var users,
u => u.DisplayName = "Ada",
u => u.DisplayName = "Grace"
)
.Add(
Blueprints.UserAlias,
out var aliases,
(alias, ctx) => alias.DisplayValue = $"@{ctx.Parent.DisplayName}"
)
.BuildAsync();
// aliases: "@Ada" → "ada", "@Grace" → "grace"
An index describes position in this Add, so it is useful for a display label. It restarts on the next call and cannot guarantee uniqueness. Use sequences for numbering and ordering, or
ctx.NewId for IDs.A base-context trait works for children too
A trait written against BiomeContext<TSeed> is assignable to a child's ChildContext form, so you can write it once. The library's tests exercise this:
CodeReviewTests.cs
var positions = new List<(int Parent, int Child)>();
Action<PullRequestRecord, BiomeContext<CodeReviewSeed>> title = (pr, ctx) =>
pr.Title = ctx.Seed.Owner.Id;
await TestBiome
.Start(out _)
.AddCodeRepositories(out var repos, 2)
.Add(
Blueprints.PullRequest,
out var prs,
title,
(pr, ctx) =>
{
positions.Add((ctx.ParentIndex, ctx.Index));
pr.Title = ctx.Parent.Name;
}
)
.BuildAsync();
// positions == [(0, 1), (1, 1)]: the second customizer
// ran once per parent (ParentIndex, Index)
Singular child adds need one parent
A singular child Add needs one parent so its
out value can identify one child. After AddCodeRepositories(out var repos, 2), use the plural Add for both repositories or select one with .Under(repos[0]). A singular child Add with zero or multiple parents throws.