Examples

Custom seed

A plain record seed, an interface default, save hooks, frozen options and parallel safety.

Sometimes the shared context is an actor and tenant, rather than rows to save. This example uses a plain record seed and shows how interface defaults and save hooks carry those conventions into the graph.

The seed doesn't have to be your app's entities. Here it is a plain record, CustomSeed(string Actor, int Tenant), and ownership comes from a global default on an interface. Source: tests/Zeeq.Biome.Tests/CustomSeed.

Seed, interface default, blueprint

CustomSeed/Domain.cs
internal sealed record CustomSeed(string Actor, int Tenant);

internal interface IHasCreatedBy
{
    string? CreatedBy { get; set; }
}

internal sealed class Note : IHasCreatedBy
{
    public string? CreatedBy { get; set; }
    public string? Id { get; set; }
    public int Number { get; set; }
    public string Text { get; set; } = "note";
}

// PerSession spans every root Add; each test session has independent progression.
internal static readonly Sequence<int> Numbers = Sequence
    .Of(1, n => n + 1)
    .PerSession();

// Set the ID in the factory, leave ownership to the
// interface default, and derive numbering.
internal static readonly Blueprint<CustomSeed, Note> Note = Blueprint
    .For<CustomSeed, Note>(ctx => new() { Id = ctx.NewId("note") })
    .Derive(n => n.Number, Numbers);

// Match an interface: ownership defaults apply across
// entity implementations and remain fill-only.
internal static BiomeOptions<CustomSeed> NewOptions() =>
    new BiomeOptions<CustomSeed>().Default<IHasCreatedBy, string?>(
        n => n.CreatedBy,
        (_, ctx) => ctx.Seed.Actor
    );

Defaults and hooks together

The test registers an audit row in BeforeSave, then records the order in which things happen. An explicit CreatedBy still wins over the interface default.

var events = new List<string>();
var store = new RecordingStore { OnSave = () => events.Add("save") };
var options = TestBiome
    .NewOptions()
    .BeforeSave(
        (ctx, _) =>
        {
            events.Add("before");
            ctx.Entities.Add(new Audit(ctx.Seed.Actor));
            return ValueTask.CompletedTask;
        }
    )
    .AfterSave(
        (ctx, _) =>
        {
            events.Add("after");
            return ValueTask.CompletedTask;
        }
    );
var biome = TestBiome.Start(out var seed, store, options);

await biome
    .AddNotes(out var notes, _ => { }, n => n.CreatedBy = "custom")
    .BuildAsync();

// notes[0].CreatedBy == seed.Actor; notes[1].CreatedBy == "custom"
// events: "before,save,after"; the Audit row reached the store

Options freeze on first use

Every configuration mutator throws once a session has started, so a running test can't change shared options:

CustomSeedTests.cs
var options = TestBiome.NewOptions();
TestBiome.Start(out _, options: options); // first use freezes the options

Assert.Throws<InvalidOperationException>(() => options.Ids(p => p));
Assert.Throws<InvalidOperationException>(() =>
    options.TimePrecision(TimeSpan.Zero)
);
Assert.Throws<InvalidOperationException>(() => options.SeedEntities(_ => []));
// …Default, BeforeSave and AfterSave throw the same way

Parallel-safe by construction

Blueprints, sequences and options are immutable and shared. Sessions hold all the mutable state, so 32 concurrent scenarios don't interfere:

CustomSeedTests.cs
var options = TestBiome.NewOptions();

await Parallel.ForEachAsync(
    Enumerable.Range(0, 32),
    new ParallelOptions { MaxDegreeOfParallelism = 8 },
    async (tenant, ct) =>
    {
        await Task.Yield();
        var seed = new CustomSeed($"actor-{tenant}", tenant);
        var biome = Biome.Start(seed, options);
        await biome.AddNotes(out var notes, 3).BuildAsync(ct);
        // every session starts at Number 1, keeps its
        // own actor, and gets distinct IDs
    }
);
This also shows that Biome.Start(seed, options) works without a TestBiome wrapper, handy for a custom seed built inline.

Custom IDs and session sequences

CustomSeedTests.cs
var options = TestBiome.NewOptions().Ids(prefix => $"{prefix}_known");

await TestBiome
    .Start(out _, options: options)
    .AddNote(out var first)
    .AddNotes(out var rest, 2)
    .BuildAsync();

// first.Number == 1; rest[0].Number == 2
// (a PerSession sequence continues across Adds)
// first.Id == "note_known"                 (the ID override reached the factory)