Skip to content

Data Seeding

github-actions[bot] edited this page Aug 4, 2026 · 1 revision

Data Seeding

Seeding puts reference data like currencies and categories or demo content for a first run into a fresh database. The framework has no dedicated seeding API because two existing pieces cover it, a guarded insert at startup and the migration chain. Pick by how the data should behave later.

Insert when empty

The simplest seed. After the schema exists, check and fill:

await db.Schema.CreateTableAsync<Category>();

if (!await db.Table<Category>().AnyAsync())
{
    await db.Table<Category>().AddRangeAsync(
    [
        new Category { Name = "Fiction" },
        new Category { Name = "Science" },
        new Category { Name = "History" },
    ]);
}

AddRange wraps the inserts in one transaction by default. This pattern never touches a database that already has data, so users keep their edits. That is also its limit. It cannot add the new category you introduce in version two. Use it for demo content and starting points the user owns afterwards.

Idempotent seed with fixed keys

Reference data the app owns and may extend in later releases wants an upsert instead of a guard. Give each row a fixed key and run the seed on every startup:

await db.Table<Country>().AddOrUpdateRangeAsync(
[
    new Country { Code = "BG", Name = "Bulgaria" },
    new Country { Code = "DE", Name = "Germany" },
]);

New rows appear. Existing rows are refreshed to the shipped values. User data in other tables is untouched. AddOrUpdate replaces the whole row on conflict. When user-editable columns must survive, use Upsert with a DoUpdate that lists only the columns you own, see CRUD Operations.

Seeding inside a migration

When seed data is part of a schema version, put it in the migration chain with the typed Insert step. It runs exactly once per database, inside the migration's transaction and after the tables of that run are reconciled.

await db.Schema.Migrations()
    .Version(1, m => m
        .CreateTable<Category>()
        .Insert(
            new Category { Name = "Fiction" },
            new Category { Name = "Science" }))
    .Version(2, m => m
        .Insert(new Category { Name = "History" }))
    .MigrateAsync();

The rows go through the same write pipeline as Add, so storage modes, converters, write hooks and auto-increment key write-back all apply. A failed insert rolls the whole run back, like every other step.

When a seed row may already be in the table, for example because users can create the same category themselves, use InsertIfMissing. It takes a key selector and inserts only the rows whose key value is not in the table yet. The rows are checked against the table, not against each other.

await db.Schema.Migrations()
    .Version(3, m => m.InsertIfMissing(c => c.Name,
        new Category { Name = "History" },
        new Category { Name = "Poetry" }))
    .MigrateAsync();

Seed rows follow the same rule as every migration change. Never add them to a version that has shipped, because databases that passed that version will not run it again. Declare the next version instead, like version 2 above. See Migrations.

For a data fix that is not an insert, an UPDATE over old rows for example, use the raw Sql step or a Run callback, see Migrations.

Seed data from a file

Larger datasets read better as an asset than as code. Ship a JSON file, parse it, insert with AddRange:

if (!await db.Table<Book>().AnyAsync())
{
    await using Stream stream = File.OpenRead(seedPath);
    List<Book> books = await JsonSerializer.DeserializeAsync<List<Book>>(stream) ?? [];
    await db.Table<Book>().AddRangeAsync(books);
}

The Avalonia sample in the repository ships SeedData.json and a small seed service built exactly this way. See Samples.

What seeding is not

A column DEFAULT is not seeding. It fills a column on rows inserted later. See Schema. Test fixtures are not seeding either. Tests build their own data per test. See Testing.

Clone this wiki locally