Data Seeding in EF Core 9 – Everything You Need to Know

When working with databases, it’s often necessary to populate them with initial data — such as predefined values, default settings, or lookup tables.
In Entity Framework Core, this process is known as data seeding.

With EF Core 9, Microsoft has introduced a more flexible and powerful approach for seeding data that goes beyond static definitions.

In this article, we’ll explore everything you need to know about data seeding, from the traditional HasData() method to the new UseSeeding() and UseAsyncSeeding() APIs.

What Is Data Seeding?

Data seeding is the process of automatically adding data to your database when it’s created or migrated. This ensures that your application always starts with the necessary data without requiring manual SQL scripts or extra setup.

Example use cases:

  • Populating lookup tables like “Status”, “Priority”, or “Category”
  • Adding default configuration values
  • Ensuring consistent test or development environments

The Traditional Way — Using HasData()

Before EF Core 9, seeding was primarily done using the HasData() method inside OnModelCreating().

Let’s look at how this works.

C#
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Status>().HasData(
        new Status { Id = 1, Name = "Pending" },
        new Status { Id = 2, Name = "Approved" },
        new Status { Id = 3, Name = "Rejected" }
    );
}

When you run Add-Migration and Update-Database, EF Core automatically generates INSERT statements in your migration file. This method is simple and effective for small, static datasets.

Limitations of HasData()

While HasData() works for basic cases, it comes with several limitations:

  • Requires hard-coded primary keys: EF Core needs fixed IDs to track and manage the inserted data.
  • Data becomes part of migrations: Every change to the seed data requires a new migration, even for minor edits.
  • Static-only data: You can’t use dynamic values like DateTime.Now, Guid.NewGuid(), or access services.
  • Snapshot bloat: Large seeded datasets can make the model snapshot file unnecessarily large.

To address these limitations, EF Core 9 introduces a new way to seed data — one that’s more flexible and code-driven.

The Modern Way — UseSeeding() and UseAsyncSeeding()

Starting in EF Core 9, you can define seeding logic directly when configuring your DbContext. This allows you to write real C# code to control how your seed data behaves — including conditions, async operations, and more.

C#
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(@"Server=localdb)\mssqllocaldb;Database=AppDb;Trusted_Connection=True;")
        .UseSeeding((context, _) =>
        {
            // Check and insert default data
            if (!context.Set<Status>().Any())
            {
                context.Set<Status>().AddRange(
                    new Status { Name = "Pending" },
                    new Status { Name = "Approved" },
                    new Status { Name = "Rejected" }
                );
                
                context.SaveChanges();
            }
        })
        .UseAsyncSeeding(async (context, _, cancellationToken) =>
        {
            if (!await context.Set<Status>().AnyAsync(cancellationToken))
            {
                await context.Set<Status>().AddRangeAsync(new[]
                {
                    new Status { Name = "Pending" },
                    new Status { Name = "Approved" },
                    new Status { Name = "Rejected" }
                }, cancellationToken);

                await context.SaveChangesAsync(cancellationToken);
            }
        });
}

How EF Core 9 Executes Seeding

Whenever EF Core creates or updates your database — through

  • Database.EnsureCreated()
  • Database.Migrate()
  • or dotnet ef database update

It automatically triggers the seeding logic defined in your UseSeeding() or UseAsyncSeeding() configuration.

The important thing to understand is that these two methods work hand in hand:

  • UseSeeding() runs during synchronous database creation from EnsureCreated() method.
  • UseAsyncSeeding() runs during asynchronous database creation from EnsureCreatedAsync() method.

Even if your application mainly uses asynchronous database operations, it’s highly recommended to implement both versions with similar logic.

That’s because EF Core’s CLI tooling still depends on the synchronous UseSeeding() method. If it’s missing, your database might not seed correctly when running migrations or using EF commands from the terminal.

The new UseSeeding() and UseAsyncSeeding() methods are the recommended approach for adding initial data in EF Core 9.

They give you the flexibility to use C# logic, conditions, and async operations — all without cluttering your migration files.

When to Use Each Method

ScenarioRecommended Approach
Small static lookup dataHasData()
Dynamic or logic-based dataUseSeeding() or UseAsyncSeeding()
Asynchronous initializationUseAsyncSeeding()
Tooling and migrations compatibilityInclude both sync and async versions

Best Practices for Data Seeding in EF Core 9

  • Keep seeding idempotent: Always check if data exists before inserting. This prevents duplicate records.
  • Separate static and dynamic data: Use HasData() for fixed data and UseSeeding() for logic-based seeding.
  • Use both sync and async versions: EF CLI tools use synchronous seeding during migration.
  • Avoid long-running operations: Seeding should be lightweight — don’t perform large file reads or API calls.
  • Use Database.Migrate() in production: EnsureCreated() skips migrations and should only be used for local testing.
  • Handle environment-based seeding: Use configuration or environment variables to seed data differently across environments
  • Ensure thread safety: In multi-instance environments, concurrent seeding can cause conflicts. Always include existence checks before inserting.

Summary

Data seeding helps ensure your database always starts in a usable state — without running manual scripts or imports.

With EF Core 9, seeding becomes much more flexible and expressive, thanks to the new UseSeeding() and UseAsyncSeeding() methods.

Takeaways

  • Use HasData() for simple, static data.
  • Use UseSeeding() or UseAsyncSeeding() for dynamic logic.
  • Combine both methods when needed.
  • Keep seeding code lightweight, idempotent, and environment-aware.

EF Core 9 gives you full control over database initialization — using clean, maintainable, and testable code.

Found this article useful? Share it with your network and spark a conversation.