Merhaba,
.NET Core / EF Core kullanan hemen herkesin bir noktada karşılaştığı klasik bir soru vardır: DTO’ya map ederken hangi yaklaşım daha performanslıdır?
return _context.Users
.Select(u => new UserDto { ... })
.ToList();
yoksa:
var list = _context.Users.ToList();
return list.Adapt<List<UserDto>>();
Bu yazıda, bu iki yaklaşımı gerçek performans farkları, EF Core’un çalışma şekli ve en iyi pratikler üzerinden net bir şekilde ele alacağım.
Senaryo
Bir API endpoint’i düşünelim. Veritabanından User entity’lerini çekiyor ve bunları client’a DTO olarak döndürmek istiyoruz.
Ancak burada kritik bir ayrım var:
-
DTO’yu veritabanı sorgusu sırasında mı oluşturacağız?
-
Yoksa önce entity’leri belleğe alıp, ardından mı map edeceğiz?
Bu fark küçük projelerde çoğu zaman hissedilmez. Ancak ölçek büyüdükçe, bu tercih ciddi performans problemlerine yol açabilir.
Yaklaşım 1: Select ile DTO Projection (Önerilen)
return _context.Users
.Where(u => u.IsActive)
.Select(u => new UserDto
{
Id = u.Id,
Name = u.Name,
Email = u.Email
}).ToList();
Bu yaklaşımda ne oluyor?
EF Core, bu LINQ ifadesini doğrudan SQL’e çevirir ve yalnızca gerekli kolonları sorgular.
Oluşan SQL kabaca şu şekildedir:
SELECT Id, Name, Email FROM Users WHERE IsActive = 1
Yani Bu yaklaşım sayesinde veritabanından yalnızca ihtiyaç duyulan alanlar çekilir. Böylece taşınan veri miktarı azalır, bellek kullanımı düşer ve CPU daha az yorulur. Sonuç olarak uygulama, client’a çok daha hızlı bir response dönebilir.
Yaklaşım 2: ToList() + Adapt() (Daha Maliyetli)
var list = _context.Users.Where(u => u.IsActive).ToList();
return list.Adapt<List<UserDto>>();
Mapster veya AutoMapper kullandığımızı varsayalım
Bu yaklaşımda EF Core, Users tablosundaki tüm kolonları veritabanından çekerek entity’leri belleğe alır. DTO oluşturma işlemi ise sorgu tamamlandıktan sonra, tamamen uygulama tarafında gerçekleşir.
Bu da gereksiz veri taşınmasına, daha fazla bellek kullanımına ve mapping işlemi sırasında ek CPU maliyetine neden olur. Veri seti büyüdükçe bu maliyetler katlanarak artar ve response süreleri belirgin şekilde uzamaya başlar.
SELECT * FROM Users WHERE IsActive = 1
Sonuç olarak, ihtiyaç duyulmayan kolonlar da veritabanından çekilmiş olur. Bu durum gereksiz veri transferine yol açar, bellek kullanımını artırır ve mapping sırasında ek CPU maliyeti oluşturur. Özellikle büyük veri setlerinde bu yaklaşım, ciddi performans problemlerine ve belirgin yavaşlamalara neden olabilir.
Performans Karşılaştırması
| Kriter | Select → DTO | ToList() + Adapt() | Açıklama |
|---|---|---|---|
| DB’den çekilen kolonlar | Sadece gerekenler | Tüm kolonlar | Select ile projection yapıldığında EF Core yalnızca ihtiyaç duyulan alanları SQL’e yansıtır. ToList() ise SELECT * benzeri bir sorgu üretir. |
| Entity oluşturma | Yok | Var | Projection’da entity nesneleri hiç oluşturulmaz. ToList() sonrasında ise her satır için entity instance’ı üretilir. |
| Change Tracking | Yok | Var | EF Core, entity’leri track eder. DTO projection’da bu mekanizma devreye girmez, bu da ek maliyeti ortadan kaldırır. |
| Mapping maliyeti | Query seviyesinde | Memory seviyesinde | Select mapping’i SQL tarafında çözer. Adapt() ise bellekte ek bir dönüşüm yapar. |
| Büyük dataset performansı | Yüksek | Düşük | Veri miktarı arttıkça bellek, CPU ve mapping maliyetleri hızla yükselir. |
Eğer veri miktarı yüksekse ve hem performansı korumak hem de kod okunabilirliğini artırmak amacıyla AutoMapper veya Mapster kullanmak isteniyorsa, doğru yaklaşım query seviyesinde projection yapmaktır.
Mapster – ProjectToType
return _context.Users.
Where(u => u.IsActive).
ProjectToType<UserDto>().ToList();
AutoMapper – ProjectTo
return _context.Users.
Where(u => u.IsActive).
ProjectTo<UserDto>(_mapper.ConfigurationProvider).ToList();
Bu yöntemlerde mapping işlemi SQL seviyesine taşınır. EF Core, sorguyu oluştururken yalnızca DTO’da tanımlı alanları seçer ve bu sayede Select ile yapılan manuel projection ile aynı performans karakteristiği elde edilir. Aynı zamanda kod daha temiz, okunabilir ve uzun vadede daha sürdürülebilir hâle gelir.
Ne Zaman ToList() + Adapt() Kabul Edilebilir?
ToList() sonrası mapping yaklaşımı her zaman yanlış değildir; ancak kapsamı iyi tanımlanmış senaryolarda tercih edilmelidir. Örneğin küçük veri setlerinde (örneğin 100 kayıttan az), admin panellerinde, domain logic çalıştıktan sonra mapping yapılmasının zorunlu olduğu durumlarda kabul edilebilir bir çözümdür.
Buna karşılık, public API’ler, yüksek trafik alan endpoint’ler ve büyük tablolar söz konusu olduğunda bu yaklaşım ciddi performans sorunlarına yol açabileceği için kesinlikle önerilmez.
Sonuç
EF Core kullanırken DTO dönüşümünü mümkün olduğunca veritabanı tarafında, yani Select veya query seviyesinde projection ile yapmak performans açısından kritik öneme sahiptir. Bu yaklaşım, gereksiz veri transferini ve bellek kullanımını önlerken daha hızlı ve ölçeklenebilir API’ler geliştirmenizi sağlar.
Bununla birlikte, her zaman mapper ya da her zaman projection gibi katı kurallar yoktur. Doğru tercih; veri miktarı, kullanım senaryosu, trafik yoğunluğu ve kodun sürdürülebilirliği gibi faktörler göz önünde bulundurularak yapılmalıdır. Senaryoya göre bilinçli karar vermek, hem performans hem de uzun vadeli bakım açısından en doğru yaklaşımdır.
Diğer Bloglarımda Görüşmek Üzere 👋