EFEF Core Handbook

ORTA

Seed Data (Başlangıç Verisi)

Veritabanı ilk oluştuğunda veya test ortamında ihtiyaç duyulan başlangıç verilerini (kategoriler, roller, ülke listesi gibi) otomatik eklemek için kullanılır.

Veritabanı sağlayıcısı Bu sayfadaki eşleşen örnekleri seçilen sağlayıcıya göre gösterir.

HasData — Migration-Based Seed

Kritik: HasData runtime'da çalışmaz. Migration dosyasına SQL olarak yazılır ve migration sadece bir kez uygulanır.

  • Kayıt zaten varsa → tekrar eklenmez (migration bir daha çalışmaz)
  • Veriyi silersen → geri gelmez (migration tekrar uygulanmaz)
  • HasData'daki değeri değiştirirsen → yeni migration oluşturduğunda UPDATE üretilir

🚨 HasData'da ASLA dinamik değer kullanma:

  • DateTime.UtcNow, DateTimeOffset.Now → Migration derleme anındaki sabit değer yazılır (her migrations add'da farklı SQL üretir!)
  • Guid.NewGuid() → Her migration'da yeni GUID = gereksiz UPDATE SQL'leri
  • Doğrusu: new DateTime(2024, 1, 1) veya new Guid("sabit-guid-string") gibi deterministik değerler kullan
public class CategoryConfiguration : IEntityTypeConfiguration<Category>
{
    public void Configure(EntityTypeBuilder<Category> builder)
    {
        builder.ToTable("Categories");
        builder.HasKey(c => c.Id);
        builder.Property(c => c.Name).IsRequired().HasMaxLength(100);

        // Seed data — migration'a dahil edilir
        builder.HasData(
            new Category { Id = 1, Name = "Elektronik" },
            new Category { Id = 2, Name = "Giyim" },
            new Category { Id = 3, Name = "Kitap" },
            new Category { Id = 4, Name = "Ev & Yaşam" }
        );
    }
}

Migration'da oluşan SQL:

-- Up metodu
INSERT INTO [Categories] ([Id], [Name]) VALUES (1, N'Elektronik');
INSERT INTO [Categories] ([Id], [Name]) VALUES (2, N'Giyim');
INSERT INTO [Categories] ([Id], [Name]) VALUES (3, N'Kitap');
INSERT INTO [Categories] ([Id], [Name]) VALUES (4, N'Ev & Yaşam');

-- IDENTITY_INSERT otomatik açılır/kapanır (Id belirttiğimiz için)
SET IDENTITY_INSERT [Categories] ON;
-- ... insert'ler ...
SET IDENTITY_INSERT [Categories] OFF;
-- Up metodu
INSERT INTO categories (id, name) OVERRIDING SYSTEM VALUE VALUES (1, 'Elektronik');
INSERT INTO categories (id, name) OVERRIDING SYSTEM VALUE VALUES (2, 'Giyim');
INSERT INTO categories (id, name) OVERRIDING SYSTEM VALUE VALUES (3, 'Kitap');
INSERT INTO categories (id, name) OVERRIDING SYSTEM VALUE VALUES (4, 'Ev & Yaşam');

-- GENERATED ALWAYS sütununa manuel değer yazmak için OVERRIDING SYSTEM VALUE gerekir
-- Sequence değerini güncellemek için EF otomatik ekler:
SELECT setval(pg_get_serial_sequence('categories', 'id'), MAX(id)) FROM categories;

Kurallar:

  • HasData ile PK değerini zorunlu olarak belirtmek gerekir (Id = 1, 2, ...)
  • Navigation property kullanılamaz — FK değerini doğrudan yaz
  • Owned entity seed'lemek için anonim tip kullan
// İlişkili veri seed'leme — FK ile
builder.HasData(
    new Product { Id = 1, Name = "Laptop", CategoryId = 1 },
    new Product { Id = 2, Name = "T-shirt", CategoryId = 2 }
);

// Owned entity seed — anonim tip
builder.OwnsOne(c => c.Address).HasData(
    new { CustomerId = 1, Street = "Atatürk Cad.", City = "İstanbul" }
);

UseSeeding — Koşullu Seed (EF Core 9+)

Özellik HasData UseSeeding (EF Core 9+)
Migration gerektir Evet Hayır
Koşullu seed ("yoksa ekle")
Async desteği
Navigation property FK ile manuel Normal entity gibi
Ne zaman çalışır Migration uygulanırken (bir kez) MigrateAsync() / EnsureCreated() her çağrısında
builder.Services.AddDbContext<AppDbContext>((sp, options) =>
    options.UseSqlServer(connectionString)
           .UseAsyncSeeding(async (context, _, ct) =>
           {
               if (!await context.Set<Category>().AnyAsync(ct))
               {
                   context.Set<Category>().AddRange(
                       new Category { Name = "Elektronik", Slug = "elektronik" },
                       new Category { Name = "Giyim", Slug = "giyim" },
                       new Category { Name = "Kitap", Slug = "kitap" }
                   );
                   await context.SaveChangesAsync(ct);
               }
           }));

// Çalıştırma — MigrateAsync içinde otomatik tetiklenir
await context.Database.MigrateAsync();

Hangisini ne zaman kullan:

  • Sabit lookup tabloları (ülkeler, roller, para birimleri) → HasData (deterministik, migration'da)
  • Koşullu / dinamik seed (admin kullanıcı yoksa oluştur) → UseSeeding

PostgreSQL Seed Farkları:

  • HasData her iki provider'da aynı Fluent API ile çalışır (SQL farkını EF halleder)
  • PostgreSQL'de HasData üretilen SQL: INSERT INTO ... ON CONFLICT DO NOTHING değil, INSERT INTO düzdür (migration'da idempotent değil!)

HasDefaultValueSql provider farkları:

builder.Property(p => p.CreatedAt).HasDefaultValueSql("GETUTCDATE()");
builder.Property(p => p.Id).HasDefaultValueSql("NEWSEQUENTIALID()");
builder.Property(p => p.CreatedAt).HasDefaultValueSql("NOW() AT TIME ZONE 'UTC'");
builder.Property(p => p.Id).HasDefaultValueSql("gen_random_uuid()");

Upsert (varsa güncelle, yoksa ekle) — Raw SQL karşılaştırması:

// SQL Server: MERGE statement
await context.Database.ExecuteSqlRawAsync(@"
    MERGE INTO [Categories] AS target
    USING (VALUES (1, N'Elektronik', 'elektronik'), (2, N'Giyim', 'giyim'))
        AS source (Id, Name, Slug)
    ON target.Id = source.Id
    WHEN MATCHED THEN UPDATE SET Name = source.Name, Slug = source.Slug
    WHEN NOT MATCHED THEN INSERT (Id, Name, Slug) VALUES (source.Id, source.Name, source.Slug);
");
// PostgreSQL: ON CONFLICT (çok daha temiz syntax)
await context.Database.ExecuteSqlRawAsync(@"
    INSERT INTO categories (id, name, slug)
    VALUES (1, 'Elektronik', 'elektronik'),
           (2, 'Giyim', 'giyim')
    ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, slug = EXCLUDED.slug;
");