When I first started using EF Core in one of my projects, I thought seeding data was just about throwing some “default” values into the database. You know, like inserting an admin user or a few predefined roles so the app wouldn’t break during first login. But over time, after struggling with migrations, production issues, and a couple of “why is this data duplicated”? moments, I realized that seeding is not only about filling tables it’s about making sure your application always starts with the right foundation.
So in this post, I’ll share how I approach database seeding in Entity Framework Core (EF Core) today. Not just the “how-to” but also the lessons I learned from my own mistakes (like hardcoding passwords in seeds don’t ever do that). Hopefully, this will help you avoid the pitfalls I fell into.
What Do We Mean by “Seeding” Anyway?
Seeding is basically providing initial data to your database. Think of it like setting up a brand new laptop you install the OS, but you also want to have the default apps, settings, and maybe Wi-Fi passwords ready so you don’t have to start from zero.
In EF Core, seeding is about:
- Populating lookup tables (like countries, currencies, payment statuses).
- Creating default users or roles (like “Admin,” “Merchant,” “Customer”).
- Adding configuration settings that your app needs right away.
In one of my projects, we had a payment facilitator system where the database had to know at least the roles (Admin, Support, Merchant) and some default configuration (transaction types, supported currencies). Without those, the API would just crash on first run.
The Old Way vs. The EF Core Way
When I started, my “seeding” was just a SQL script I ran manually after deploying. The problem? Sometimes I forgot. Other times, the script failed silently because the schema already changed. And yes, I had a couple of late night fixes because a staging DB didn’t get the correct seed data.
With EF Core, seeding can live inside your DbContext or migrations. That means it’s versioned, automated, and less error-prone.
Let me show you.
Basic Seeding with HasData
The most straightforward way to seed is using the HasData method inside your model configuration.
Here’s a simple example with roles:
using Microsoft.EntityFrameworkCore;
public class AppDbContext : DbContext
{
public DbSet<Role> Roles { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Role>().HasData(
new Role { Id = 1, Name = "Admin" },
new Role { Id = 2, Name = "Merchant" },
new Role { Id = 3, Name = "Customer" }
);
}
}
public class Role
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}When you run a migration, EF Core will generate the insert statements.
Lesson learned from my project: Don’t seed users with passwords here. Why? Because HasData generates static SQL, and you’ll end up hardcoding values. That’s a nightmare if you later decide to hash passwords differently.
When HasData Isn’t Enough
In one of my projects, we needed to seed an admin user with a hashed password. Since HasData can’t run custom logic (like calling a password hasher), I had to find another approach.
That’s where runtime seeding comes in. Instead of seeding inside the model, you run your own code after the database is created or migrated.
Here’s how I usually do it now:
public static class DatabaseSeeder
{
public static async Task SeedAsync(AppDbContext context, UserManager<AppUser> userManager, RoleManager<Role> roleManager)
{
// Seed roles
if (!roleManager.Roles.Any())
{
await roleManager.CreateAsync(new Role { Name = "Admin" });
await roleManager.CreateAsync(new Role { Name = "Merchant" });
await roleManager.CreateAsync(new Role { Name = "Customer" });
}
// Seed admin user
if (!userManager.Users.Any())
{
var admin = new AppUser
{
UserName = "admin",
Email = "admin@demo.com"
};
await userManager.CreateAsync(admin, "SuperSecurePassword123!");
await userManager.AddToRoleAsync(admin, "Admin");
}
}
}Then you call this in your Program.cs:
using (var scope = app.Services.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<AppUser>>();
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<Role>>();
await DatabaseSeeder.SeedAsync(context, userManager, roleManager);
}This way, you can hash passwords, assign claims, or load config from JSON files.
Pitfalls I Faced (And How I Solved Them)
1. Duplicate seeds after multiple runs
- At first, I just inserted blindly. That gave me “Admin” role three times.
- Fix: Always check with .Any() before seeding.
2. Data mismatch after schema changes
- Example: A role name column changed length, and migration failed because the seed didn’t fit.
- Fix: Keep seed data consistent with the latest model. Test migrations locally before pushing.
3. Environment-specific data
- I once accidentally seeded production with test users (ouch).
- Fix: Add environment checks. For example, only seed demo users in Development.
if (app.Environment.IsDevelopment())
{
await DatabaseSeeder.SeedDevUsersAsync(context, userManager);
}Advanced Seeding Ideas
- JSON-based seeds: Store default config in a JSON file, then load it at startup. Handy for long lists like countries.
- Versioned seeds: Keep track of what seeds ran, almost like mini-migrations for data.
- Conditional seeds: For example, only seed new roles if they don’t exist yet.
My Personal Best Practices
If I could summarize my journey with EF Core seeding, here are the key points:
- Use HasData for simple, static lookup values (like currency codes).
- Use runtime seeding for anything dynamic, sensitive, or environment-specific.
- Never hardcode secrets in seeds (passwords, API keys).
- Keep seeding code part of your project lifecycle—test it just like normal code.
- Document what gets seeded, so your teammates don’t wonder why an “Admin” account magically appears.
Wrapping Up
Seeding may sound like a small thing, but in reality, it’s the foundation of your app’s data. A bad seed strategy will haunt you with duplicated rows, broken migrations, or worse production bugs.
From my own projects, I learned that it’s better to invest some time making your seeding process clean and predictable. Whether you go with HasData or a custom runtime seeder, the important thing is to treat it seriously.
So next time you start a new EF Core project, don’t just think of seeding as “filling tables”. Think of it as bootstrapping your application for success.






