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

𝗙𝗹𝘂𝗲𝗻𝘁𝗩𝗮𝗹𝗶𝗱𝗮𝘁𝗶𝗼𝗻 𝗶𝗻 𝗔𝗦𝗣.𝗡𝗘𝗧 𝗖𝗼𝗿𝗲 - 𝗖𝗹𝗲𝗮𝗻, 𝗙𝗹𝗲𝘅𝗶𝗯𝗹𝗲 𝗠𝗼𝗱𝗲𝗹 𝗩𝗮𝗹𝗶𝗱𝗮𝘁𝗶𝗼𝗻 𝗳𝗼𝗿 𝗠𝗼𝗱𝗲𝗿𝗻 .𝗡𝗘𝗧 𝗔𝗽𝗽𝘀

Azure Event Grid Sample code