Start Debugging

HasData vs UseSeeding for seeding data in EF Core 11: which should you use?

Use HasData only for fixed, model-owned reference data. Use UseSeeding and UseAsyncSeeding for everything else in EF Core 11. A side-by-side comparison with the rules that force the decision.

For seeding data in EF Core 11, use HasData only for small, fixed, deterministic reference tables that you want versioned inside your migrations (country codes, currencies, enum-like lookup rows). Use UseSeeding and UseAsyncSeeding for everything else: anything with database-generated keys, computed or non-deterministic values, conditional logic, navigation properties, or rows that depend on what is already in the database. The EF team renamed HasData to “model-managed data” precisely to discourage using it as a general seeding tool. This post uses .NET 11, EF Core 11 (Microsoft.EntityFrameworkCore 11.0), and C# 14.

If you only remember one line: HasData participates in the model and the migration diff, UseSeeding runs as ordinary code against a live DbContext. Almost every painful seeding bug comes from picking the first when you needed the second.

The feature matrix

This is the table you came for. Every row is a real constraint, not a vibe.

FeatureHasData (model-managed data)UseSeeding / UseAsyncSeeding
Configured onOnModelCreating (the model)DbContextOptionsBuilder
When it runsInside migration SQL (InsertData)On Migrate/EnsureCreated and dotnet ef database update
Primary keysMust be specified by handDatabase-generated keys are fine
Non-deterministic valuesForbidden (re-diffs every build)Allowed (Guid.NewGuid(), DateTime.UtcNow)
Navigation propertiesNo, foreign keys onlyYes, full graph inserts
Conditional logicNoYes, you write the if
External calls / transformsNoYes (hashing, HTTP, file reads)
IdempotencyAutomatic (diffed by EF)You write the existence check
Captured in source controlYes, in the migration snapshotNo, it is startup code
Updates existing rowsYes, via migration diffOnly if you write the update
Available sinceEF Core 1.0 (was HasData)EF Core 9, current in EF Core 11

The single most important row is “non-deterministic values”. HasData is compared against the model snapshot on every build, so a DateTime.UtcNow or Guid.NewGuid() in your seed makes the model look changed every time, and EF throws PendingModelChangesWarning or generates an endless trickle of pointless migrations. That is the wall most people hit, usually during a migration from EF Core 6 to EF Core 11.

When to pick HasData

HasData is the right tool when the data is genuinely part of your schema’s meaning. The mental test: would you be comfortable hard-coding these rows in a constant, and will they almost never change?

// .NET 11, EF Core 11 (Microsoft.EntityFrameworkCore 11.0), C# 14
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<OrderStatus>().HasData(
        new OrderStatus { Id = 1, Name = "Pending" },
        new OrderStatus { Id = 2, Name = "Shipped" },
        new OrderStatus { Id = 3, Name = "Delivered" });
}

The keys must be set explicitly and the values must be deterministic. No Guid.NewGuid(), no DateTime.Now, no navigation properties (set the foreign key column directly). Break any of those and HasData will fight you on every build.

When to pick UseSeeding and UseAsyncSeeding

UseSeeding is the recommended general-purpose mechanism in EF Core 11. It is plain application code with a live DbContext, so the limits of HasData simply do not exist. Reach for it whenever any of these is true:

// .NET 11, EF Core 11 (Microsoft.EntityFrameworkCore 11.0), C# 14
services.AddDbContext<AppDbContext>(options =>
    options
        .UseSqlServer(connectionString)
        .UseAsyncSeeding(async (context, _, ct) =>
        {
            if (!await context.Set<User>().AnyAsync(u => u.Email == "admin@example.com", ct))
            {
                context.Set<User>().Add(new User
                {
                    Email = "admin@example.com",
                    PasswordHash = PasswordHasher.Hash("change-me"), // runtime transform
                    CreatedAt = DateTime.UtcNow                        // non-deterministic
                });
                await context.SaveChangesAsync(ct);
            }
        })
        .UseSeeding((context, _) =>
        {
            if (!context.Set<User>().Any(u => u.Email == "admin@example.com"))
            {
                context.Set<User>().Add(new User
                {
                    Email = "admin@example.com",
                    PasswordHash = PasswordHasher.Hash("change-me"),
                    CreatedAt = DateTime.UtcNow
                });
                context.SaveChanges();
            }
        }));

The catch is the one most people miss: you have to implement both overloads, and the existence check is on you. EF Core tooling (dotnet ef database update) only calls the synchronous UseSeeding; your runtime path calls UseAsyncSeeding. Implement one and not the other and seeding silently does nothing from whichever path you forgot. For the full mechanics, see how to seed data with UseSeeding and UseAsyncSeeding.

What actually happens on disk

The clearest way to see the difference is to look at what each one produces.

HasData materializes into your migration. After dotnet ef migrations add SeedStatuses, the generated Up method contains literal SQL-shaped calls:

// .NET 11, EF Core 11 generated migration
migrationBuilder.InsertData(
    table: "OrderStatuses",
    columns: new[] { "Id", "Name" },
    values: new object[,]
    {
        { 1, "Pending" },
        { 2, "Shipped" },
        { 3, "Delivered" }
    });

The data is now a versioned artifact. It runs once, inside the migration’s transaction, and EF tracks it in the model snapshot so a later edit produces an UpdateData. That is exactly why a non-deterministic value is poison here: the snapshot would never match, so EF would believe the model changed on every single build.

UseSeeding produces nothing on disk. It is a callback that fires after the schema is up to date, on every Migrate, EnsureCreated, or dotnet ef database update, including runs where no migration was applied. Because it runs unconditionally, the existence check is not optional housekeeping, it is the only thing standing between you and duplicate rows. EF Core 11 does protect the callback with the same migration locking mechanism it uses for migrations, so two app instances starting at once will not both run the seed concurrently, but locking does not make a missing if idempotent.

The gotcha that picks for you

A few constraints override preference entirely. If any of these applies, the decision is made for you:

You can absolutely use both in one app. A common, healthy split: HasData for the three lookup tables your schema cannot function without, and UseSeeding for the demo data, the default admin, and anything tenant-specific. They do not conflict, because they run at different stages.

The recommendation, restated

Default to UseSeeding and UseAsyncSeeding in EF Core 11. They are the recommended mechanism, they handle the dynamic, key-generated, conditional data that real applications actually seed, and they fail loudly instead of silently corrupting your migration history. Reserve HasData for the narrow case it was renamed to serve: small, fixed, deterministic reference data with hand-assigned keys that you genuinely want versioned and diffed alongside your schema.

The trap to avoid is the historical default. For years HasData was the only built-in option, so codebases reached for it reflexively and then drowned in phantom migrations and PendingModelChangesWarning. If you are starting fresh on EF Core 11, invert that instinct: UseSeeding first, HasData only when you can name why the data belongs in the model. If you maintain records-based entities, the same split applies cleanly, since records work fine with EF Core 11 under both mechanisms.

Sources

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Back