Skip to main content

Command Palette

Search for a command to run...

Entity Framework Core - Data Seeding

Updated
4 min readView as Markdown

For EF Core, there isn't one "correct" way to seed data; the best strategy depends entirely on what kind of data we are seeding (static lookups vs. transactional test data).

Below is a breakdown of the three primary strategies, when to use them, and the industry-standard patterns for production applications.


Strategy 1: Model-Managed Seeding (HasData)

This is the native EF Core method. We define the data in our OnModelCreating method.1 EF Core tracks this data in our Migrations, meaning it will generate INSERT, UPDATE, or DELETE SQL scripts automatically when we run dotnet ef database update.

Best For:

  • Static Lookup Tables: Countries, Currency Codes, Status Types (e.g., "Pending", "Approved").2

  • Data that must exist for the application to function and rarely changes.

How it works:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<OrderStatus>().HasData(
        new OrderStatus { Id = 1, Name = "Pending" },
        new OrderStatus { Id = 2, Name = "Shipped" }
    );
}

Pros:

  • Version Controlled: Changes to data are tracked in migration files.3

  • Automatic: Applied automatically when updating the database.4

Cons:

  • Migration Bloat: If we seed 1,000 rows, our migration file becomes massive and slow.

  • Strict PK Requirement: We must manually specify the Primary Key (ID).5

  • No Logic: We cannot use services (like UserManager for identity) or dynamic logic (like DateTime.Now).6


Strategy 2: Custom Initialization Logic (The "Startup Pattern")

Industry Standard for Enterprise Apps

Instead of using EF's internal configuration, we create a dedicated "Seeder" service. we manually invoke this service during the application startup (in Program.cs) immediately after the database migration runs.

Best For:

  • Identity Data: Creating Admin users and Roles (requires UserManager to hash passwords securely).7

  • Dynamic Data: Data needing DateTime.Now or logic.8

  • Large Datasets: Inserting 10,000+ rows (performance is better than HasData).

How it works:

  1. Create a Seeder Interface/Service:

     public interface IDbSeeder
     {
         Task SeedAsync();
     }
    
     public class DbSeeder : IDbSeeder
     {
         private readonly AppDbContext _context;
         private readonly UserManager<AppUser> _userManager;
    
         public DbSeeder(AppDbContext context, UserManager<AppUser> userManager)
         {
             _context = context;
             _userManager = userManager;
         }
    
         public async Task SeedAsync()
         {
             // 1. Check if data exists (Idempotency)
             if (await _context.Roles.AnyAsync()) return;
    
             // 2. Add Data safely
             await _userManager.CreateAsync(new AppUser { UserName = "admin" }, "Pa$$w0rd");
         }
     }
    
  2. Call it in Program.cs:

     var app = builder.Build();
    
     // Create a scope to resolve scoped services
     using (var scope = app.Services.CreateScope())
     {
         var seeder = scope.ServiceProvider.GetRequiredService<IDbSeeder>();
         await seeder.SeedAsync();
     }
    
     app.Run();
    

Pros:

  • Flexible: Can use Dependency Injection (DI) to access other services.

  • Clean: Keeps our DbContext clean of data concerns.

  • Secure: Best way to handle password hashing for Identity.


Strategy 3: EF Core 9+ UseSeeding (The New Standard)

If we are on .NET 9 or newer, EF Core introduced a dedicated hook for seeding that combines the ease of Strategy 1 with the power of Strategy 2.

Best For:

  • Modern .NET 9+ applications wanting a standardized place for seeding without modifying Program.cs.

How it works:

We configure this directly in our DbContext setup options.9

options.UseSeeding((context, _) =>
{
    var testData = context.Set<Product>().FirstOrDefault(p => p.Name == "Test Product");
    if (testData == null)
    {
        context.Set<Product>().Add(new Product { Name = "Test Product", Price = 10 });
        context.SaveChanges();
    }
});
options.UseAsyncSeeding(async (context, _, cancellationToken) =>
{
    // Async logic here
});

Summary of Use Cases

ScenarioRecommended StrategyWhy?
Lookup Tables (Status, Country)Strategy 1 (HasData)Keeps static data consistent across all environments via migrations.
Users / Roles / IdentityStrategy 2 (Seeder Service)Requires UserManager for password hashing; HasData cannot do this securely.
Dev / Test Dummy DataStrategy 2 (Seeder Service)Use libraries like Bogus to generate fake data only when Environment.IsDevelopment().
Massive Data (ZipCodes)SQL Script / Bulk InsertEF Core change tracking is too slow for millions of rows. Use raw SQL or SqlBulkCopy.

Industry Standard Practices

1. Idempotency is King

Our seed logic must be runnable multiple times without crashing or creating duplicates.

  • Bad: context.Add(new Role("Admin")); (Crashes on 2nd run)

  • Good: if (!context.Roles.Any(r => r.Name == "Admin")) { ... }

2. Environment Separation

Do not seed fake data in Production.

public async Task SeedAsync()
{
    // Always seed these (Lookups)
    await SeedLookupsAsync();

    // Only seed these in Dev (Fake Users)
    if (_environment.IsDevelopment())
    {
        await SeedFakeDataAsync();
    }
}

3. Use "Bogus" for Dummy Data

Don't write manual loops to create fake users. Use the Bogus library.

var faker = new Faker<User>()
    .RuleFor(u => u.FirstName, f => f.Name.FirstName())
    .RuleFor(u => u.Email, f => f.Internet.Email());

var users = faker.Generate(100); // Generates 100 realistic fake users

4. Extension Methods for Cleanup

If we use HasData, do not put 500 lines of code inside OnModelCreating. Move it to an extension method.

// In OnModelCreating
modelBuilder.SeedCountries(); // Much cleaner

5. Disable Change Tracking for Bulk Seeds

If seeding large data via a custom seeder, performance will tank if EF Core tracks every entity.

// Optimizes performance for large inserts
_context.ChangeTracker.AutoDetectChangesEnabled = false;
await _context.Products.AddRangeAsync(largeList);
await _context.SaveChangesAsync();
_context.ChangeTracker.AutoDetectChangesEnabled = true;

More from this blog

E

EF Core

31 posts