
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.
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.
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 fromEnsureCreated()method.UseAsyncSeeding()runs during asynchronous database creation fromEnsureCreatedAsync()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()andUseAsyncSeeding()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
| Scenario | Recommended Approach |
| Small static lookup data | HasData() |
| Dynamic or logic-based data | UseSeeding() or UseAsyncSeeding() |
| Asynchronous initialization | UseAsyncSeeding() |
| Tooling and migrations compatibility | Include 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 andUseSeeding()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()orUseAsyncSeeding()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.
