EF Core Code First: Migration Ekleme ve Geri Alma Rehberi
Yazar: Oğuzhan KARAGÜZEL ·
Code First yaklaşımında önce C# sınıflarını yazarsınız, veritabanını ise EF Core'un migration mekanizmasıyla bu sınıflardan üretirsiniz. Bu yazıda sıfırdan bir market modeli kuruyor, ilk migration'ı oluşturup veritabanına uyguluyor, ardından modelde değişiklik yapıp bu değişikliği geri alıyoruz. Serinin ilk yazısı olan EF Core ve DB First yazısındaki ortam hazırlığını (SQL Server, NuGet paketleri) tamamladığınızı varsayıyorum.
Code First nedir?
Code First, önce kodun yazıldığı ve veritabanı yapısının bu koda göre oluşturulduğu bir yöntemdir. Daha fazla esneklik ve kontrol sağlar: veritabanı değişikliklerini uygulamadan önce migration dosyası olarak görüp inceleyebilir, sorun yoksa veritabanına uygulayabilir ve yaptığınız son değişikliği kolayca geri alabilirsiniz. Migration dosyaları kodla birlikte Git'te tutulduğu için veritabanının geçmişi de kodun geçmişiyle birlikte izlenir.
Temiz bir başlangıç yapmak
DB First yazısında kullandığımız veritabanını ve üretilen sınıfları silip temiz başlayabilirsiniz. Veritabanını Package Manager Console'dan silmek için aşağıdaki komut yeterlidir; EF Core silme işleminden önce onay ister.
Drop-Database.NET CLI karşılığı: dotnet ef database drop. Ardından projedeki Context ve Entities klasörlerini de silerek sıfır bir uygulama ile devam edin.
Adım 1: Entity sınıfları ve DbContext
Önce tablolarımıza karşılık gelecek entity'leri yazıyoruz. Modelimizde dört sınıf var: Musteri, Siparis, Urun ve Fatura. İlişkiler şöyle: bir müşterinin birden çok siparişi olur (bire çok), bir siparişin tek faturası vardır (bire bir), bir sipariş birden çok ürün içerir ve bir ürün birden çok siparişte yer alır (çoka çok). Hepsi ortak bir BaseEntity sınıfından türüyor.
public abstract class BaseEntity
{
public int Id { get; set; }
public DateTime CreatedDate { get; set; } = DateTime.UtcNow;
}
public class Musteri : BaseEntity
{
public string MusteriAdi { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public ICollection<Siparis> Siparisler { get; set; } = new List<Siparis>();
}
public class Urun : BaseEntity
{
public string UrunAdi { get; set; } = string.Empty;
public decimal Fiyat { get; set; }
public ICollection<Siparis> Siparisler { get; set; } = new List<Siparis>();
}
public class Siparis : BaseEntity
{
public DateTime SiparisTarihi { get; set; }
public int MusteriId { get; set; }
public Musteri Musteri { get; set; } = null!;
public Fatura? Fatura { get; set; }
public ICollection<Urun> Urunler { get; set; } = new List<Urun>();
}
public class Fatura : BaseEntity
{
public DateTime FaturaTarihi { get; set; }
public int SiparisId { get; set; }
public Siparis Siparis { get; set; } = null!;
}Dikkat: Bire bir ilişkide yabancı anahtar yalnızca bir tarafta bulunmalıdır. Burada Fatura.SiparisId bu görevi görür; Siparis sınıfına ayrıca bir FaturaId eklemek gereksiz ve yanıltıcı bir sütun üretir.
Sonra DbContext sınıfını yazıyoruz. İlişkileri OnModelCreating içinde Fluent API ile açıkça belirtiyoruz. Kod yapısı EF Core'un adlandırma kurallarına uygunsa bu yapılandırmaların çoğu zaten otomatik algılanır; ama özellikle bire bir ilişkilerde ve para birimi gibi alanlarda (HasPrecision) açık yazmak güvenlidir.
public class MarketDbContext : DbContext
{
public MarketDbContext() { }
public MarketDbContext(DbContextOptions<MarketDbContext> options)
: base(options) { }
public DbSet<Urun> Urunler => Set<Urun>();
public DbSet<Siparis> Siparisler => Set<Siparis>();
public DbSet<Fatura> Faturalar => Set<Fatura>();
public DbSet<Musteri> Musteriler => Set<Musteri>();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// Bağlantı dizesini koda gömme: ortam değişkeninden okuyoruz.
if (!optionsBuilder.IsConfigured)
{
var connectionString = Environment.GetEnvironmentVariable("MARKETDB_CONNECTION")
?? throw new InvalidOperationException("MARKETDB_CONNECTION tanımlı değil.");
optionsBuilder.UseSqlServer(connectionString);
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Çoka çok: Siparis <-> Urun (ara tabloyu EF Core üretir)
modelBuilder.Entity<Siparis>()
.HasMany(s => s.Urunler)
.WithMany(u => u.Siparisler);
// Bire çok: Musteri -> Siparis
modelBuilder.Entity<Siparis>()
.HasOne(s => s.Musteri)
.WithMany(m => m.Siparisler)
.HasForeignKey(s => s.MusteriId);
// Bire bir: Siparis -> Fatura (FK Fatura tarafında)
modelBuilder.Entity<Fatura>()
.HasOne(f => f.Siparis)
.WithOne(s => s.Fatura)
.HasForeignKey<Fatura>(f => f.SiparisId);
modelBuilder.Entity<Urun>()
.Property(u => u.Fiyat)
.HasPrecision(18, 2);
// Başlangıç verisi (seed). Sabit tarih kullanın: DateTime.Now/UtcNow
// her derlemede farklı değer üretip gereksiz migration oluşturur.
var seedDate = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
modelBuilder.Entity<Musteri>().HasData(
new Musteri { Id = 1, MusteriAdi = "Ahmet Yılmaz", Email = "ahmet@example.com", CreatedDate = seedDate },
new Musteri { Id = 2, MusteriAdi = "Ayşe Demir", Email = "ayse@example.com", CreatedDate = seedDate });
modelBuilder.Entity<Urun>().HasData(
new Urun { Id = 1, UrunAdi = "Laptop", Fiyat = 5000, CreatedDate = seedDate },
new Urun { Id = 2, UrunAdi = "Telefon", Fiyat = 3000, CreatedDate = seedDate });
}
}Bağlantı dizesini koda gömmek yerine ortam değişkeninden okuduk. Değişkeni tanımladıktan sonra Visual Studio'yu yeniden başlatmanız gerekebilir. Bir ASP.NET Core projesinde ise bağlantı dizesi appsettings.json veya User Secrets içinde durur ve AddDbContext ile enjekte edilir; hiçbir zaman şifreyi kaynak koda ya da Git'e koymayın.
Adım 2: İlk migration'ı oluşturmak
Kodlara göre veritabanı yapısını üretmek için migration komutlarını kullanıyoruz. Package Manager Console'a migration adını vererek komutu girin. Migration adı bir C# sınıf adına dönüşür; bu yüzden boşluk, nokta ve tire içermemelidir. Benim tercihim değişikliği anlatan açıklayıcı bir ad kullanmak; ilk migration için klasik ad InitialCreate'tir.
Add-Migration InitialCreate.NET CLI: dotnet ef migrations add InitialCreate. Komut çalışınca projede Migrations klasörü oluşur ve içinde üç dosya bulunur: migration sınıfı, onun .Designer.cs dosyası ve MarketDbContextModelSnapshot.cs.
Snapshot dosyası, modelinizin o anki tam halinin bir kopyasıdır. EF Core bir sonraki migration'ı üretirken yeni modeli bu snapshot ile karşılaştırarak farkı hesaplar; bu yüzden yalnızca basit bir yönlendirme dosyası değildir ve elle düzenlenmemelidir.
Migration sınıfında iki metot vardır. Up, migration uygulanırken veritabanına yapılacak işlemleri; Down, migration geri alınırken yapılacak işlemleri içerir. Uygulamadan önce Up metodunu inceleyip bir hata görürseniz migration'ı silip baştan başlayabilirsiniz. Geri almadan önce de Down metodunu okumak iyi bir alışkanlıktır.
Up metodunu incelerseniz yazmadığınız bir tablonun üretildiğini görürsünüz:
migrationBuilder.CreateTable(
name: "SiparisUrun",
columns: table => new
{
SiparislerId = table.Column<int>(type: "int", nullable: false),
UrunlerId = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SiparisUrun", x => new { x.SiparislerId, x.UrunlerId });
table.ForeignKey(
name: "FK_SiparisUrun_Siparisler_SiparislerId",
column: x => x.SiparislerId,
principalTable: "Siparisler",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_SiparisUrun_Urunler_UrunlerId",
column: x => x.UrunlerId,
principalTable: "Urunler",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});Sipariş ve ürün arasında çoka çok ilişki kurmuştuk; bu ilişki bir ara tablo gerektirir ve EF Core bu tabloyu (SiparisUrun) sizin için üretip yönetir. Ara tabloya ek sütun koymanız gerekiyorsa (örneğin adet ya da indirim) o tabloyu kendiniz bir entity olarak tanımlamanız ve ilişkileri ona göre yapılandırmanız gerekir. Yalnızca bağlantı görevi gören bir ara tablo için ise ekstra bir şey yazmanıza gerek yoktur.
Adım 3: Veritabanını oluşturmak
Veritabanını oluşturmak için Update-Database komutunu girin. Veritabanı varsa uygulanmamış migration'ları çalıştırır, yoksa sıfırdan oluşturur.
Update-Database.NET CLI: dotnet ef database update. İşlem bittiğinde tabloların, ilişkilerin ve seed verisinin oluştuğunu DBeaver'dan görebilirsiniz. Ayrıca __EFMigrationsHistory adlı bir tablo oluşur; hangi migration'ların uygulandığı burada tutulur.
Hata alırsanız önce iki şeyi kontrol edin: bağlantı dizesi ve projenin derlenebilir olması. Proje derlenmiyorsa migration komutlarının hiçbiri çalışmaz.
Modelde değişiklik yapmak: ikinci migration
Şimdi hem ortak sınıfa yeni bir sütun hem de yeni bir tablo ekleyelim. Önce BaseEntity içine bir alan ekleyin:
public abstract class BaseEntity
{
public int Id { get; set; }
public DateTime CreatedDate { get; set; } = DateTime.UtcNow;
public bool IsDeleted { get; set; } = false; // yeni
}Ardından ilişkisiz, boş bir entity oluşturup DbContext'e ekleyin:
public class Calisan : BaseEntity
{
public string Adi { get; set; } = string.Empty;
}
// MarketDbContext içine:
public DbSet<Calisan> Calisanlar => Set<Calisan>();İkinci migration'ı oluşturup uygulayın:
Add-Migration AddIsDeletedAndCalisan
Update-DatabaseBaseEntity'den türeyen tüm tablolara IsDeleted sütunu eklenir, Calisanlar tablosu oluşur. Bu değişikliği veritabanında elle yapsaydınız her sütunu ayrı ayrı eklemeniz, ardından kodu güncellemeniz gerekirdi. Code First'te tek bir model değişikliği ve iki komut yeterli.
Değişikliği geri almak
Migration'ları geri alırken iki durum vardır.
Migration henüz veritabanına uygulanmadıysa (yani Update-Database çalıştırmadıysanız) son migration'ı Remove-Migration ile kaldırabilirsiniz.
Migration veritabanına uygulandıysa önce veritabanını bir önceki migration'a döndürürsünüz. Bunun için Update-Database komutuna hedef migration'ın adını verin; EF Core uygulanan migration'ların Down metotlarını çalıştırır.
Update-Database InitialCreateKonsolda AddIsDeletedAndCalisan migration'ının geri alındığını görürsünüz. Ancak migration sınıfı projede hâlâ durmaktadır; bırakırsanız ileride yapılacak güncellemelerde sorun çıkarır. Onu kaldırmak için:
Remove-MigrationMigration dosyasını doğrudan silmek yerine neden Remove-Migration kullanmalıyız? Çünkü komut, migration veritabanına uygulanmışsa onu silmeyi reddeder (uygulanmış bir migration'ı sessizce silmek veritabanı ile kod arasında tutarsızlık yaratır) ve snapshot dosyasını da güncel hale getirir. Snapshot güncellenmezse genellikle hemen hata görmezsiniz ama bir sonraki migration yanlış fark hesaplayabilir. Tüm migration'ları geri almak için Update-Database 0 komutu kullanılır.
Not: Bu işlemleri anlatmak için eklediğimiz IsDeleted alanını ve Calisan sınıfını, bir sonraki yazıdaki örneklere geçmeden önce kodunuzdan da çıkarmayı unutmayın.
Üretim ortamında migration uygulamak
Geliştirme makinenizde Update-Database pratik bir araçtır; fakat canlı veritabanına doğrudan Visual Studio'dan komut göndermek yerine migration'ları SQL script ya da bundle olarak üretip deployment sürecinizde çalıştırmanız önerilir:
dotnet ef migrations script --idempotent -o migrate.sql
dotnet ef migrations bundle -o efbundleScript'i uygulamadan önce incelemek, veri kaybı riski olan işlemleri (sütun silme, tür değiştirme) fark etmenizi sağlar. Canlı veritabanında her zaman önce yedek alın.
Code First'ün avantajları ve dezavantajları
Avantajlar
- Veritabanı yapısını koddan kontrol edebilir ve yönetebilirsiniz.
- Migration ve güncelleme işlemleri otomatiktir.
- Daha fazla esneklik ve kod üzerinde tam kontrol sağlar.
- Git sayesinde migration'ları adım adım izleyebilir, olası hataları geri alabilirsiniz.
- Değişiklikleri uygulamadan önce inceleyebilir, kolayca geri alabilirsiniz.
- Ekipte çalışırken herkes veritabanı şemasını kodla birlikte güncel tutar.
Dezavantajlar
- Mevcut bir veritabanıyla çalışırken zorluk çıkarır (bu durumda DB First veya Mixed daha uygundur).
- Veritabanına özgü gelişmiş özellikler (görünümler, saklı yordamlar, özel indeksler) için ek migration kodu yazmanız gerekebilir.
Sıradaki yazıda bu modelin üzerinde ilişkilerle çalışmayı, Mixed yaklaşımı ve doğru yaklaşımı seçmeyi ele alıyoruz.

