Skip to main content

05 - Seeding Data

Data Seeding

Data seeding is the process of populating a database with an initial set of data. This is useful for:

  • Lookup/reference data (roles, statuses, contact types, categories)
  • Default admin accounts or system configuration
  • Test/demo data for development

EF Core provides several approaches for seeding data, each with different trade-offs.

Model Seed Data (HasData)

EF Core's built-in seeding mechanism uses HasData() in OnModelCreating. Seed data becomes part of the migration and is applied via Up()/Down() methods.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ContactType>().HasData(
new ContactType { Id = 1, Name = "Phone" },
new ContactType { Id = 2, Name = "Email" },
new ContactType { Id = 3, Name = "Skype" }
);
}

After adding HasData(), create a new migration:

dotnet ef migrations add SeedContactTypes

The generated migration will contain InsertData operations in Up() and DeleteData in Down().

HasData Rules and Limitations

Primary key values must be specified explicitly - even for auto-increment columns. EF uses PK values to track seed data across migrations.

// Correct - explicit PK
new ContactType { Id = 1, Name = "Phone" }

// Wrong - will throw an error
new ContactType { Name = "Phone" }

Navigation properties cannot be used. Use foreign key values instead.

// Wrong - cannot use navigation property
new Contact { Id = 1, ContactType = someContactType }

// Correct - use FK value
new Contact { Id = 1, ContactTypeId = 1 }

Changes to seed data require a new migration. If you modify HasData() values, you must create a new migration for the changes to take effect.

dotnet ef migrations add UpdateSeedData

When seeding entities with relationships, use FK properties and ensure referenced entities are also seeded.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ContactType>().HasData(
new ContactType { Id = 1, Name = "Phone" },
new ContactType { Id = 2, Name = "Email" }
);

modelBuilder.Entity<Person>().HasData(
new Person { Id = 1, FirstName = "John", LastName = "Doe" }
);

modelBuilder.Entity<Contact>().HasData(
new Contact { Id = 1, Value = "+372 555 1234", PersonId = 1, ContactTypeId = 1 },
new Contact { Id = 2, Value = "john@example.com", PersonId = 1, ContactTypeId = 2 }
);
}

When to Use HasData

  • Lookup/reference data that rarely changes (roles, statuses, categories)
  • Data that should be version-controlled as part of migrations
  • Small datasets with known, stable PK values

Custom Initialization (DbInitializer)

For more complex seeding scenarios, create a custom initializer class. This approach gives full control and supports navigation properties.

public static class DbInitializer
{
public static void SeedData(AppDbContext context)
{
// Check if data already exists (idempotent)
if (context.ContactTypes.Any())
{
return;
}

var phone = new ContactType { Name = "Phone" };
var email = new ContactType { Name = "Email" };
context.ContactTypes.AddRange(phone, email);
context.SaveChanges();

var person = new Person
{
FirstName = "Admin",
LastName = "User",
Contacts = new List<Contact>
{
new Contact { Value = "+372 555 0000", ContactType = phone },
new Contact { Value = "admin@example.com", ContactType = email }
}
};
context.Persons.Add(person);
context.SaveChanges();
}
}

Call from Program.cs:

// After building the app, before app.Run()
using var scope = app.Services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
DbInitializer.SeedData(context);

Advantages Over HasData

  • Can use navigation properties directly
  • No need to specify PK values for auto-increment columns
  • Supports complex logic (conditional seeding, computed values)
  • Can call external services or read from files

Disadvantages

  • Not tracked in migrations (harder to reproduce exact state)
  • Runs on every application startup (must handle idempotency)
  • Separate from schema versioning

Seeding in Migrations

You can add seed data directly in migration files using migrationBuilder.InsertData().

public partial class SeedInitialRoles : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.InsertData(
table: "ContactTypes",
columns: new[] { "Id", "Name" },
values: new object[,]
{
{ 1, "Phone" },
{ 2, "Email" },
{ 3, "Skype" }
});
}

protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DeleteData(
table: "ContactTypes",
keyColumn: "Id",
keyValues: new object[] { 1, 2, 3 });
}
}

You can also use raw SQL in migrations for complex seeding:

protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
@"INSERT INTO ContactTypes (Id, Name) VALUES (1, 'Phone'), (2, 'Email')");
}

When to Use Migration-Based Seeding

  • Data that must be applied exactly once as part of a specific schema change
  • When you need full control over the SQL generated
  • Production deployments where repeatable scripts are required

Choosing an Approach

ApproachBest ForPK RequiredTracked in MigrationsNavigation Properties
HasDataLookup data, reference tablesYes (explicit)YesNo (use FK values)
DbInitializerDev/test data, complex graphsNo (auto-generated)NoYes
Migration InsertDataProduction seed, one-time dataYes (explicit)YesNo