Blueprints
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:
| Kind | Factory | Use 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
}
);
}
// A factory method in a helper class: no sequences, no clock, no parent wiring,
// and every test must remember to pass the right organization.
public static class TestData
{
public static CodeRepository Repository(
Organization organization,
string? ownerQualifiedName = null,
bool enabled = true,
DateTimeOffset? createdAtUtc = null
)
{
var now = DateTimeOffset.UtcNow;
return new CodeRepository
{
Id = $"repo_{Guid.CreateVersion7():N}",
OrganizationId = organization.Id,
OwnerQualifiedName = ownerQualifiedName ?? "zeeq-ai/example",
Enabled = enabled,
CreatedAtUtc =
createdAtUtc
?? new DateTimeOffset(now.Ticks - now.Ticks % 10, TimeSpan.Zero),
};
}
}
The factory receives a context (ctx):
| Member | Use |
|---|---|
ctx.Seed | The 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.Index | Position 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.ParentIndex | Child 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:
- Valid and uninteresting. Every required column gets a default no test should care about.
- IDs come from
ctx.NewId("prefix"). Time comes fromctx.Now/ctx.Ago/ctx.FromNow. - Take the parent from the typed argument, and grandparents from
ctx.Ancestor<T>(). - Leave derived properties unset, so a
Derivecan fill them. - A property that tests vary must be settable, or come from an input that the factory reads.
.Under make that assumption unreliable. Use the typed parent for relationships, and keep ctx.Last<T>() and ctx.All<T>() for inspection.{ 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:
public static readonly Blueprint<ZeeqSeed, CodeRepository> Repository = /* … */;
// A variant: Repository itself is unchanged.
public static readonly Blueprint<ZeeqSeed, CodeRepository> DisabledRepository =
Repository.With(Repositories.Disabled);
| Modifier | Effect |
|---|---|
.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. |
partial files is unspecified. Put a sequence above the blueprint that uses it for the same reason.Adding blueprints to a chain
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.