Guides

Blueprints

One factory per entity type, as an immutable value.

Writing a valid entity often takes more properties than the test actually cares about. A blueprint keeps those routine choices in one factory, leaving the test free to describe its scenario. Declare it as a static readonly value with valid defaults. Choose among these forms based on the relationship you need:

KindFactoryUse for
Blueprint<TSeed, T>Blueprint.For<TSeed, T>(ctx => new() { … })A root entity
ChildBlueprint<TSeed, TParent, T>Blueprint.ChildOf<TSeed, TParent, T>((parent, ctx) => new() { … })An entity that belongs to a parent
InputBlueprint<TSeed, TInput, T>Blueprint.FromInput<TSeed, TInput, T>(() => new(), (input, ctx) => …)A root entity (or group of rows) built from a customizable input
ChildInputBlueprint<TSeed, TParent, TInput, T>Blueprint.ChildFromInput<TSeed, TParent, TInput, T>(() => new(), (parent, input, ctx) => …)A child built from a customizable input

The two input forms are for rows that tests cannot edit after construction, such as init-only properties. See Input blueprints and check that the package referenced by your project exposes the form you use.

A root blueprint

public static partial class Blueprints
{
    public static readonly Blueprint<ZeeqSeed, CodeRepository> Repository =
        Blueprint.For<ZeeqSeed, CodeRepository>(ctx =>
            new()
            {
                Id = ctx.NewId("repo"), // "repo_<uuidv7>"
                OrganizationId = ctx.Seed.Organization.Id,
                OwnerQualifiedName = "zeeq-ai/example",
                Enabled = true,
                CreatedAtUtc = ctx.Now, // never DateTimeOffset.UtcNow
            }
        );
}

The factory receives a context (ctx):

MemberUse
ctx.SeedThe baseline rows (organization, owner).
ctx.Now, ctx.Ago(t), ctx.FromNow(t)The session clock, already rounded.
ctx.NewId("prefix")prefix_<uuidv7> unless options override it.
ctx.IndexPosition in this Add. Cosmetic values only.
ctx.Next(seq)Advance a sequence manually. Prefer Derive(prop, seq).
ctx.Ancestor<T>()Nearest ancestor of type T. Throws a clear error if none.
ctx.All<T>(), ctx.Last<T>()Inspection only. Never for wiring relationships.
ctx.Parent, ctx.ParentIndexChild context only.

Factory rules

A useful factory makes ordinary entities easy to build without choosing the behavior under test. These conventions keep defaults predictable as the test suite grows:

  1. Valid and uninteresting. Every required column gets a default no test should care about.
  2. IDs come from ctx.NewId("prefix"). Time comes from ctx.Now / ctx.Ago / ctx.FromNow.
  3. Take the parent from the typed argument, and grandparents from ctx.Ancestor<T>().
  4. Leave derived properties unset, so a Derive can fill them.
  5. A property that tests vary must be settable, or come from an input that the factory reads.
The most recently created entity is not necessarily the intended parent. Siblings and .Under make that assumption unreliable. Use the typed parent for relationships, and keep ctx.Last<T>() and ctx.All<T>() for inspection.
Customizers assign properties, so a property a test varies needs a setter. With { get; init; } the compiler rejects the assignment (CS8852). Derive can still fill init properties. If the model must stay immutable, let tests customize an input blueprint instead of changing the model.

Blueprints are immutable values

Tests often need a recurring variation, such as a disabled repository. A variant lets you name that choice without changing the defaults other tests use. Every modifier returns a new blueprint:

Blueprints.cs
public static readonly Blueprint<ZeeqSeed, CodeRepository> Repository = /* … */;

// A variant: Repository itself is unchanged.
public static readonly Blueprint<ZeeqSeed, CodeRepository> DisabledRepository =
    Repository.With(Repositories.Disabled);
ModifierEffect
.With(trait)Adds a trait. Runs before call-site customizers, so the call site still wins.
.Derive(e => e.Prop, (e, ctx) => value)Fill-only assignment after customizers.
.Derive(e => e.Prop, sequence)Fill-only assignment from a sequence.
.AsTransient()The output is built and returned, but never saved. See Transients.
Static fields initialize top to bottom, so a variant needs its base to be initialized first. Declare them in that order in the same file; ordering across partial files is unspecified. Put a sequence above the blueprint that uses it for the same reason.

Adding blueprints to a chain

CodeRepositoryTests.cs
await TestBiome
    .Start(out var seed)
    .AddOne(Blueprints.Repository, out var repo)
    .BuildAsync();

In real tests you rarely call AddOne directly. Mark the blueprint catalog [BiomeGraft] and the generator supplies methods such as .AddCodeRepository(out var repo). See Generated grafts and scenario helpers.