Guides

Sequences

Numbers and ordering that continue across Add calls, per session, parent or ancestor.

Why not index + 1

Numbering from ctx.Index + 1 looks reasonable while a test has one Add. Add another batch, though, and the index starts over. In zeeq-app tests, that produced duplicate "PR #1" rows and a unique-constraint failure.

A sequence addresses that by keeping its position across Adds. Declare a start value and a next function once, then choose where the counter should be shared. Its state lives in each session, so one test's numbering does not affect another.

Declare, scope, derive

Blueprints.cs
public static partial class Blueprints
{
    public static readonly Sequence<int> PullRequestNumbers = Sequence
        .Of(1, n => n + 1)
        .Per<CodeRepository>(); // numbering restarts per repository

    public static readonly ChildBlueprint<
        ZeeqSeed,
        CodeRepository,
        PullRequestRecord
    > PullRequest = Blueprint
        .ChildOf<ZeeqSeed, CodeRepository, PullRequestRecord>(
            (repo, ctx) =>
                new()
                {
                    Id = ctx.NewId("pr"),
                    OrganizationId = repo.OrganizationId,
                    RepositoryId = repo.Id,
                    State = PullRequestState.Open,
                    CreatedAtUtc = ctx.Now,
                }
        )
        .Derive(pr => pr.PullRequestNumber, PullRequestNumbers)
        // runs after the number
        .Derive(pr => pr.Title, (pr, _) => $"Pull request {pr.PullRequestNumber}")
        .Derive(pr => pr.Branch, (pr, _) => $"feature/{pr.PullRequestNumber}")
        // globally unique
        .Derive(pr => pr.GitHubNodeId, (_, ctx) => ctx.NewId("PR"));
}

Assign it with the fill-only Derive(property, sequence).

Scope: one counter per…

ScopeOne counter perExample
.PerSession() (default)testanything unique across the whole test
.PerParent()immediate parentcard rank within a column
.Per<TAncestor>()nearest ancestor of that typePR number within a repository

State is keyed by (sequence instance, scope entity identity), not by blueprint and not by anchor position. So blueprint variants and .Under(repo) resume the same progression.

await TestBiome
    .Start(out _)
    .AddCodeRepositories(out var repos, 2)
    .AddPullRequests(out var first, 2) // 1, 2 | 1, 2
    .Under(repos[0])
    .AddPullRequest(out var third) // 3: resumes repos[0]
    .Under(repos[0])
    .AddPullRequests(
        out var mixed,
        _ => { }, // 5 (4 is taken below)
        pr => pr.PullRequestNumber = 4
    ) // explicit: kept, reserved
    .BuildAsync();

Explicit values win

Sometimes the number itself matters to the test. The sequence needs to leave that value available while assigning the surrounding defaults. Biome reserves explicit values before filling the unset ones:

  • Explicit values never move the sequence. And the sequence skips any value already set explicitly in that scope, in this step or an earlier one.
  • Within one step, Biome first reserves every explicit value, then fills the rest. The test below shows [2, 1, 3, 4]: the first unset entry takes 2 because 1 and 3 are explicit.
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 numbers: 2, 1, 3, 4; next.Number: 5

An explicit high value doesn't advance the counter either:

CodeReviewTests.cs
await TestBiome
    .Start(out _)
    .AddCodeRepository(out var repo)
    .AddPullRequest(
        out var explicitPr,
        pr =>
        {
            pr.Number = 99;
            pr.Title = "Regression";
        }
    )
    .Under(repo)
    // Number == 1; the explicit 99 did not move the sequence
    .AddPullRequest(out var generated)
    .BuildAsync();
Reservation applies to values Biome already knows about. If a later step supplies a number used by an earlier one, the earlier assignment stays as it was. Set a required number in the same step as the defaults, or before them, to avoid that collision.

Prefer Derive(prop, sequence) over ctx.Next

The factory runs before customizers, so ctx.Next(sequence) cannot see values the test will supply. That can create a collision. Prefer .Derive(property, sequence), which reserves explicit values before assigning defaults.

Rules of thumb

  • Declare the sequence above the blueprints that use it. In a static class, fields initialize top to bottom. Keep a base blueprint and its variants in the same file; across partial files the initialization order is unspecified.
  • Derives run in declaration order, so derive Title after the number it depends on.
  • Variants made with .With share their base blueprint's sequence.
  • Globally unique values (node IDs, slugs, emails) use a PerSession sequence or ctx.NewId, never PerParent / Per<T>: two parents would both produce PR_1.
  • The next function may be any type. Ranks work too, as in the Board tests below.
  • A next function that cycles through reserved values throws, rather than hanging.

Non-numeric sequences: ranks

Board/Domain.cs
// PerParent resumes for the same column reference;
// it restarts for a different column.
internal static readonly Sequence<string> Ranks = Sequence
    .Of("a", rank => rank + "a")
    .PerParent();

internal static readonly ChildBlueprint<BoardSeed, Column, Card> Card =
    Blueprint
        .ChildOf<BoardSeed, Column, Card>(
            (column, _) => new() { Column = column }
        )
        .Derive(c => c.Rank, Ranks);
Rank here is null until the Derive runs. An empty string would count as explicitly set and the sequence would never fill it.
BoardTests.cs
await TestBiome
    .Start(out _)
    .AddBoard(out var board)
    .AddColumns(out var columns, 2)
    .AddCards(out var cards, 2) // "a", "aa" | "a", "aa"
    .Under(columns[0])
    .AddCard(out var next) // "aaa": resumes only this column
    .Under(board)
    .AddColumn(out var sibling)
    .AddCard(out var siblingCard) // "a": a new column restarts
    .BuildAsync();