You deploy your API to staging, a tester tries to create a user, and boom - crash. The roles table is empty. There are no default categories, no lookup data, no admin account. The database schema is perfect, but it’s a ghost town.
This is one of the most common oversights in .NET projects. You spend hours perfecting your entities, configurations, and migrations - but forget that an empty database is a broken database. Your app expects certain data to exist, and when it doesn’t, things fall apart in ways that are surprisingly hard to debug.
In this article, I’ll walk you through every data seeding strategy available in EF Core 10 (Entity Framework Core 10) - from migration-based seeding with HasData to runtime seeding with the UseSeeding API introduced in EF Core 9. I’ll show you exactly when to use each approach, the common pitfalls that catch developers off guard, and a decision matrix so you pick the right strategy from day one. Let’s get into it.
What Is Data Seeding in EF Core?
Data seeding is the process of populating a database with an initial set of data when the database is first created or when migrations are applied. In EF Core 10 (Entity Framework Core 10), data seeding ensures that your application starts with the required reference data, lookup values, default configurations, or test records that the application depends on to function correctly.
EF Core provides three primary approaches to seed data: HasData for migration-based seeding that bakes data into your schema migrations, UseSeeding/UseAsyncSeeding for runtime seeding that executes when the database is initialized, and custom seeding logic in Program.cs for scenarios that require dependency injection or complex initialization workflows.
The right seeding strategy depends on your data’s lifecycle - reference data that never changes belongs in migrations, while test data that varies by environment belongs in runtime seeding.
Prerequisites
This article builds on the Movie API I created in the CRUD with EF Core tutorial. If you’ve been following the course, you already have the Movie entity, MovieDbContext, and MovieConfiguration set up. If not, grab the source code from the repo linked above - you’ll need:
- .NET 10 SDK - Install from here
- Docker Desktop - Install from here (for PostgreSQL)
- The existing Movie API project structure with EF Core 10 and PostgreSQL configured
Three Approaches at a Glance
Before diving into implementation, here’s a quick comparison of the three seeding strategies:
| Approach | When It Runs | Stored In | Idempotent? | Best For |
|---|---|---|---|---|
HasData | During dotnet ef database update | Migration files | Automatic | Reference data, lookup tables |
UseSeeding / UseAsyncSeeding | During EnsureCreated / Migrate | DbContext code | You implement it | Dev/test data, complex seeding |
Custom Program.cs | At app startup | Program.cs / service classes | You implement it | Seeding that needs DI services |
Each approach solves different problems. Let’s break them down.
Approach 1: HasData - Migration-Based Seeding
HasData is EF Core’s built-in mechanism for including seed data directly in your migrations. This is documented in the official EF Core Data Seeding guide. When you call HasData in your entity configuration, EF Core generates INSERT statements in the migration file - the data becomes part of your database schema history.
This approach is best for reference data that rarely changes: role definitions, status codes, country lists, default categories - data that should exist in every environment from day one.
Adding HasData to Entity Configuration
Add HasData to your IEntityTypeConfiguration class. Here’s how to seed default movies in our MovieConfiguration:
public class MovieConfiguration : IEntityTypeConfiguration<Movie>{ public void Configure(EntityTypeBuilder<Movie> builder) { builder.ToTable("Movies"); builder.HasKey(m => m.Id);
builder.Property(m => m.Title).IsRequired().HasMaxLength(200); builder.Property(m => m.Genre).IsRequired().HasMaxLength(100); builder.Property(m => m.ReleaseDate).IsRequired(); builder.Property(m => m.Rating).IsRequired(); builder.Property(m => m.Created).IsRequired().ValueGeneratedOnAdd(); builder.Property(m => m.LastModified).IsRequired().ValueGeneratedOnUpdate(); builder.HasIndex(m => m.Title);
// Seed default movies builder.HasData( new { Id = Guid.Parse("d1a7b9c3-4e56-4f89-a123-b456c789d012"), Title = "The Shawshank Redemption", Genre = "Drama", ReleaseDate = new DateTimeOffset(new DateTime(1994, 9, 23), TimeSpan.Zero), Rating = 9.3, Created = new DateTimeOffset(new DateTime(2026, 1, 1), TimeSpan.Zero), LastModified = new DateTimeOffset(new DateTime(2026, 1, 1), TimeSpan.Zero) }, new { Id = Guid.Parse("e2b8c0d4-5f67-4890-b234-c567d890e123"), Title = "The Dark Knight", Genre = "Action", ReleaseDate = new DateTimeOffset(new DateTime(2008, 7, 18), TimeSpan.Zero), Rating = 9.0, Created = new DateTimeOffset(new DateTime(2026, 1, 1), TimeSpan.Zero), LastModified = new DateTimeOffset(new DateTime(2026, 1, 1), TimeSpan.Zero) }, new { Id = Guid.Parse("f3c9d1e5-6078-4901-c345-d678e901f234"), Title = "Inception", Genre = "Sci-Fi", ReleaseDate = new DateTimeOffset(new DateTime(2010, 7, 16), TimeSpan.Zero), Rating = 8.8, Created = new DateTimeOffset(new DateTime(2026, 1, 1), TimeSpan.Zero), LastModified = new DateTimeOffset(new DateTime(2026, 1, 1), TimeSpan.Zero) } ); }}Notice I’m using anonymous objects here, not Movie.Create(). This is critical and one of the first things that trips up developers using DDD patterns. Our Movie entity has private constructors and private setters - HasData can’t use the factory method. Instead, EF Core uses anonymous objects and maps properties by name. Every property that maps to a column must be included - Id, Created, and LastModified from EntityBase are all required.
Why Fixed GUIDs Are Non-Negotiable
See those hardcoded Guid.Parse(...) calls? They’re not optional. If you let EF Core use Guid.NewGuid() (which is our EntityBase default), every time you create a migration, EF Core sees “new” data and generates a DELETE + INSERT instead of recognizing existing records. Fixed GUIDs tell EF Core “this is the same record” across migrations.
The mistake I see most devs make with HasData is using dynamic values for primary keys. It works fine on the first migration, but the second one turns into chaos.
Generating the Migration
After adding HasData, create a migration:
dotnet ef migrations add SeedDefaultMoviesEF Core generates a migration with the insert statements:
protected override void Up(MigrationBuilder migrationBuilder){ migrationBuilder.InsertData( schema: "app", table: "Movies", columns: new[] { "Id", "Created", "Genre", "LastModified", "Rating", "ReleaseDate", "Title" }, values: new object[,] { { new Guid("d1a7b9c3-4e56-4f89-a123-b456c789d012"), new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Unspecified), TimeSpan.Zero), "Drama", new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Unspecified), TimeSpan.Zero), 9.3, new DateTimeOffset(new DateTime(1994, 9, 23, 0, 0, 0, DateTimeKind.Unspecified), TimeSpan.Zero), "The Shawshank Redemption" }, { new Guid("e2b8c0d4-5f67-4890-b234-c567d890e123"), new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Unspecified), TimeSpan.Zero), "Action", new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Unspecified), TimeSpan.Zero), 9.0, new DateTimeOffset(new DateTime(2008, 7, 18, 0, 0, 0, DateTimeKind.Unspecified), TimeSpan.Zero), "The Dark Knight" }, { new Guid("f3c9d1e5-6078-4901-c345-d678e901f234"), new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Unspecified), TimeSpan.Zero), "Sci-Fi", new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Unspecified), TimeSpan.Zero), 8.8, new DateTimeOffset(new DateTime(2010, 7, 16, 0, 0, 0, DateTimeKind.Unspecified), TimeSpan.Zero), "Inception" } });}Apply it with:
dotnet ef database updateThe data is now permanently part of your migration history. Every new database created from these migrations will have these three movies.
What Happens When You Change Seeded Data
Here’s something that catches developers off guard. If you change a seeded value - say you update the rating of The Shawshank Redemption from 9.3 to 9.4 - the next migration generates an UPDATE statement:
migrationBuilder.UpdateData( schema: "app", table: "Movies", keyColumn: "Id", keyValue: new Guid("d1a7b9c3-4e56-4f89-a123-b456c789d012"), column: "Rating", value: 9.4);This is by design. HasData tracks changes to your seed data just like it tracks schema changes. But in production, if someone has already modified that record through the API, the migration will overwrite their changes. Keep this in mind.
PRO TIP: Only use
HasDatafor data that your application owns and controls - not user-modifiable data.
Limitations of HasData
Before choosing this approach, know what HasData can’t do:
- Cannot use navigation properties - you must specify foreign key values directly
- Cannot call entity factory methods - anonymous objects only (for DDD entities with private constructors)
- Cannot seed data conditionally - it’s all or nothing, same data in every environment
- Every change creates a new migration - changing a seeded rating from 9.3 to 9.4 generates a full migration
- No dynamic values -
Guid.NewGuid()orDateTime.UtcNowmust be hardcoded constants
Approach 2: UseSeeding and UseAsyncSeeding - Runtime Seeding
Starting with EF Core 9, there’s a much cleaner way to seed data at runtime: the UseSeeding and UseAsyncSeeding callbacks. These run every time EnsureCreated, EnsureCreatedAsync, Migrate, or MigrateAsync is called - making them perfect for data that doesn’t belong in migration files.
If you’ve been following this course, you’ve already seen this in action. In the CRUD article, I used UseAsyncSeeding to seed a sample movie. Now let’s go deeper.
UseSeeding and UseAsyncSeeding are callbacks configured on DbContextOptionsBuilder (typically in OnConfiguring or during service registration). They receive the DbContext instance and execute custom seeding logic. Unlike HasData, these callbacks give you full control - you can query existing data, call entity factory methods like Movie.Create(), and implement any conditional logic you need.
Implementing UseSeeding in MovieDbContext
public class MovieDbContext(DbContextOptions<MovieDbContext> options) : DbContext(options){ public DbSet<Movie> Movies => Set<Movie>();
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.HasDefaultSchema("app"); modelBuilder.ApplyConfigurationsFromAssembly(typeof(MovieDbContext).Assembly); base.OnModelCreating(modelBuilder); }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder .UseAsyncSeeding(async (context, _, cancellationToken) => { if (!await context.Set<Movie>().AnyAsync(cancellationToken)) { var movies = new[] { Movie.Create("The Shawshank Redemption", "Drama", new DateTimeOffset(new DateTime(1994, 9, 23), TimeSpan.Zero), 9.3), Movie.Create("The Dark Knight", "Action", new DateTimeOffset(new DateTime(2008, 7, 18), TimeSpan.Zero), 9.0), Movie.Create("Inception", "Sci-Fi", new DateTimeOffset(new DateTime(2010, 7, 16), TimeSpan.Zero), 8.8), Movie.Create("Interstellar", "Sci-Fi", new DateTimeOffset(new DateTime(2014, 11, 7), TimeSpan.Zero), 8.7), Movie.Create("The Matrix", "Sci-Fi", new DateTimeOffset(new DateTime(1999, 3, 31), TimeSpan.Zero), 8.7) };
await context.Set<Movie>().AddRangeAsync(movies, cancellationToken); await context.SaveChangesAsync(cancellationToken); } }) .UseSeeding((context, _) => { if (!context.Set<Movie>().Any()) { var movies = new[] { Movie.Create("The Shawshank Redemption", "Drama", new DateTimeOffset(new DateTime(1994, 9, 23), TimeSpan.Zero), 9.3), Movie.Create("The Dark Knight", "Action", new DateTimeOffset(new DateTime(2008, 7, 18), TimeSpan.Zero), 9.0), Movie.Create("Inception", "Sci-Fi", new DateTimeOffset(new DateTime(2010, 7, 16), TimeSpan.Zero), 8.8) };
context.Set<Movie>().AddRange(movies); context.SaveChanges(); } }); }}See the difference? I’m calling Movie.Create() directly - the factory method validates inputs, sets timestamps, and returns a properly initialized entity. No anonymous objects, no hardcoded GUIDs, no bypassing domain rules.
You Must Implement Idempotency
Unlike HasData, which EF Core handles automatically, UseSeeding callbacks run every time EnsureCreated or Migrate is called. If you don’t check whether data already exists, you’ll get duplicate records - or worse, primary key violations that crash your app on startup.
The simplest check:
if (!await context.Set<Movie>().AnyAsync(cancellationToken))For more granular seeding where you’re adding individual records that might not exist, check by a unique property:
var exists = await context.Set<Movie>() .AnyAsync(m => m.Title == "The Shawshank Redemption", cancellationToken);if (!exists){ context.Set<Movie>().Add( Movie.Create("The Shawshank Redemption", "Drama", new DateTimeOffset(new DateTime(1994, 9, 23), TimeSpan.Zero), 9.3)); await context.SaveChangesAsync(cancellationToken);}Why You Need Both Callbacks
You might wonder why you need both UseSeeding and UseAsyncSeeding. UseAsyncSeeding runs when async methods like EnsureCreatedAsync or MigrateAsync are called. UseSeeding runs for synchronous methods like EnsureCreated. In practice, you’ll almost always use async in ASP.NET Core, but EF Core requires the synchronous callback as a fallback.
PRO TIP: Always implement both callbacks. If you only implement
UseAsyncSeedingand some code path callsEnsureCreated(sync), no seeding happens - silently. I’ve debugged this exact issue in production. Trust me, the few extra lines are worth it.
When UseSeeding Runs
The seeding callback executes in these scenarios:
context.Database.EnsureCreated()/EnsureCreatedAsync()- runs every time it’s calledcontext.Database.Migrate()/MigrateAsync()- runs every time it’s called
This means your idempotency check is not optional. If you call EnsureCreatedAsync() on every startup (as our Movie API does in Program.cs), the callback fires on every startup.
HasData vs UseSeeding - Feature Comparison
| Feature | HasData | UseSeeding |
|---|---|---|
| Uses entity factory methods | No (anonymous objects) | Yes |
| Supports DDD patterns | Awkward | Natural |
| Conditional logic | No | Yes |
| Creates migration files | Yes (bloat risk) | No |
| Automatic idempotency | Yes | Manual |
| Dynamic values allowed | No | Yes |
Rollback via Down migration | Yes | No |
Approach 3: Custom Seeding in Program.cs
Before UseSeeding existed (EF Core 8 and earlier), the standard approach was to create a service scope in Program.cs and seed data manually. This approach is still valid and sometimes necessary - especially when your seeding logic requires services from the DI container that aren’t available in the DbContext.
var app = builder.Build();
// Seed dataawait using (var scope = app.Services.CreateAsyncScope()){ var dbContext = scope.ServiceProvider.GetRequiredService<MovieDbContext>(); var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
await dbContext.Database.MigrateAsync();
if (!await dbContext.Movies.AnyAsync()) { logger.LogInformation("Seeding default movie data...");
var movies = new[] { Movie.Create("The Shawshank Redemption", "Drama", new DateTimeOffset(new DateTime(1994, 9, 23), TimeSpan.Zero), 9.3), Movie.Create("The Dark Knight", "Action", new DateTimeOffset(new DateTime(2008, 7, 18), TimeSpan.Zero), 9.0), Movie.Create("Inception", "Sci-Fi", new DateTimeOffset(new DateTime(2010, 7, 16), TimeSpan.Zero), 8.8) };
await dbContext.Movies.AddRangeAsync(movies); await dbContext.SaveChangesAsync(); logger.LogInformation("Seeded {Count} default movies", movies.Length); }}When to use this over UseSeeding:
- You need services from the DI container (like
ILogger,IConfiguration, or custom services) - You need to seed data across multiple DbContexts
- Your seeding logic is complex enough to warrant its own service class
- You’re running EF Core 8 or earlier where
UseSeedingdoesn’t exist
For most scenarios in EF Core 10, UseSeeding is the cleaner choice. But this approach gives you full access to the DI container, which UseSeeding’s callback doesn’t have.
Seeding Related Entities with Foreign Keys
Things get interesting when you need to seed entities that have relationships. Let’s say you refactor your Movie entity to use a proper Genre entity instead of a string property - a common evolution as your domain grows.
With HasData: Use Foreign Key Values, Not Navigation Properties
HasData doesn’t support navigation properties. You must specify the foreign key value directly:
public class GenreConfiguration : IEntityTypeConfiguration<Genre>{ public void Configure(EntityTypeBuilder<Genre> builder) { builder.ToTable("Genres"); builder.HasKey(g => g.Id); builder.Property(g => g.Name).IsRequired().HasMaxLength(50);
builder.HasData( new { Id = Guid.Parse("a1b2c3d4-0000-0000-0000-000000000001"), Name = "Drama", Created = new DateTimeOffset(new DateTime(2026, 1, 1), TimeSpan.Zero), LastModified = new DateTimeOffset(new DateTime(2026, 1, 1), TimeSpan.Zero) }, new { Id = Guid.Parse("a1b2c3d4-0000-0000-0000-000000000002"), Name = "Action", Created = new DateTimeOffset(new DateTime(2026, 1, 1), TimeSpan.Zero), LastModified = new DateTimeOffset(new DateTime(2026, 1, 1), TimeSpan.Zero) }, new { Id = Guid.Parse("a1b2c3d4-0000-0000-0000-000000000003"), Name = "Sci-Fi", Created = new DateTimeOffset(new DateTime(2026, 1, 1), TimeSpan.Zero), LastModified = new DateTimeOffset(new DateTime(2026, 1, 1), TimeSpan.Zero) } ); }}Then in MovieConfiguration, reference the genre by its foreign key - not the navigation property:
// MovieConfiguration.cs - reference Genre by FKbuilder.HasData( new { Id = Guid.Parse("d1a7b9c3-4e56-4f89-a123-b456c789d012"), Title = "The Shawshank Redemption", GenreId = Guid.Parse("a1b2c3d4-0000-0000-0000-000000000001"), // FK to Drama ReleaseDate = new DateTimeOffset(new DateTime(1994, 9, 23), TimeSpan.Zero), Rating = 9.3, Created = new DateTimeOffset(new DateTime(2026, 1, 1), TimeSpan.Zero), LastModified = new DateTimeOffset(new DateTime(2026, 1, 1), TimeSpan.Zero) });The key takeaway: you reference GenreId (the foreign key property), not Genre (the navigation property). EF Core will enforce referential integrity in the generated migration - if a Genre with the specified Id doesn’t exist in HasData, the migration will fail.
With UseSeeding: Order Matters
When using UseSeeding with related entities, you must seed parent entities first, then children:
.UseAsyncSeeding(async (context, _, cancellationToken) =>{ // 1. Seed genres first (parent entity) if (!await context.Set<Genre>().AnyAsync(cancellationToken)) { var genres = new[] { Genre.Create("Drama"), Genre.Create("Action"), Genre.Create("Sci-Fi") }; await context.Set<Genre>().AddRangeAsync(genres, cancellationToken); await context.SaveChangesAsync(cancellationToken); }
// 2. Then seed movies (child entity) - genre already exists if (!await context.Set<Movie>().AnyAsync(cancellationToken)) { var drama = await context.Set<Genre>() .FirstAsync(g => g.Name == "Drama", cancellationToken);
var movie = Movie.Create("The Shawshank Redemption", drama.Id, new DateTimeOffset(new DateTime(1994, 9, 23), TimeSpan.Zero), 9.3);
await context.Set<Movie>().AddAsync(movie, cancellationToken); await context.SaveChangesAsync(cancellationToken); }})Notice the separate SaveChangesAsync calls - the first one commits genres to the database so they’re available when seeding movies. If you combine everything into a single SaveChanges, EF Core might try to insert the movie before the genre exists, resulting in a foreign key violation.
Environment-Specific Seeding
One of UseSeeding’s biggest advantages over HasData is environment awareness. You almost certainly want different seed data in development, staging, and production.
In development, you want a rich dataset for testing - lots of records, edge cases, maybe some intentionally diverse data to test pagination and filtering. In production, you want only the essential reference data - roles, permissions, default categories.
The custom Program.cs approach gives you full DI access, which includes IHostEnvironment:
await using (var scope = app.Services.CreateAsyncScope()){ var dbContext = scope.ServiceProvider.GetRequiredService<MovieDbContext>(); var environment = scope.ServiceProvider.GetRequiredService<IHostEnvironment>();
await dbContext.Database.MigrateAsync();
if (!await dbContext.Movies.AnyAsync()) { // Always seed essential reference data var essentialMovies = new[] { Movie.Create("The Shawshank Redemption", "Drama", new DateTimeOffset(new DateTime(1994, 9, 23), TimeSpan.Zero), 9.3), Movie.Create("The Dark Knight", "Action", new DateTimeOffset(new DateTime(2008, 7, 18), TimeSpan.Zero), 9.0) }; await dbContext.Movies.AddRangeAsync(essentialMovies);
// Development-only: add test data for pagination and filtering if (environment.IsDevelopment()) { var testMovies = Enumerable.Range(1, 50) .Select(i => Movie.Create( $"Test Movie {i}", i % 3 == 0 ? "Drama" : i % 3 == 1 ? "Action" : "Sci-Fi", new DateTimeOffset(new DateTime(2020, 1, i % 28 + 1), TimeSpan.Zero), Math.Round(i % 10 * 1.0, 1))) .ToArray(); await dbContext.Movies.AddRangeAsync(testMovies); }
await dbContext.SaveChangesAsync(); }}This pattern ensures production databases get only the data they need, while development databases have enough records to test pagination, sorting, and searching properly.
PRO TIP: Never seed admin accounts with hardcoded passwords in production. I’ve seen this in more codebases than I’d like to admit. Use environment variables or a secure vault for initial credentials.
My Take: Which Approach Should You Use?
Here’s my decision matrix - but context matters more than any table:
| Criteria | HasData | UseSeeding | Custom Program.cs |
|---|---|---|---|
| Migration safety | High (tracked in history) | Medium (runs every startup) | Medium (runs every startup) |
| Idempotency | Automatic | Manual (you implement) | Manual (you implement) |
| Environment flexibility | None (same everywhere) | Limited (no DI access) | Full (DI + IHostEnvironment) |
| DDD compatibility | Poor (anonymous objects) | Excellent (factory methods) | Excellent (factory methods) |
| Migration bloat | Yes (every change = migration) | None | None |
| Rollback support | Yes (Down migration) | No | No |
| Production readiness | Best for reference data | Good for app data | Good for complex scenarios |
My Recommendations
Use HasData for data that is essentially part of your schema - role definitions, status enums stored in tables, country codes, permission types. This data should exist in every environment and rarely changes. The migration history gives you a clear audit trail, and the Down migration can cleanly remove it if needed.
Use UseSeeding/UseAsyncSeeding for application-level seed data - default categories, sample records for development, initial configuration values. This is my go-to for most scenarios in EF Core 10. It respects your DDD patterns by calling factory methods, doesn’t bloat migration files, and handles the common case cleanly.
Use custom Program.cs seeding when you need DI services during seeding - external API calls to fetch reference data, logging, seeding across multiple DbContexts, or complex business logic that requires injected services.
My take: start with UseSeeding - it covers 80% of scenarios. Upgrade to custom Program.cs seeding when you need DI access, and reserve HasData for true reference data that belongs in your migration history. The mistake I see most devs make is using HasData for everything. They end up with migrations full of seed data updates, anonymous objects that are painful to maintain, and no way to seed different data per environment.
Common Pitfalls and Troubleshooting
HasData Generates Empty Migrations
Problem: You run dotnet ef migrations add and get a migration with no changes, even though you added HasData.
Solution: Make sure HasData is called inside the same configuration class that’s loaded by ApplyConfigurationsFromAssembly. If your configuration isn’t being discovered, the seed data is silently ignored. Verify that OnModelCreating includes:
modelBuilder.ApplyConfigurationsFromAssembly(typeof(MovieDbContext).Assembly);Also check that your configuration class is public - internal classes won’t be discovered by the assembly scan.
UseSeeding Creates Duplicate Records
Problem: Every time the app restarts, duplicate records appear in the database.
Solution: Your idempotency check is missing or incorrect. Always check if records exist before inserting:
if (!await context.Set<Movie>().AnyAsync(cancellationToken))For individual records, check by a unique business property - not by Id, which changes per run with Guid.NewGuid().
HasData Fails with Private Constructors
Problem: EF Core throws an error like “The seed entity for entity type ‘Movie’ cannot be added” when you try to use HasData with entity instances.
Solution: Use anonymous objects instead of entity instances. EF Core maps properties by name:
// Won't work with private constructorsbuilder.HasData(Movie.Create("Title", "Genre", date, 9.0));
// Works - use anonymous objects with all required propertiesbuilder.HasData(new { Id = Guid.Parse("..."), Title = "...", Genre = "...", ReleaseDate = date, Rating = 9.0, Created = timestamp, LastModified = timestamp });Seeded Data Gets Overwritten by Migrations
Problem: You modified a seeded record through the API, but the next migration resets it to the original value.
Solution: This is HasData working as designed. If you change a seed value in code, EF Core generates an UPDATE migration that will overwrite the current database value. Either accept this behavior (correct for reference data that shouldn’t be user-modified) or switch to UseSeeding for data that users might change.
UseSeeding Doesn’t Run
Problem: You implemented UseAsyncSeeding but no data appears in the database.
Solution: Check two things:
- Are you calling
EnsureCreatedAsync()orMigrateAsync()at startup? The seeding callback only runs during these calls - it won’t fire on its own. - Did you also implement
UseSeeding(synchronous version)? If any code path calls the syncEnsureCreated, the async callback won’t fire for that path.
Foreign Key Violations During HasData Seeding
Problem: Migration fails with a foreign key constraint error when seeding related entities.
Solution: Make sure the referenced parent entity’s seed data exists. If a Genre with Id X doesn’t exist in GenreConfiguration.HasData(), you can’t reference it as GenreId = X in MovieConfiguration.HasData(). Both must be defined - EF Core handles the insert order automatically in the generated migration, but both sides must be present.
Key Takeaways
- HasData bakes seed data into migration files - use it for stable reference data that’s part of your schema (roles, statuses, lookup tables) and rarely changes
- UseSeeding/UseAsyncSeeding (EF Core 9+) is the recommended approach for runtime seeding - it respects DDD patterns, doesn’t create migration bloat, and supports conditional logic
- Always implement idempotency in
UseSeedingcallbacks - check if data exists before inserting, because the callback runs on every startup whenEnsureCreatedorMigrateis called - Fixed primary keys are mandatory for
HasData- dynamic GUIDs cause delete/insert cycles on every migration - Environment-specific seeding belongs in custom
Program.cslogic where you have access toIHostEnvironmentand the full DI container
What is data seeding in Entity Framework Core?
Data seeding is the process of populating a database with an initial set of data when the database is first created or when migrations are applied. EF Core provides three approaches: HasData for migration-based seeding, UseSeeding/UseAsyncSeeding for runtime seeding, and custom Program.cs logic for scenarios requiring dependency injection.
What is the difference between HasData and UseSeeding in EF Core?
HasData bakes seed data into migration files as INSERT statements and runs during dotnet ef database update. It handles idempotency automatically but requires anonymous objects and fixed primary keys. UseSeeding runs at application startup during EnsureCreated or Migrate calls, supports entity factory methods and conditional logic, but requires you to implement idempotency checks manually.
Can I seed data without creating a migration in EF Core?
Yes. UseSeeding and UseAsyncSeeding (available since EF Core 9) let you seed data at runtime without generating migration files. You configure these callbacks in DbContext.OnConfiguring, and they execute during EnsureCreated or Migrate calls. Custom Program.cs seeding also works without migrations.
How do I seed related entities with foreign keys using HasData?
With HasData, you must use foreign key values directly instead of navigation properties. Seed the parent entity first with a fixed GUID primary key, then reference that GUID as the foreign key value in the child entity HasData call. EF Core handles the insert order automatically in the generated migration.
Does HasData run every time the application starts?
No. HasData generates INSERT statements inside migration files that run only when you execute dotnet ef database update or call context.Database.Migrate(). The data is inserted once when the migration is applied. However, UseSeeding callbacks run every time EnsureCreated or Migrate is called, which is why idempotency checks are mandatory.
How do I seed different data for development and production environments?
Use the custom Program.cs seeding approach where you have access to IHostEnvironment through the DI container. Check environment.IsDevelopment() to conditionally seed test data in development while only seeding essential reference data in production. HasData cannot do this because it generates the same migration for all environments.
What happens if I change a seeded entity property value in HasData?
EF Core detects the change and generates a new migration with an UpdateData statement. When applied, this migration updates the record in the database to match the new value in code. If someone modified that record through the API, the migration will overwrite their changes.
Can I use HasData with auto-generated GUID primary keys?
You must use fixed hardcoded GUIDs with HasData, not auto-generated ones. If you use Guid.NewGuid() in your entity base class, HasData requires you to specify the exact GUID value using anonymous objects. Without fixed GUIDs, every migration sees new data and generates delete plus insert operations instead of recognizing existing records.
Summary
Data seeding is one of those things that seems simple until you pick the wrong approach and spend a weekend debugging migration conflicts or duplicate records. The three strategies I covered - HasData for migration-tracked reference data, UseSeeding/UseAsyncSeeding for runtime application data, and custom Program.cs for complex DI-dependent scenarios - each have clear use cases.
My default recommendation: start with UseSeeding/UseAsyncSeeding for most scenarios. It’s clean, respects your DDD patterns, and doesn’t bloat your migration history. Reserve HasData for true schema-level reference data like role definitions or status enums, and reach for custom Program.cs seeding only when you need the DI container.
The complete source code for this article is available in the GitHub repository linked above. Clone it, run the Docker container, and see seeding in action.
If you found this helpful, share it with your colleagues - and if there’s a seeding strategy I missed or a gotcha you’ve run into, drop a comment and let me know.
Happy Coding :)



What's your Feedback?
Do let me know your thoughts around this article.