Data Seeding in EF Core 9
Data Seeding in EF Core 9
When setting up a new database, having default data ready, such as statuses, roles, or configurations, saves a lot of time.
That’s where Data Seeding comes in.With EF Core 9, seeding just got a significant upgrade. No more static data or hard-coded IDs, you can now use UseSeeding() and UseAsyncSeeding() to add data dynamically using real C# logic.
Here’s what you’ll learn in this guide
✅ Difference between HasData() and UseSeeding()
✅ How EF Core automatically triggers seeding during migrations
✅ When to use sync vs async methods
✅ Best practices to avoid duplicate or missing data
Whether you're building a fresh app or maintaining a legacy one, understanding how EF Core 9 handles data seeding can save you time and headaches.
Difference between HasData() and UseSeeding()
Assuming
UseSeeding()is a pattern likemodelBuilder.UseSeedData()or a custom extension that runs code-based seeding.
HasData()
-
What it is: Fluent API method used inside
OnModelCreating: -
How it works:
-
EF Core generates migration code that inserts/updates/deletes rows to match this static data.
-
Data is inserted via migrations, not at runtime arbitrarily.
-
-
Pros:
-
Purely declarative; simple for static lookup/reference data.
-
Automatically idempotent as long as keys don’t change.
-
Works great in CI/CD, new environments, etc. – it’s just part of migrations.
-
-
Cons:
-
No logic: cannot call services, use DI, read config, or async.
-
Awkward for complex scenarios or large datasets.
-
Updates require new migrations when the data changes.
-
UseSeeding() / custom code-based seeding
This is typically a pattern like:
-
Where it runs:
-
Usually called at app startup, after
context.Database.Migrate():
-
-
Pros:
-
Full C# power: logic, loops, conditions, services, config, async I/O.
-
Can integrate with external systems, identity, etc.
-
Better for initial app data that may vary by environment (dev/stage/prod).
-
-
Cons:
-
You must ensure it’s called at the right time and only in one place.
-
Easier to accidentally create duplicate data if you don’t make it idempotent.
-
Not represented in migration history, so restoring purely from migrations may miss it unless seeding runs on first start.
-
Rule of thumb:
-
Use
HasData()for:-
Simple, static, reference data that rarely changes (e.g., enum-like tables).
-
-
Use runtime seeding (
UseSeeding()pattern) for:-
Anything that needs logic, async, services, environment-based behavior, or large/complex datasets.
-
✅ How EF Core automatically triggers seeding during migrations
This only applies to HasData().
-
You define seeding with
HasData()inOnModelCreating. -
When you run
Add-Migration/dotnet ef migrations add:-
EF Core compares the model’s
HasData()with prior snapshots. -
It generates
InsertData,UpdateData, orDeleteDatacalls in the migration’sUp/Downmethods.
-
-
When you run
Update-Databaseorcontext.Database.Migrate():-
Those
InsertData/UpdateData/DeleteDatacommands are executed as SQL. -
That’s when the data actually gets written.
-
So:
EF Core does not “run seeding” at runtime in some magic hook.
It just generates migration operations that insert data on Migrate / Update-Database.
For code-based seeding (UseSeeding() style), EF Core does nothing automatically. You must explicitly call your seed method somewhere (usually app startup, after migrations).
✅ When to use sync vs async methods
General guideline: prefer async in modern apps, especially web apps, because seeding often hits the database and can block threads.
Use async when:
-
You’re in an ASP.NET Core app and seeding runs during startup:
-
You perform any I/O:
-
AnyAsync,AddRangeAsync,SaveChangesAsync, etc.
-
Sync might be OK when:
-
You’re in a console / migration script that is fully synchronous and short-lived.
-
You are absolutely sure that blocking is harmless and simplicity is more important.
But even then, using async is usually easy and future-proof.
Important: Don’t mix (e.g., calling Result or Wait on async calls in ASP.NET Core startup) – that risks deadlocks. If you start async, stay async all the way.
✅ Best practices to avoid duplicate or missing data
This is the part that saves a lot of pain.
1. Make seeding idempotent
Always seed with checks:
Or:
This avoids duplicates if the seed runs multiple times (which it often will).
2. Use stable keys with HasData()
-
Always specify primary keys explicitly:
-
Never change those IDs casually; EF uses them to decide whether to insert/update/delete.
3. Run migrations + seeds in a clear, single place
-
Typical ASP.NET Core pattern:
-
Don’t also run migrations or seeds:
-
In
OnModelCreating -
In controllers
-
In multiple startup paths
…that’s how you get duplicates or race conditions.
-
4. Make seeding safe for multiple instances
If you have multiple app instances starting (Kubernetes, scale-out):
-
Use idempotent checks before inserting.
-
Ideally ensure unique constraints at DB level (e.g., unique index on
Namefor roles), so even if two instances race, one fails but you don’t end up with duplicates. -
If appropriate, wrap seeding in a transaction.
5. Separate concerns: reference data vs environment/app data
-
HasData():-
For stable, global reference data (e.g. Country list, fixed statuses).
-
-
Code-based seeding:
-
For anything that might differ per environment (admin user email, initial tenant, test users, etc.).
-
-
This reduces surprises when deploying to prod vs dev.
6. Keep seeding logic versionable and testable
-
Treat seeding like “mini features”:
-
Put it in separate classes (“Seeds” or “Initializers”).
-
Call them in a clear order.
-
Log what they do (e.g., “Created default admin role”).
-
-
This helps you reason about what should exist and why.
Comments
Post a Comment