DTO nedir ve neden entity'yi doğrudan dönmemeliyiz?
DTO'ların ne olduğunu ve neden veritabanı nesnelerini doğrudan dönmemeniz gerektiğini işliyoruz: güvenlik, gizlilik, esneklik ve sözleşme kararlılığı açısından DTO kullanmanın somut faydaları.
Şimdiye kadar örneklerimizde ya basit metinler ya da isim listeleri döndük. Ama gerçek bir uygulamada döneceğin şey genelde bir veritabanı kaydıdır: bir kullanıcı, bir ürün, bir sipariş. İşte tam burada çoğu yeni geliştiricinin farkında olmadan yaptığı bir hata var: veritabanından gelen nesneyi olduğu gibi alıp dışarı dönmek. Bugün neden bunu yapmamamız gerektiğini ve yerine kullanacağımız DTO kavramını konuşuyoruz.
Önce Terimleri Netleştirelim
İki kelimeyi ayırmakla başlayalım. Entity, veritabanındaki bir tabloyu temsil eden sınıftır; içinde o kaydın tüm alanları bulunur. Bir User entity'sinde kullanıcının adı, e-postası, ama aynı zamanda şifresinin özeti, oluşturulma tarihi, iç kayıt numaraları gibi dışarının görmesine hiç gerek olmayan alanlar da olabilir.
DTO ise "Data Transfer Object", yani veri taşıma nesnesi demek. Adı üstünde: tek görevi, iki nokta arasında veri taşımak. DTO, dışarıya tam olarak neyi göstermek istiyorsan sadece onu içeren, sade bir taşıyıcı sınıftır. Entity'nin kırpılmış, cilalanmış ve dışarıya sunulmaya hazır halidir diyebilirsin.
Peki Entity'yi Doğrudan Dönmek Neden Kötü?
Kod çalışır, veri de gider; ilk bakışta bir sorun yokmuş gibi görünür. Ama bu alışkanlığın altında birkaç ciddi risk yatıyor.
Gizli Bilgi Sızdırma
En tehlikelisi bu. Entity'ni olduğu gibi dönersen, içindeki her alan cevaba dahil olur. Şifre özeti, iç notlar, silinme durumu gibi alanlar farkında olmadan dışarıya sızabilir. Bir gün User entity'sine yeni bir hassas alan eklersin ve o alan, senin haberin bile olmadan API cevaplarında belirmeye başlar. DTO kullandığında ise cevaba yalnızca DTO'da tanımladığın alanlar girer; gerisi ne olursa olsun dışarı çıkmaz.
Sözleşmenin Kırılganlaşması
API'nin dışarıya döndüğü veri, aslında onu kullananlarla arandaki bir sözleşmedir. Entity'ni doğrudan dönersen, veritabanı yapında yaptığın her değişiklik bu sözleşmeyi de değiştirir. Tabloya bir alan ekledin diye API cevabın değişir, bunu kullanan mobil uygulama beklenmedik şekilde kırılabilir. DTO, bu iki dünyayı birbirinden ayırır: içeride veritabanını istediğin gibi değiştirirsin, dışarıdaki sözleşme sen istemedikçe sabit kalır.
Esneklik Kaybı
Çoğu zaman dışarıya döneceğin şey, entity'nin birebir kopyası değildir. Bazen iki tablodan gelen bilgiyi tek bir cevapta birleştirmek, bazen bir alanı farklı biçimlendirmek, bazen de birkaç alanı hiç göstermemek istersin. Entity'ye mahkûm kaldığında bu esnekliğin olmaz. DTO ile ise cevabının şeklini tamamen sen belirlersin.
Nasıl Görünüyor?
Diyelim ki elimizde şöyle bir entity var. İçinde dışarıya hiç göstermek istemeyeceğimiz alanlar da bulunuyor:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string PasswordHash { get; set; }
public DateTime CreatedAt { get; set; }
public bool IsDeleted { get; set; }
}
Bu entity'yi doğrudan dönersek, PasswordHash ve IsDeleted gibi alanlar da cevaba dahil olur ki bunu kesinlikle istemeyiz. Bunun yerine, dışarıya sadece göstermek istediğimiz alanları içeren bir DTO tanımlıyoruz:
public class UserDto
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
Gördüğün gibi DTO çok daha sade. Sadece dışarının görmesinde sakınca olmayan üç alan var; şifre özeti ya da iç durum bilgisi burada yok. Artık controller'ımızda entity yerine bu DTO'yu dönüyoruz:
[HttpGet("{id}")]
public IActionResult GetById(int id)
{
var user = _users.FirstOrDefault(u => u.Id == id);
if (user is null)
{
return NotFound();
}
var dto = new UserDto
{
Id = user.Id,
Name = user.Name,
Email = user.Email
};
return Ok(dto);
}
Burada entity'den DTO'ya elle bir kopyalama yapıyoruz. Kullanıcıyı veritabanından çekiyoruz ama dışarıya yalnızca DTO'yu dönüyoruz. Böylece hassas alanlar hiçbir zaman API cevabına girmiyor. Bu elle kopyalama küçük projelerde gayet iyidir; ilerleyen bir yazıda bu dönüşümü otomatikleştiren AutoMapper'ı da göreceğiz, ama önce işin elle nasıl yürüdüğünü görmek çok değerli.
Gelen Veri İçin de DTO Kullanılır
DTO sadece dışarı dönerken değil, veri alırken de işine yarar. Yeni bir kullanıcı oluştururken istemciden Id ya da CreatedAt gibi alanları almak istemezsin; bunları sen belirlersin. Bu yüzden sadece istemciden beklediğin alanları içeren ayrı bir giriş DTO'su tanımlarsın:
public class CreateUserDto
{
public string Name { get; set; }
public string Email { get; set; }
public string Password { get; set; }
}
Bu DTO, "yeni kullanıcı oluştururken senden şu üç bilgiyi bekliyorum" der. Id'yi sistem üretecek, oluşturulma tarihini sunucu koyacak, şifreyi de sen özetleyip saklayacaksın. İstemcinin bu iç işlere karışmasına gerek yok. Giriş için ayrı, çıkış için ayrı DTO kullanmak başta fazladan sınıf gibi görünse de, kodu hem güvenli hem de anlaşılır kılar.
Küçük Bir Deneme
Elindeki User entity'sine PasswordHash gibi hassas bir alan ekle ve önce hiç DTO kullanmadan entity'yi doğrudan dön. Cevaba baktığında o hassas alanın da göründüğünü fark et. Sonra bir UserDto tanımla, sadece güvenli alanları içine koy ve controller'ı bu DTO'yu dönecek şekilde değiştir. İki cevabı yan yana koyduğunda DTO'nun ne işe yaradığını tek bakışta göreceksin. Bu küçük deney, "neden zahmet edip ayrı sınıf yazıyoruz?" sorusunu kalıcı olarak cevaplar.
Bir sonraki yazıda model validation konusuna geçeceğiz: gelen DTO'daki verinin geçerli olup olmadığını nasıl kontrol edeceğimizi, Data Annotations ile FluentValidation yaklaşımlarını işleyeceğiz. Bugün tanımladığın giriş DTO'ları, o doğrulama kurallarını üzerine yazacağın tam da doğru yer olacak.