Custom seed
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
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
var seed = new CustomSeed("actor", 42);
var notes = new[]
{
new Note
{
Id = $"note_{Guid.CreateVersion7():N}",
Number = 1,
CreatedBy = seed.Actor,
},
new Note
{
Id = $"note_{Guid.CreateVersion7():N}",
Number = 2,
CreatedBy = "custom",
},
};
var audit = new Audit(seed.Actor); // the audit row must be remembered in every test
db.AddRange(notes);
db.Add(audit);
await db.SaveChangesAsync();
Options freeze on first use
Every configuration mutator throws once a session has started, so a running test can't change shared options:
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:
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
}
);
Biome.Start(seed, options) works without a TestBiome wrapper, handy for a custom seed built inline.Custom IDs and session sequences
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)