# Customizing

> How customizers and defaults work together, and why their order matters.

## The one rule

A default is useful only if the test can choose a different value. For example, changing an invitation's creation time should also change its default expiry, without replacing an expiry the test supplied. That is why Biome runs derived defaults after customizers.

<note>

**Customizers choose the values; derived and global defaults fill what is still unset.** More specific defaults run before global ones.

</note>

For every entity in an Add step, Biome runs the same pipeline, in this order:

1. **Factory** creates the entity. IDs are set here with `ctx.NewId`.
2. **Traits** (`.With`) run, in declaration order.
3. **Call-site customizers** run.
4. **Derives** run, fill-only and in declaration order. Sequences first reserve every explicit value in the step, then fill the rest.
5. **Global defaults** (`options.Default`) run, fill-only.
6. The entity is recorded, with its parent link and step.

There is no `OnCreated` hook that can overwrite a value. Steps 4 and 5 only fill properties that are still unset.

An [input blueprint](/guides/input-blueprints) runs steps 2 to 4 on its **input** object (traits and customizers for every input in the step first, then each input's `Derive`s followed by the factory), and then applies global defaults to the built entity (step 5).

## Set only the property under test

<code-group>

```csharp [With Biome]
await TestBiome
    .Start(out _)
    .AddOne(Blueprints.Repository, out var disabled, r => r.Enabled = false)
    .Add(
        Blueprints.Repository,
        out var pair,
        r => r.OwnerQualifiedName = "zeeq-ai/api",
        r => r.OwnerQualifiedName = "zeeq-ai/web"
    )
    .BuildAsync();
```

<code-collapse label="Without Biome" name="setup">

```csharp
// Every property has to be spelled out, even the ones the test ignores.
var now = DateTimeOffset.UtcNow;
now = new DateTimeOffset(now.Ticks - now.Ticks % 10, TimeSpan.Zero);

CodeRepository NewRepository(string name, bool enabled) =>
    new()
    {
        Id = $"repo_{Guid.CreateVersion7():N}",
        OrganizationId = seed.Organization.Id,
        OwnerQualifiedName = name,
        Enabled = enabled,
        CreatedAtUtc = now,
    };

var disabled = NewRepository("zeeq-ai/example", enabled: false);
var api = NewRepository("zeeq-ai/api", enabled: true);
var web = NewRepository("zeeq-ai/web", enabled: true);
_db.AddRange(seed.Owner, seed.Organization, disabled, api, web);
await _db.SaveChangesAsync();
```

</code-collapse>
</code-group>

<tip>

A repeated default looks just as deliberate as a value chosen for the assertion. Set only the properties the test depends on, so the reader can see why this scenario exists.

</tip>

## Traits: comma makes a new entity, plus stacks

If several tests make the same choice, give it a name. A **trait** is an `Action<T>` value that you can pass anywhere a customizer goes.

```csharp [Repositories.cs]
public static class Repositories
{
    public static readonly Action<CodeRepository> Disabled = r => r.Enabled = false;
    public static readonly Action<CodeRepository> Hidden = r =>
        r.VisibleInLibraryPicker = false;
}
```

```csharp [Traits in a chain]
await TestBiome
    .Start(out _)
    .Add(
        Blueprints.Repository,
        out var repos,
        Repositories.Disabled, // repos[0]: disabled
        Repositories.Disabled + Repositories.Hidden
    ) // repos[1]: disabled and hidden
    .BuildAsync();
```

<note>

Choose `+` when one entity needs both traits, and a comma when the test needs separate entities. For example, `PullRequests.Closed + PullRequests.Merged` creates one pull request with both traits; `PullRequests.Closed, PullRequests.Merged` creates two.

</note>

Bake a trait into a **variant** with `.With`. The base blueprint is not changed:

```csharp [Blueprints.cs]
public static readonly Blueprint<ZeeqSeed, CodeRepository> DisabledRepository =
    Repository.With(Repositories.Disabled);
```

## Context-aware customizers

A customizer that needs the context takes **two parameters**:

```csharp [Relative time]
await TestBiome
    .Start(out _)
    .AddCodeRepository(out _)
    .AddOne(
        Blueprints.Repository,
        out var old,
        (r, ctx) => r.CreatedAtUtc = ctx.Ago(TimeSpan.FromDays(30))
    )
    .BuildAsync();
```

<warning>

C# cannot mix one-parameter and two-parameter lambdas in a single call. When you mix them, write **all** of them in the two-parameter form. Generated grafts accept `Action<T>` (or `Action<TInput>` for an input blueprint) unless the app has added a [context overload](/guides/scenario-helpers-and-sugar#context-overloads-when-a-test-needs-them) for that type; otherwise a context-aware customizer goes through the core `Add` / `AddOne`.

</warning>

## Derived defaults are fill-only

Some defaults depend on the entity's final values: a title may use its number, or expiry may use its creation time. `.Derive` runs after traits and customizers for that reason. It assigns a property only if it still has its default value; a non-default value supplied by the test is kept.

```csharp [Blueprints.cs]
public static readonly Blueprint<
    ZeeqSeed,
    OrganizationMembership
> PendingInvitation = Blueprint
    .For<ZeeqSeed, OrganizationMembership>(ctx =>
        new()
        {
            Id = ctx.NewId("mem"),
            OrganizationId = ctx.Seed.Organization.Id,
            Status = MembershipStatus.Pending,
            InvitedEmail = "invited@example.test",
            CreatedByUserId = ctx.Seed.Owner.Id,
            CreatedAtUtc = ctx.Now,
        }
    )
    .Derive(m => m.ExpiresAtUtc, (m, _) => m.CreatedAtUtc.AddDays(7));
```

```csharp [Membership tests]
var created = new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
await TestBiome
    .Start(out _)
    .Add(
        Blueprints.PendingInvitation,
        out var invitations,
        i => i.CreatedAtUtc = created, // ExpiresAtUtc derived → Jan 8
        i => i.ExpiresAtUtc = created.AddDays(-1)
    ) // explicit → kept
    .BuildAsync();
```

The same pattern from the library's own tests (`Membership/Domain.cs`): expiry derives **after** the customizer has changed the creation time.

```csharp [Membership/Domain.cs]
internal static readonly Blueprint<
    MembershipSeed,
    PendingInvitation
> Invitation = Blueprint
    .For<MembershipSeed, PendingInvitation>(ctx =>
        new() { CreatedAt = ctx.Now }
    )
    .Derive(i => i.ExpiresAt, (i, _) => i.CreatedAt.AddDays(7));
```

### What counts as "unset"

<table>
<thead>
  <tr>
    <th>
      Type
    </th>
    
    <th>
      Unset means
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      reference types, <code>
        T?
      </code>
    </td>
    
    <td>
      <code>
        null
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      numbers, <code>
        bool
      </code>
    </td>
    
    <td>
      <code>
        0
      </code>
      
      , <code>
        false
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        DateTimeOffset
      </code>
    </td>
    
    <td>
      <code>
        default(DateTimeOffset)
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        null
      </code>
      
       only. <strong>
        <code>
          ""
        </code>
        
         counts as set.
      </strong>
    </td>
  </tr>
</tbody>
</table>

<caution>

Biome uses the type's default value to recognize an unset property. For a string that means `null`, so `""` is kept as an explicit value. Leave derived strings `null` in the factory. If a test needs to retain `null`, use a base blueprint without that `Derive`.

</caution>

For nullable value types the distinction holds too: `int?` `null` fills, but `0` survives.

## Derives run in order

A title that includes a sequence number needs the final number first. `Derive`s run in declaration order, so place the title derivation after the sequence:

```csharp [Blueprints.cs]
.Derive(pr => pr.PullRequestNumber, PullRequestNumbers)
    .Derive(pr => pr.Title, (pr, _) => $"Pull request {pr.PullRequestNumber}")
    .Derive(pr => pr.Branch, (pr, _) => $"feature/{pr.PullRequestNumber}")
```

## Properties must be settable

<warning>

A customizer cannot assign an `init` property (`CS8852`), so a property that a test varies on the entity itself needs a `set`. If the model must stay `init` or `required`, keep production code as it is and customize an **input** instead: see [Input blueprints](/guides/input-blueprints). A blueprint factory method with optional named parameters also works, but it creates a new blueprint on every call.

</warning>

The property you select in `Derive` or `options.Default` must be a **directly writable instance property**, like `e => e.Name`. Anything else throws `ArgumentException` at declaration time. `init` properties are accepted here, because `Derive` and `Default` assign through a compiled setter.
