Entity Framework Core Nedir? ORM Fikri ve EF Core'un Mimarisi
ORM'in neden var olduğunu düz ADO.NET kodundan yola çıkarak görüyoruz: EF Core'un DbContext, model, provider ve change tracker parçaları, SQLite ile SQL Server'ın aynı LINQ'ten ürettiği farklı SQL, EF6 ile EF Core farkı, Dapper karşılaştırması ve serinin kurgusu.
Sürüm notu: Bu yazıdaki kodlar EF Core 6 ile yazılmıştır ve EF Core 10'da da değişmeden çalışır.
Önce Tahmin Et
Bir kitapçının veritabanında Books adında bir tablo var. Fiyatı 30'un altındaki kitapları isme göre sıralı getirmek istiyorsun. .NET'te bunu hiçbir kütüphane kullanmadan, düz ADO.NET ile şöyle yazarsın:
using Microsoft.Data.Sqlite;
using SqliteConnection connection = new("Data Source=bookstore.db");
connection.Open();
using SqliteCommand command = connection.CreateCommand();
command.CommandText = "SELECT Id, Title, Price FROM Books WHERE Price < $maxPrice ORDER BY Title";
command.Parameters.AddWithValue("$maxPrice", 30m);
List<Book> books = new();
using SqliteDataReader reader = command.ExecuteReader();
while (reader.Read())
{
books.Add(new Book
{
Id = reader.GetInt32(0),
Title = reader.GetString(1),
Price = reader.GetDecimal(2)
});
}
İki soru. Birincisi: yarın tabloya Isbn kolonu eklendiğinde bu kodda kaç yere dokunman gerekir? İkincisi: aynı sorguyu birazdan Entity Framework Core ile tek bir LINQ ifadesi olarak yazacağız. O zaman veritabanına giden SQL'i kim yazacak ve neye benzeyecek?
Elle Eşlemenin Bedeli
Yukarıdaki kod çalışır ve hızlıdır. Ama içinde üç ayrı iş iç içe geçmiş durumda. SQL'i string olarak sen yazıyorsun. Parametreyi sen ekliyorsun. Her satırı da kolon sırasına bakarak elle bir Book object'ine çeviriyorsun. GetString(1) derken 1'in Title olduğunu bilen tek şey senin hafızan. SELECT'teki kolon sırası değişirse derleyici sesini çıkarmaz, hatayı çalışma anında görürsün.
Isbn sorusunun cevabı da burada saklı: SELECT listesi, mapping bloğu, INSERT komutu, UPDATE komutu ve bu tabloya dokunan her sorgu.
Asıl mesele ise iki dünyanın farklı düşünmesi. Veritabanı tablolar, satırlar ve foreign key'lerle konuşur. C# tarafı ise class'lar, object'ler ve referanslarla. Veritabanında bir kitabın yazarı AuthorId = 7 gibi bir sayıdır, C#'ta ise book.Author.Name diye üzerinden yürüyebildiğin bir object. Bir object'in hangi property'sinin değiştiğini veritabanı bilmez. Hangi satırın hangi object'e karşılık geldiğini de C# bilmez. Literatürde buna object-relational impedance mismatch deniyor: aynı veriyi iki farklı şekle sokmanın sürtünmesi.
ORM Ne Yapar?
ORM (Object-Relational Mapper) bu sürtünmeyi senin yerine taşıyan katmandır. İki dili de konuşan bir tercüman gibi düşün: sen C# ile konuşursun, o veritabanına SQL ile aktarır, gelen cevabı da tekrar C# object'lerine çevirir. İyi bir ORM dört işi üstlenir:
Birincisi mapping: hangi class hangi tabloya, hangi property hangi kolona denk geliyor. İkincisi sorgu çevirisi: LINQ ile yazdığın ifadeyi o veritabanının SQL lehçesine çevirmek. Üçüncüsü change tracking: okuduğun object'lerin sonradan değişip değişmediğini takip etmek. Dördüncüsü kaydetme: bu değişikliklerden doğru INSERT, UPDATE ve DELETE komutlarını üretmek. EF Core bunlara bir beşincisini de ekliyor: modelden veritabanı şemasını oluşturmak ve migration'larla güncel tutmak.
Aynı kitap sorgusunu EF Core ile yazalım. Önce entity class'ı ve DbContext:
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
public class Book
{
public int Id { get; set; }
public string Title { get; set; } = "";
public decimal Price { get; set; }
}
public class BookstoreContext : DbContext
{
public DbSet<Book> Books => Set<Book>();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlite("Data Source=bookstore.db")
.LogTo(Console.WriteLine, new[] { DbLoggerCategory.Database.Command.Name }, LogLevel.Information);
}
}
Sonra sorgunun kendisi:
using BookstoreContext db = new();
List<Book> cheapBooks = db.Books
.Where(b => b.Price < 30m)
.OrderBy(b => b.Title)
.ToList();
LogTo satırı sayesinde EF Core'un veritabanına gönderdiği her komut console'a düşüyor. Çıktının ilgili kısmı şu:
info: 11/4/2026 00:46:45.735 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT "b"."Id", "b"."Price", "b"."Title"
FROM "Books" AS "b"
WHERE ef_compare("b"."Price", '30.0') < 0
ORDER BY "b"."Title"
İkinci sorunun cevabı bu. SQL'i EF Core yazdı ve SELECT listesine entity'nin bütün property'lerini kendisi koydu. Isbn eklediğinde değiştireceğin tek yer Book class'ı olacak; sorgu, mapping ve INSERT kendiliğinden güncellenir. ef_compare denen garip fonksiyona birazdan döneceğiz.
EF Core'un Parçaları
Bu kısa örnekte EF Core'un bütün ana parçaları göründü. DbContext, veritabanıyla yaptığın bir çalışma oturumunu temsil eder. Bir alışveriş sepeti gibi düşünebilirsin: içine okuduğun, eklediğin, değiştirdiğin object'leri koyarsın, SaveChanges dediğinde sepetin tamamı kasaya gider. DbSet<Book> ise bu oturumda Book entity'lerine açılan kapıdır; üzerine yazdığın LINQ ifadeleri SQL'e çevrilir.
Model, EF Core'un class'larına bakarak çıkardığı haritadır: hangi entity hangi tabloya gider, key hangi property, hangi kolon NULL olabilir. Bu haritayı convention'lar ve senin vereceğin ayarlar birlikte oluşturur; üçüncü ve dördüncü yazının konusu bu. Provider, modeli ve LINQ'i belirli bir veritabanının diline çeviren paket. Change tracker ise sepetteki her object'in durumunu tutan defter. Onu iş başında görelim:
Book first = cheapBooks[0];
Console.WriteLine(db.Entry(first).State);
first.Price = 27.50m;
Console.WriteLine(db.Entry(first).State);
int affected = db.SaveChanges();
Console.WriteLine($"{affected} row(s) updated");
Console.WriteLine(db.Entry(first).State);
Unchanged
Modified
info: 11/4/2026 00:46:45.738 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (0ms) [Parameters=[@p1='?' (DbType = Int32), @p0='?' (DbType = Decimal)], CommandType='Text', CommandTimeout='30']
UPDATE "Books" SET "Price" = @p0
WHERE "Id" = @p1;
SELECT changes();
1 row(s) updated
Unchanged
Hiçbir yerde "bu object değişti" demedik. Change tracker, sorgudan gelen her entity'nin o anki değerlerinin bir kopyasını sakladı. SaveChanges sırasında kopyayla güncel hâli karşılaştırdı, yalnızca Price'ın değiştiğini gördü ve sadece o kolonu güncelleyen bir UPDATE üretti. Kayıt bitince entity yeniden Unchanged oldu.
Bu çıktı EF Core 6'dan. Aynı kodu EF Core 10 ile çalıştırdığında UPDATE'in sonu SELECT changes(); yerine RETURNING 1; olarak gelir. Kodun bir harfi değişmedi; değişen, provider'ın SQLite için ürettiği SQL. Seri boyunca bu tür farkları yerinde göstereceğiz.
Provider: Aynı LINQ, Farklı SQL
Şimdi ef_compare'e dönelim. SQLite'ın gerçek bir decimal tipi yok; EF Core decimal değerleri SQLite'ta TEXT olarak saklıyor. Metin olarak saklanan iki sayıyı doğru karşılaştırmak için de provider kendi kaydettiği ef_compare fonksiyonunu kullanıyor. Aynı modeli SQL Server provider'ına bağlarsak ne olur? Bunu görmek için bir SQL Server'a bile ihtiyacın yok, çünkü ToQueryString veritabanına bağlanmadan SQL'i üretir:
public class SqlServerBookstoreContext : DbContext
{
public DbSet<Book> Books => Set<Book>();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Server=localhost;Database=Bookstore;Trusted_Connection=True;");
}
}
using SqlServerBookstoreContext db = new();
string sql = db.Books
.Where(b => b.Price < 30m)
.OrderBy(b => b.Title)
.ToQueryString();
Console.WriteLine(sql);
SELECT [b].[Id], [b].[Price], [b].[Title]
FROM [Books] AS [b]
WHERE [b].[Price] < 30.0
ORDER BY [b].[Title]
LINQ aynı, SQL farklı. SQL Server'da köşeli parantezler var, decimal doğal bir tip olduğu için düz bir < karşılaştırması yeterli. Bu örnek için Microsoft.EntityFrameworkCore.SqlServer paketini ekledik. PostgreSQL için Npgsql'in Npgsql.EntityFrameworkCore.PostgreSQL paketi, Cosmos DB için Microsoft'un kendi provider'ı var. "Provider değiştirince her şey aynen çalışır" beklentisi ise yanıltıcı. Her veritabanının kendine özgü tipleri ve sınırları var; SQLite'ın decimal'ı bunun ilk örneği. Seri boyunca örnekleri kurulum gerektirmeyen SQLite ile çalıştıracağız, yalnızca SQL Server'a özgü konularda Docker'la SQL Server kullanacağız ve bunu yerinde söyleyeceğiz.
EF6 ile EF Core Aynı Şey Değil
İsimler kafa karıştırıyor, o yüzden net konuşalım. Entity Framework ilk kez 2008'de .NET Framework 3.5 SP1 ile geldi. Bugün "EF6" dediğimiz sürüm o çizginin devamı: NuGet paketi EntityFramework, namespace'i System.Data.Entity, EDMX tasarımcısı ve DbModelBuilder gibi parçalarıyla .NET Framework dünyasından gelen klasik ORM. Hâlâ yama alıyor (NuGet'teki son sürümü 6.5.2) ve 6.4'ten beri .NET Core üzerinde de çalışabiliyor, ama bakım modunda; Microsoft'un yeni özellik geliştirmesi EF Core tarafında.
EF Core ise sıfırdan yeniden yazılmış ayrı bir ürün. İlk sürümü 1.0 Haziran 2016'da çıktı. Paketleri Microsoft.EntityFrameworkCore ile başlıyor, platformlar arası çalışıyor, provider modeli üzerine kurulu ve EDMX gibi bir tasarımcı dosyası yok. Bu serinin konusu EF Core. İnternette bulduğun bir örnekte using System.Data.Entity; görüyorsan o örnek EF6 içindir ve EF Core projesinde derlenmez.
En sinsi tuzak sürüm numarasında: EF Core 6, EF6 değildir. Biri .NET 6 ile çıkan EF Core sürümü, diğeri klasik Entity Framework'ün altıncı ana sürümü.
Dapper ile Kısa Karşılaştırma
ORM yelpazesinin öbür ucunda Dapper var. Dapper bir micro-ORM: SQL'i yine sen yazarsın, Dapper sadece parametreleri bağlar ve sonucu object'lere çevirir. İlk örnekteki ADO.NET kodunun Dapper'lı hâli şöyle:
using Dapper;
using Microsoft.Data.Sqlite;
using SqliteConnection connection = new("Data Source=bookstore.db");
List<Book> books = connection
.Query<Book>("SELECT Id, Title, Price FROM Books WHERE Price < @maxPrice ORDER BY Title",
new { maxPrice = 30m })
.ToList();
Mapping derdi gitti ama SQL hâlâ senin. Dapper'da LINQ çevirisi, change tracking, ilişki yönetimi ve migration yok. Bu bir eksiklik değil, bilinçli bir tercih. SQL üzerinde tam kontrol isteyen raporlama sorgularında, yoğun okuma yapan endpoint'lerde Dapper çok rahattır. EF Core ise yazma tarafında, ilişkili veriyi bir bütün olarak yönetirken ve şemayı kodla birlikte evrimleştirirken öne çıkar. Dapper'ı ayrıntılı tanımak istersen Dapper serisine bakabilirsin; hangisinin ne zaman seçileceğini serinin mimari yazısında tekrar tartışacağız.
Bu Seri Nasıl İlerleyecek
Seri iki bloktan oluşuyor. İlk 23 yazı kemik yapı: DbContext, model, ilişkiler, migration'lar, sorgulama, change tracking, kaydetme, transaction ve concurrency, raw SQL, ASP.NET Core entegrasyonu, performans, test ve mimari. Bu bloktaki bütün kodlar EF Core 6 API'siyle yazılıyor ve her örnek hem EF Core 6 (net6.0) hem EF Core 10 (net10.0) üzerinde derlenip çalıştırılarak doğrulanıyor. Örneklerin tamamı aynı alan üzerinde: Author, Book, Order, OrderItem ve Tag entity'lerinden oluşan bir kitapçı.
Neden EF Core 6? Desteği 12 Kasım 2024'te bitti, yeni bir projeye onu seçmeni önermiyoruz. Ama EF Core 6'nın API yüzeyi, bugün sahada çalışan bütün modern sürümlerde aynen var. Burada öğrendiğin her şey hem eski bir projede hem de EF Core 10 ile açtığın yeni projede işine yarar. Yeni bir projeye başlıyorsan EF Core 10 kullan; LTS sürüm ve desteği Kasım 2028'e kadar sürüyor.
İkinci blok sürümlerle evrim. 24. yazı EF Core 3.1 ve 5'ten 6'ya geçişi, 25–28 arası da sırasıyla EF Core 7, 8, 9 ve 10'u anlatacak: "EF Core 6'da böyle yapıyorduk, yeni sürümle şöyle yapıyoruz". EF Core 8 ve 9'un desteği 10 Kasım 2026'da birlikte bitiyor. Seri, Kasım 2026'da çıkması planlanan EF Core 11 ile devam edecek.
Sık Düşülen Tuzaklar
İlk tuzak, ORM'i SQL bilmemenin bahanesi yapmak. EF Core SQL'i senin yerine yazar ama sorumluluğu senden almaz. Yavaş bir sayfanın sebebi neredeyse her zaman üretilen SQL'dedir. LogTo ve ToQueryString'i baştan alışkanlık hâline getir; bu seride neredeyse her örnekte üretilen SQL'e bakacağız.
İkincisi yanlış paket. NuGet'te EntityFramework aratıp klasik EF6'yı kurmak, ya da EF6 için yazılmış bir blog yazısındaki DbModelBuilder örneğini EF Core projesine taşımaya çalışmak sık görülür. EF Core paketlerinin hepsi Microsoft.EntityFrameworkCore önekiyle başlar.
Üçüncüsü DbContext'i bir veritabanı bağlantısı sanmak. DbContext bağlantıyı gerektiğinde kendisi açar ve kapatır. Kendisi kısa ömürlü bir çalışma oturumudur, uygulama boyunca yaşayan tek bir instance olarak tasarlanmamıştır. Change tracker'ın tuttuğu defter, uzun yaşayan bir context'te büyüdükçe büyür. Yaşam süresini ASP.NET Core entegrasyonu yazısında ayrıntılı göreceğiz.
Dördüncüsü paket sürümünü hedef framework'ten bağımsız sanmak. EF Core 6 net6.0 hedefler, EF Core 10 ise yalnızca net10.0 üzerinde çalışır. .NET 8 projesine EF Core 10 kuramazsın; projedeki bütün EF Core paketlerini de aynı sürümde tutmak en sorunsuz yoldur.
Küçük Bir Deneme
★☆☆ ADO.NET örneğini ve EF Core örneğini aynı veritabanı üzerinde çalıştır. Sonra Book class'ına public string Isbn { get; set; } = ""; ekle, veritabanını silip yeniden oluştur ve iki kodda da kaç yeri değiştirmen gerektiğini say.
★★☆ Sorgudaki koşulu b.Price >= 30m yap. SQLite context'inde LogTo çıktısına, SQL Server context'inde ToQueryString çıktısına bak. ef_compare'in sonucu hangi sayıyla karşılaştırılıyor?
★★★ Change tracking örneğinde first.Price'a mevcut değerinin aynısını ata ve SaveChanges'i çağır. Entity'nin durumunu ve dönen satır sayısını çalıştırmadan önce tahmin et, sonra kontrol et. Sonucu change tracker'ın sakladığı kopya üzerinden açıklayabiliyor musun?
Bu yazıda EF Core'u uzaktan, parçalarıyla tanıdık. Bir sonraki yazıda boş bir klasörden başlayıp ilk EF Core uygulamasını kuracağız: hangi paketler gerekiyor, dotnet-ef aracı ne işe yarıyor, DbContext ve DbSet nasıl tanımlanıyor, EnsureCreated veritabanını nasıl oluşturuyor ve üretilen SQL'i nasıl okuyoruz.