Kurumsal projelerde çalışırken en sık karşılaştığım sorunlardan biri, iş kurallarının controller'lara, servis sınıflarına ve hatta bazen doğrudan veritabanı katmanına dağılmış olması. Zamanla bu tür projeler "değiştirmesi korkutucu" hale geliyor: küçük bir değişiklik yapmak istediğinizde nereye dokunacağınızı kestiremiyorsunuz. Bu yazıda, ASP.NET Core projelerinde Clean Architecture prensiplerini nasıl uyguladığımı, katmanları nasıl ayırdığımı ve bunun günlük geliştirme sürecine pratikte ne kattığını anlatmak istiyorum.

Neden Clean Architecture?

Clean Architecture'ı bir trend olduğu için değil, somut bir sorunu çözdüğü için kullanıyorum: bağımlılık yönetimi. Klasik bir katmanlı mimaride (Presentation → Business → Data Access) genellikle Business katmanı, Data Access katmanına doğrudan bağımlıdır. Bu da şu anlama gelir: veritabanı teknolojinizi değiştirmek, ORM'i değiştirmek ya da sadece unit test yazmak istediğinizde iş mantığınız Entity Framework'e ya da başka bir altyapı detayına kenetlenmiş olur.

Clean Architecture'da bağımlılık kuralı tersine döner: dış katmanlar iç katmanlara bağımlı olabilir, ama iç katmanlar (Domain, Application) dış katmanlar hakkında hiçbir şey bilmez. Bu basit kural, projenin büyüklüğü arttıkça giderek daha fazla değer üretiyor.

Katmanlar Pratikte Nasıl Ayrılıyor?

Bir ASP.NET Core çözümünde tipik olarak dört katman kullanıyorum:

  • Domain: Entity'ler, value object'ler ve saf iş kuralları. Hiçbir NuGet paketine, veritabanına ya da web framework'üne bağımlı değil.
  • Application: Use case'ler, DTO'lar, interface tanımları (repository, servis arayüzleri). Domain'e bağımlı, ama Infrastructure'a değil.
  • Infrastructure: Entity Framework Core, dış servis entegrasyonları, dosya sistemi, e-posta gönderimi gibi somut implementasyonlar.
  • Presentation (API): Controller'lar, middleware'ler, dependency injection kaydı. Sadece burada, uygulamanın en dış katmanında, Infrastructure ve Application birbirine bağlanıyor.

Çözümde Klasör/Proje Yapısı

Somut olması için tipik bir çözümün proje yapısını paylaşayım:

src/
  Domain/            (bagimsiz sinif kutuphanesi)
  Application/       (Domain'e referans verir)
  Infrastructure/     (Application'a referans verir, EF Core burada)
  WebApi/             (Application + Infrastructure'a referans verir)
tests/
  Domain.Tests/
  Application.Tests/

Referans yönü tek yönlü: WebApi → Infrastructure → Application → Domain. Infrastructure projesinin Domain'e referansı var ama tam tersi asla olmuyor — Domain katmanı, kendisini kimin kullandığından tamamen habersiz. Bu, projeye yeni katılan bir geliştiricinin bile "bu kod nereye ait?" sorusuna klasör yapısına bakarak cevap bulabilmesini sağlıyor.

Uçtan Uca Bir Akış: Sipariş Oluşturma

Bir API isteği geldiğinde akış şöyle işliyor: WebApi katmanındaki controller, isteği alıp doğrudan bir use case'i (Application katmanında) çağırıyor. Use case, ihtiyaç duyduğu IOrderRepository gibi arayüzleri constructor injection ile alıyor; hangi somut implementasyonun enjekte edileceğine ise sadece WebApi katmanındaki Program.cs içinde, dependency injection kaydı sırasında karar veriliyor. Yani iş kuralı (siparişin geçerli olup olmadığı, stok kontrolü gibi) tamamen Application/Domain katmanlarında, veritabanına dokunmadan test edilebilir durumda kalıyor; controller ise ince bir "orkestratör" görevi görüyor, kendisi hiçbir iş kuralı barındırmıyor.

Bağımlılık Tersine Çevirme (Dependency Inversion) Örneği

Application katmanında bir arayüz tanımlıyorum, somut implementasyonu Infrastructure'da yazıyorum:

// Application katmani
public interface IOrderRepository
{
    Task<Order?> GetByIdAsync(Guid id);
    Task AddAsync(Order order);
}

// Infrastructure katmani
public class OrderRepository : IOrderRepository
{
    private readonly AppDbContext _context;
    public OrderRepository(AppDbContext context) => _context = context;

    public Task<Order?> GetByIdAsync(Guid id) =>
        _context.Orders.FirstOrDefaultAsync(o => o.Id == id);

    public Task AddAsync(Order order)
    {
        _context.Orders.Add(order);
        return _context.SaveChangesAsync();
    }
}

Application katmanındaki use case, IOrderRepository'yi kullanır ama OrderRepository'nin Entity Framework kullandığından habersizdir. Bu ayrım sayesinde Domain ve Application katmanlarını herhangi bir veritabanı ya da web framework'ü olmadan, saf C# olarak test edebiliyorum. Bu da unit testlerin hem hızını hem de güvenilirliğini önemli ölçüde artırıyor.

Test Stratejisi Nasıl Şekilleniyor?

Clean Architecture'ın en somut faydalarından biri, test piramidini doğal olarak desteklemesi. Domain katmanındaki entity'ler ve iş kuralları için hızlı, bağımlılıksız unit testler yazıyorum. Application katmanındaki use case'leri, repository arayüzlerini sahte (mock/fake) implementasyonlarla test ediyorum. Infrastructure katmanı için ise gerçek bir veritabanına karşı çalışan, daha az sayıda entegrasyon testi yeterli oluyor. Sonuç olarak testlerin büyük çoğunluğu milisaniyeler içinde çalışıyor, CI/CD sürecini yavaşlatmıyor. Örneğin bir use case'i test ederken repository'nin gerçek implementasyonuna hiç ihtiyacım olmuyor:

[Fact]
public async Task CreateOrder_Should_Throw_When_Stock_Is_Empty()
{
    var repo = new Mock<IOrderRepository>();
    var stock = new Mock<IStockService>();
    stock.Setup(s => s.HasStock(It.IsAny<Guid>())).Returns(false);

    var handler = new CreateOrderHandler(repo.Object, stock.Object);

    await Assert.ThrowsAsync<OutOfStockException>(
        () => handler.Handle(new CreateOrderCommand(...)));
}

Bu testi çalıştırmak için ne bir SQL Server'a, ne bir Docker container'a ihtiyacım var — sadece saf C# ve birkaç sahte nesne yeterli. Bu da geliştirme sırasında hızlı bir geri bildirim döngüsü kurmamı sağlıyor.

Ne Zaman Fazla Mühendislik Olur?

Her mimari kararda olduğu gibi burada da "her zaman doğru" bir cevap yok. Küçük bir prototip, kısa ömürlü bir MVP ya da tek kişilik bir yan proje geliştiriyorsanız, dört katmanlı bir Clean Architecture kurulumu gereksiz karmaşıklık ekleyebilir. Bu durumlarda daha basit bir katmanlı mimari, hatta tek proje içinde klasör bazlı bir ayrım bile yeterli olabilir. Ben bu kararı genellikle şu soruya göre veriyorum: bu proje aylarca/yıllarca yaşayacak mı, birden fazla geliştirici dokunacak mı? Cevap evetse, Clean Architecture'ın başlangıçta biraz daha fazla dosya ve interface anlamına gelen maliyeti, ilerleyen aylarda kat kat geri dönüyor. Freelance projelerimde de aynı ölçütü kullanıyorum: bir haftalık bir entegrasyon işiyse basit tutuyorum, aylarca sürecek bir ürünse baştan bu ayrımı kuruyorum, çünkü sonradan katman eklemek, baştan doğru kurmaktan çok daha maliyetli.

Sonuç

Clean Architecture bana sihirli bir çözüm değil, net bir sınır çizme aracı sunuyor: iş mantığım nerede duruyor, altyapı detaylarım nerede duruyor, bunlar birbirine hangi noktada bağlanıyor. Bu netlik, proje büyüdükçe ve ekip değiştikçe asıl değerini gösteriyor. ASP.NET Core ekosisteminde bu prensipleri uygulamak, ekstra bir framework ya da paket gerektirmiyor; sadece bağımlılık yönünü doğru kurmaya dikkat etmek yeterli.

Bu tür mimari kararları kurumsal projelerde nasıl uyguladığımı daha yakından görmek isterseniz, geliştirdiğim projelere göz atabilir ya da benimle iletişime geçebilirsiniz.