Skip to content

Database seeding

Mirko Da Corte edited this page Aug 25, 2026 · 11 revisions

Seeding initializes a database context once — to create default data, an admin account, initial configuration. Scrinium tracks whether a context has been seeded and skips it on subsequent starts, and serializes seeding across application instances — so a context is seeded once however many instances start together.

Override SeedAsync

Put your initialization in a SeedAsync override on the context. Use its repositories as usual:

public class SampleDbContext : DbContext, ISampleDbContext
{
    public IRepository<Cat, string> Cats { get; } = new Repository<Cat, string>("cats");

    protected override async Task SeedAsync()
    {
        await Cats.CreateAsync(new Cat("Adam", new DateTime(2020, 1, 1)));
        await SaveChangesAsync();
    }
    // ...
}

The default SeedAsync does nothing, so override it only when you have data to seed.

Trigger it at startup

Call SeedDbContexts() on the built app. It resolves every registered context in a dedicated scope, gives each one its own execution context, and runs their seeds in parallel, each only if not already seeded:

var app = builder.Build();
// ... middleware ...
app.SeedDbContexts();
app.Run();

You can also trigger a single context directly with SeedIfNeededAsync().

Once-only semantics

  • SeedIfNeededAsync() checks IsSeeded, claims the db context lock, then re-checks and runs SeedAsync under exclusive access, first executing a full db-context migration (a new operation — on a fresh database this is what builds the indexes). On success it marks the context seeded (cached on the engine as IsSeededCache) and logs a SeedOperation in the _db_ops collection.
  • Three things keep it once-only, at three different scopes: exclusive access serializes the flows of this process, the db context lock serializes the application instances against the same database, and the persisted SeedOperation — what IsSeeded reads — prevents reseeding across restarts.
  • On a read-only context, SeedIfNeededAsync logs and returns false without touching the database: its lifecycle belongs to the owner application.

Because it runs under exclusive access, every other flow of this context — in this process — is locked out of its collections while seeding proceeds — keep seed logic focused.

Waiting for another instance

While another owner holds the db context lock — another instance seeding the same database, or a migration, or a diagnostic dry runSeedIfNeededAsync waits, retrying every 5 seconds (more often when its wait timeout is under 20 seconds) and re-reading the seeding state from the database between attempts. It returns false without seeding as soon as a re-read finds the context seeded; a lock freed by a migration or a dry run instead lets the wait claim it and seed.

The wait is bounded, since the caller blocks on it: when the timeout elapses with the lock still held, seeding fails with a ScriniumDbSeedingException. Its default is the lease duration of that call (ResourceLock.DefaultLeaseDuration, 10 minutes, unless you pass another). So with every instance on the same lease duration, a dead owner's lease always expires inside the wait, and only an owner still alive, working longer than the wait, fails your seeding.

The wait and the lease are both arguments — on a single context:

await db.SeedIfNeededAsync(
    lockWaitTimeout: TimeSpan.FromMinutes(30),      // a long migration may be running elsewhere
    lockLeaseDuration: TimeSpan.FromMinutes(20));   // how long a crash here blocks the others

and on the startup call, which forwards them to every context it seeds:

app.SeedDbContexts(lockWaitTimeout: TimeSpan.FromMinutes(30));

Note. Raise lockWaitTimeout when your seeds are long, or when a startup can coincide with a migration: it must cover the whole work of the other owner, not just its lease.

Tip. Seeding runs in its own scope. If your application raises its own domain events on create/save and you don't want them firing during seed, disable that dispatch inside SeedAsync — Scrinium itself has no event facility to turn off. Seed logic runs once, so it needn't be idempotent.


Next: Migrations for evolving existing data, or Startup and configuration for where SeedDbContexts fits in the pipeline.

Clone this wiki locally