Examples
Board
Board, columns and cards, with rank ordering from a per-parent string sequence.
Card ordering belongs to a column. Adding more cards later should continue that column's ranks without changing another column's sequence. This example uses string ranks to show that sequences are useful beyond integer IDs.
A kanban board: Board → Column → Card. Cards are ordered by a string rank ("a", "aa", "aaa"…, in the spirit of LexoRank) that restarts for every column. Source: tests/Zeeq.Biome.Tests/Board.
The blueprints
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 Blueprint<BoardSeed, Board> Board = Blueprint.For<
BoardSeed,
Board
>(_ => new());
internal static readonly ChildBlueprint<BoardSeed, Board, Column> Column =
Blueprint.ChildOf<BoardSeed, Board, Column>(
(board, _) => new() { Board = board }
);
// Rank stays null until Derive; empty string would count as explicitly set.
internal static readonly ChildBlueprint<BoardSeed, Column, Card> Card =
Blueprint
.ChildOf<BoardSeed, Column, Card>(
(column, _) => new() { Column = column }
)
.Derive(c => c.Rank, Ranks);
Rank is declared string Rank { get; set; } = null!; and is null until the Derive runs. A default of "" would count as explicitly set, and the sequence would never fill it.Ranks resume per column, and .Under revisits
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();
var board = new Board();
var columns = new[]
{
new Column { Board = board },
new Column { Board = board },
};
// Ranks are generated and tracked by hand, one counter per column.
var ranks = new Dictionary<Column, string>();
string NextRank(Column column)
{
var rank = ranks.GetValueOrDefault(column, "a");
ranks[column] = rank + "a";
return rank;
}
var cards = new[]
{
new Card { Column = columns[0], Rank = NextRank(columns[0]) },
new Card { Column = columns[0], Rank = NextRank(columns[0]) },
new Card { Column = columns[1], Rank = NextRank(columns[1]) },
new Card { Column = columns[1], Rank = NextRank(columns[1]) },
};
var next = new Card { Column = columns[0], Rank = NextRank(columns[0]) };
var sibling = new Column { Board = board };
var siblingCard = new Card { Column = sibling, Rank = NextRank(sibling) };
db.AddRange(board, columns[0], columns[1], sibling);
db.AddRange(cards);
db.AddRange(next, siblingCard);
await db.SaveChangesAsync();
Assertions in the real test: cards[0].Rank == "a", cards[1].Rank == "aa", cards[2].Rank == "a", next.Rank == "aaa", siblingCard.Rank == "a".
Scope is declared on the sequence, not chosen by
.Under or a customizer. PerParent keys state by the column's reference identity, so two columns never share a counter, and .Under(columns[0]) always resumes the right one.An explicit rank is reserved only in its own column
BoardTests.cs
await TestBiome
.Start(out _)
.AddBoard(out _)
.AddColumns(out var columns, 2)
.Under(columns[0])
.AddCards(out var first, _ => { }, c => c.Rank = "a")
.Under(columns[1])
.AddCard(out var other)
.BuildAsync();
// first[0].Rank == "aa" (the "a" explicit value is skipped)
// other.Rank == "a" (the second column is untouched)