# Entity Framework Core - Data Seeding

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.<sup>1</sup> 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").<sup>2</sup>
    
* Data that *must* exist for the application to function and rarely changes.
    

**How it works:**

```csharp
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.<sup>3</sup>
    
* **Automatic:** Applied automatically when updating the database.<sup>4</sup>
    

**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).<sup>5</sup>
    
* **No Logic:** We cannot use services (like `UserManager` for identity) or dynamic logic (like `DateTime.Now`).<sup>6</sup>
    

---

### **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).<sup>7</sup>
    
* **Dynamic Data:** Data needing `DateTime.Now` or logic.<sup>8</sup>
    
* **Large Datasets:** Inserting 10,000+ rows (performance is better than `HasData`).
    

**How it works:**

1. **Create a Seeder Interface/Service:**
    
    ```csharp
    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`:
    
    ```csharp
    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

```csharp
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**

| **Scenario** | **Recommended Strategy** | **Why?** |
| --- | --- | --- |
| **Lookup Tables** (Status, Country) | **Strategy 1 (**`HasData`) | Keeps static data consistent across all environments via migrations. |
| **Users / Roles / Identity** | **Strategy 2 (Seeder Service)** | Requires `UserManager` for password hashing; `HasData` cannot do this securely. |
| **Dev / Test Dummy Data** | **Strategy 2 (Seeder Service)** | Use libraries like **Bogus** to generate fake data only when `Environment.IsDevelopment()`. |
| **Massive Data (ZipCodes)** | **SQL Script / Bulk Insert** | EF 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.

```csharp
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](https://github.com/bchavez/Bogus) library.

```csharp
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.

```csharp
// 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.

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