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

Parent-aware customizers

A child customizer in the two-parameter form receives a ChildContext:

MemberMeaning
ctx.ParentThis child's parent, typed.
ctx.ParentIndexThe parent's position in the anchor array.
ctx.IndexThe 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.