Repository pattern gerçekten gerekli mi? Servis katmanını konuşalım
Controller'dan doğrudan DbContext kullanmak yerine araya katman koymayı konuşuyoruz. Servis katmanı ne işe yarar, repository pattern gerçekten gerekli mi, ne zaman değer, ne zaman fazladan yük?
Önceki yazıda bir veritabanı kurduk ve DbContext'i tanıdık. En doğal refleks, şimdi controller'ın içine DbContext'i alıp doğrudan oradan veri okuyup yazmak olurdu. Bu çalışır, hem de gayet iyi çalışır. Ama küçük projeler büyüdükçe bu yaklaşımın nerede tıkandığını ve araya neden bir katman koyduğumuzu bugün konuşacağız. Bir de herkesin kafasını karıştıran o soruya dürüstçe cevap vereceğiz: repository pattern gerçekten gerekli mi?
Önce En Basit Hali: Controller İçinde DbContext
Doğrudan yaklaşımı görelim. Controller'a DbContext'i enjekte edip işi orada hallediyoruz:
[ApiController]
[Route("users")]
public class UsersController : ControllerBase
{
private readonly AppDbContext _context;
public UsersController(AppDbContext context)
{
_context = context;
}
[HttpGet]
public async Task GetAll()
{
var users = await _context.Users.ToListAsync();
return Ok(users);
}
}
Constructor'a AppDbContext koyduk ve .NET onu bize otomatik verdi; bu, üçüncü yazıdan aşina olduğun dependency injection'ın ta kendisi. Sonra _context.Users üzerinden tüm kullanıcıları çektik. Küçük bir proje için bunda hiçbir yanlış yok. Peki sorun ne zaman başlıyor?
Bu Yaklaşım Nerede Tıkanıyor?
Diyelim ki uygulaman büyüdü. "Kullanıcı oluştururken e-postanın benzersiz olması" gibi bir iş kuralın var. Bu kural sadece oluşturma endpoint'inde değil, başka birkaç yerde de geçerli. Controller'ın içine bu mantığı yazarsan, aynı kuralı her ihtiyaç duyduğun yerde tekrar yazmak zorunda kalırsın. Kod çoğalır, bir yerde düzeltirsin ama diğerini unutursun; işte hataların doğduğu yer tam burası.
İkinci sıkıntı, controller'ın asıl görevinden uzaklaşması. Bir controller'ın işi, HTTP dünyasıyla ilgilenmektir: isteği almak, doğru durum kodunu dönmek. İş mantığını da onun sırtına yüklersen, controller hem trafik polisi hem de karar mercii olur; şişer, okunması zorlaşır ve test etmesi güçleşir. İşte servis katmanı fikri buradan doğuyor.
Servis Katmanı: İş Mantığının Evi
Servis katmanı, controller ile veritabanı arasına koyduğun bir ara kattır. İş mantığı burada yaşar. Controller sadece "şunu yap" der, nasıl yapılacağıyla servis ilgilenir. Önce servisin ne yapacağını bir arayüzle (interface) tanımlıyoruz:
public interface IUserService
{
Task> GetAllAsync();
Task GetByIdAsync(int id);
}
Sonra bu arayüzü uygulayan asıl servisi yazıyoruz. Veritabanıyla konuşan DbContext artık controller'da değil, burada:
public class UserService : IUserService
{
private readonly AppDbContext _context;
public UserService(AppDbContext context)
{
_context = context;
}
public async Task> GetAllAsync()
{
return await _context.Users.ToListAsync();
}
public async Task GetByIdAsync(int id)
{
return await _context.Users.FindAsync(id);
}
}
Artık controller'ımız çok daha sade. İçinde DbContext yok; sadece servisi çağırıyor:
[ApiController]
[Route("users")]
public class UsersController : ControllerBase
{
private readonly IUserService _userService;
public UsersController(IUserService userService)
{
_userService = userService;
}
[HttpGet]
public async Task GetAll()
{
var users = await _userService.GetAllAsync();
return Ok(users);
}
}
Gördüğün gibi controller artık sadece HTTP tarafıyla ilgileniyor: isteği alıyor, servisi çağırıyor, cevabı dönüyor. İş mantığı nereye taşındıysa (servise), test etmek, değiştirmek ve tekrar kullanmak da orada kolaylaşıyor. Bu ayrımı yapmak için servisi Program.cs'te de kaydetmen gerekir; bir sonraki yazıda dependency injection'ın lifetime'larını konuşurken buna değineceğiz.
Peki Repository Pattern Nereye Oturuyor?
Şimdi seni gerçekten kafası karışık bırakan konuya geldik. Repository pattern, veritabanı erişimini bir soyutlamanın arkasına saklama fikridir. Yani DbContext'i doğrudan kullanmak yerine, araya IUserRepository gibi bir katman daha koyarsın; veri okuma-yazma işleri oradan geçer. Amaç, uygulamanın geri kalanını veritabanı teknolojisinden tamamen habersiz kılmaktır.
Kulağa temiz geliyor ve yıllarca neredeyse zorunlu bir kalıp gibi öğretildi. Ama bir gerçek var: EF Core'un DbContext'i zaten bir repository gibi davranıyor. DbSet, aslında bir repository; SaveChanges ise bir "unit of work". Yani repository pattern'in sana vaat ettiği soyutlamanın büyük kısmını EF Core sana hazır veriyor. Bunun üzerine bir katman daha eklemek, çoğu projede aynı işi iki kez yapmak anlamına gelebiliyor.
O Zaman Ne Zaman Gerekli?
Repository pattern'i kategorik olarak kötülemek de doğru değil; onu haklı çıkaran senaryolar var. Şu durumlarda gerçekten değer katabilir:
Veritabanı teknolojini ileride değiştirme ihtimalin ciddiyse, repository bu geçişi kolaylaştırır çünkü uygulamanın geri kalanı hangi veritabanını kullandığını bilmez. Çok karmaşık, tekrar eden sorguların varsa, bunları tek bir yerde toplamak repository ile daha temiz olur. Ekibin bu kalıba alışkınsa ve tutarlılık önemliyse, ortak dili korumak da başlı başına bir gerekçedir.
Ama küçük ve orta ölçekli, EF Core kullanan tipik bir projede repository pattern çoğu zaman fazladan bir katman, fazladan bir tören olur. Servis katmanı zaten iş mantığını topluyor; DbContext zaten soyutlamayı sağlıyor. Bu ikisi çoğu ihtiyacı karşılar.
Benim Pratik Tavsiyem
Öğrenme aşamasındaysan ya da küçük-orta bir proje yazıyorsan şu ikiliyle başla: controller ve servis katmanı. Servis, DbContext'i doğrudan kullansın. Bu, hem anlaşılır hem de fazlasıyla yeterli bir yapıdır. Repository pattern'i baştan eklemek için kendini zorlama; ihtiyaç doğduğunda, yani yukarıdaki senaryolardan biri gerçekten karşına çıktığında eklersin. "Herkes yapıyor" diye bir kalıbı körlemesine uygulamak, mühendislik değil taklittir. Sen ihtiyacına göre karar ver.
Şunu da unutma: bunlar dini inanç değil, mühendislik tercihleri. Farklı ekipler farklı gerekçelerle farklı yollar seçer ve çoğu zaman birden fazla doğru cevap vardır. Önemli olan, neden o yolu seçtiğini açıklayabilmen.
Küçük Bir Deneme
Elindeki UsersController'ı iki aşamada dönüştür. Önce tüm veritabanı işlemlerini controller'ın içinde, doğrudan DbContext ile yaz. Sonra bir IUserService ve UserService oluşturup bu mantığı servise taşı, controller'ı sadeleştir. İki hali karşılaştır: controller'ın ne kadar sadeleştiğini, iş mantığının nasıl tek bir yere toplandığını gör. Bu dönüşümü kendi elinle yapmak, katman ayrımının değerini herhangi bir paragraftan çok daha iyi anlatır.
Bir sonraki yazıda dependency injection'ı derinlemesine ele alacağız: bugün servisi kaydederken şöyle bir değindiğimiz konuyu açacak, servislerin lifetime'larını (singleton, scoped, transient) ve bunların neden önemli olduğunu konuşacağız. Bugün oluşturduğun servis, o kavramları anlatmak için tam da ihtiyacım olan örnek.