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
Seeding Related Entities
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
| Approach | Best For | PK Required | Tracked in Migrations | Navigation Properties |
|---|---|---|---|---|
| HasData | Lookup data, reference tables | Yes (explicit) | Yes | No (use FK values) |
| DbInitializer | Dev/test data, complex graphs | No (auto-generated) | No | Yes |
| Migration InsertData | Production seed, one-time data | Yes (explicit) | Yes | No |