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 like modelBuilder.UseSeedData() or a custom extension that runs code-based seeding.

HasData()

  • What it is: Fluent API method used inside OnModelCreating:

    modelBuilder.Entity<Role>().HasData( new Role { Id = 1, Name = "Admin" }, new Role { Id = 2, Name = "User" } );
  • 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:

public static class ApplicationDbContextSeed { public static async Task SeedAsync(IServiceProvider services) { using var scope = services.CreateScope(); var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); if (!await context.Roles.AnyAsync()) { context.Roles.Add(new Role { Name = "Admin" }); await context.SaveChangesAsync(); } } }
  • Where it runs:

    • Usually called at app startup, after context.Database.Migrate():

      await context.Database.MigrateAsync(); await ApplicationDbContextSeed.SeedAsync(app.Services);
  • 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().

  1. You define seeding with HasData() in OnModelCreating.

  2. When you run Add-Migration / dotnet ef migrations add:

    • EF Core compares the model’s HasData() with prior snapshots.

    • It generates InsertData, UpdateData, or DeleteData calls in the migration’s Up / Down methods.

  3. When you run Update-Database or context.Database.Migrate():

    • Those InsertData/UpdateData/DeleteData commands 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:

    await context.Database.MigrateAsync(); await ApplicationDbContextSeed.SeedAsync(app.Services);
  • 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:

if (!await context.Roles.AnyAsync(r => r.Name == "Admin")) { context.Roles.Add(new Role { Name = "Admin" }); await context.SaveChangesAsync(); }

Or:

var role = await context.Roles .SingleOrDefaultAsync(r => r.Name == "Admin"); if (role is null) { context.Roles.Add(new Role { Name = "Admin" }); await context.SaveChangesAsync(); }

This avoids duplicates if the seed runs multiple times (which it often will).

2. Use stable keys with HasData()

  • Always specify primary keys explicitly:

    modelBuilder.Entity<Role>().HasData( new Role { Id = 1, Name = "Admin" } );
  • 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:

    using var scope = app.Services.CreateScope(); var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); await context.Database.MigrateAsync(); await ApplicationDbContextSeed.SeedAsync(scope.ServiceProvider);
  • 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 Name for 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

Popular posts from this blog

Performance Optimization in Sitecore

Strategies for Migrating to Sitecore from legacy or upgrading from older Sitecore

Azure Event Grid Sample code