.NET'te Clean Architecture: Ölçeklenebilir Proje Yapısı

15 dakika okuma9 Şubat 2026Güncellendi: 9 Mar 2026
.NET clean architectureClean architecture C#.NET project structureSOLID .NETOnion architecture.NET layersDomain driven design .NETscalable .NET

# .NET'te Clean Architecture: Olceklenebilir Proje Yapisi Olusturmak

Clean Architecture, bagimliliklari ice dogru yoneten ve degisime acik sistemler olusturmanin en etkili yollarindan biridir. Temel prensip basittir: is kurallari, framework veya altyapi detaylarina bagli olmamalidir. Bagimliliklar her zaman disa degil, ice dogru, domain cekirdegine isaret eder.

Kurumsal .NET projelerinde mimarlik yaparken bu yaklasimin, kod tabanlarinin sagligi uzerindeki etkisini defalarca gordum. Iyi uygulanan bir Clean Architecture, bir-iki yil sonra cokmek yerine zamanla olgunlasan bir sistem olusturur. Mesele katiligi degil, ekibin kodun nereye ait oldugu konusunda ortak bir zihinsel modele sahip olmasidir.

Proje Yapisi

Iyi organize edilmis bir solution, mimariyi bir bakista anlasilir kilar. Onerim su sekilde bir klasor ve dosya duzenidir:

src/
├── MyApp.Domain/
│   ├── Entities/
│   │   ├── Order.cs
│   │   ├── OrderItem.cs
│   │   └── Customer.cs
│   ├── ValueObjects/
│   │   ├── Money.cs
│   │   └── Address.cs
│   ├── Enums/
│   │   └── OrderStatus.cs
│   ├── Events/
│   │   └── OrderPlacedEvent.cs
│   ├── Exceptions/
│   │   └── InsufficientStockException.cs
│   └── MyApp.Domain.csproj
│
├── MyApp.Application/
│   ├── Common/
│   │   ├── Interfaces/
│   │   │   ├── IOrderRepository.cs
│   │   │   ├── IPaymentGateway.cs
│   │   │   └── IUnitOfWork.cs
│   │   ├── Behaviours/
│   │   │   ├── ValidationBehaviour.cs
│   │   │   └── LoggingBehaviour.cs
│   │   └── Mappings/
│   │       └── OrderMappingProfile.cs
│   ├── Orders/
│   │   ├── Commands/
│   │   │   ├── PlaceOrder/
│   │   │   │   ├── PlaceOrderCommand.cs
│   │   │   │   ├── PlaceOrderCommandHandler.cs
│   │   │   │   └── PlaceOrderCommandValidator.cs
│   │   │   └── CancelOrder/
│   │   │       ├── CancelOrderCommand.cs
│   │   │       └── CancelOrderCommandHandler.cs
│   │   └── Queries/
│   │       └── GetOrderById/
│   │           ├── GetOrderByIdQuery.cs
│   │           ├── GetOrderByIdQueryHandler.cs
│   │           └── OrderDto.cs
│   └── MyApp.Application.csproj
│
├── MyApp.Infrastructure/
│   ├── Persistence/
│   │   ├── AppDbContext.cs
│   │   ├── Configurations/
│   │   │   └── OrderConfiguration.cs
│   │   └── Repositories/
│   │       └── OrderRepository.cs
│   ├── Services/
│   │   └── StripePaymentGateway.cs
│   ├── DependencyInjection.cs
│   └── MyApp.Infrastructure.csproj
│
├── MyApp.API/
│   ├── Controllers/
│   │   └── OrdersController.cs
│   ├── Middleware/
│   │   └── ExceptionHandlingMiddleware.cs
│   ├── Program.cs
│   └── MyApp.API.csproj
│
tests/
├── MyApp.Domain.Tests/
├── MyApp.Application.Tests/
├── MyApp.Infrastructure.Tests/
└── MyApp.API.Tests/

Kural nettir: her proje yalnizca daha ic katmanlara referans verebilir, asla disa dogru degil. `Domain` hicbir seye referans vermez. `Application` yalnizca `Domain`'e referans verir. `Infrastructure` ve `API` ise `Application`'a (ve dolayli olarak `Domain`'e) referans verir.

Katman Sorumluluklari

Domain

Domain katmani sistemin kalbidir. Entity'ler, value object'ler, domain event'ler ve is kurallarini icerir. Hicbir dis bagimliligi yoktur. Loglama icin bile bir NuGet paketi buraya ait degildir.

csharp
public class Order
{
    private readonly List<OrderItem> _items = new();

    public Guid Id { get; private set; }
    public Guid CustomerId { get; private set; }
    public OrderStatus Status { get; private set; }
    public Money TotalAmount { get; private set; }
    public DateTime CreatedAt { get; private set; }

    public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();

    private Order() { } class=class="code-string">"code-comment">// EF Core constructor

    public static Order Create(Guid customerId)
    {
        var order = new Order
        {
            Id = Guid.NewGuid(),
            CustomerId = customerId,
            Status = OrderStatus.Draft,
            TotalAmount = Money.Zero(class="code-string">"USD"),
            CreatedAt = DateTime.UtcNow
        };

        order.AddDomainEvent(new OrderPlacedEvent(order.Id));
        return order;
    }

    public void AddItem(string productName, int quantity, Money unitPrice)
    {
        if (Status != OrderStatus.Draft)
            throw new InvalidOperationException(
                class="code-string">"Yalnizca taslak siparislere urun eklenebilir.");

        if (quantity <= class="code-number">0)
            throw new ArgumentException(
                class="code-string">"Miktar pozitif olmalidir.", nameof(quantity));

        var item = new OrderItem(Id, productName, quantity, unitPrice);
        _items.Add(item);
        RecalculateTotal();
    }

    public void Confirm()
    {
        if (_items.Count == class="code-number">0)
            throw new InvalidOperationException(
                class="code-string">"Urunu olmayan bir siparis onaylanamaz.");

        Status = OrderStatus.Confirmed;
    }

    private void RecalculateTotal()
    {
        TotalAmount = _items
            .Aggregate(Money.Zero(class="code-string">"USD"),
                (sum, item) => sum.Add(item.LineTotal));
    }
}

Dikkat edin: tum is kurallari entity'nin icinde yasiyor. Siparis, urun olmadan onaylanamayacagini biliyor. Toplam tutari nasil hesaplayacagini biliyor. Hicbir servis veya controller bu kurallari tekrarlamamali.

Application

Application katmani kullanim senaryolarini (use case) yonetir. Komutlari, sorgulari ve altyapinin uygulamasi gereken arayuzleri tanimlar. Dispatching icin MediatR kullaniyorum, ama ayni desen herhangi bir mediator veya duz metot cagrilariyla da calisir.

csharp
class=class="code-string">"code-comment">// Komut tanimi
public record PlaceOrderCommand(
    Guid CustomerId,
    List<OrderItemDto> Items) : IRequest<Guid>;

class=class="code-string">"code-comment">// Komut isleyicisi
public class PlaceOrderCommandHandler
    : IRequestHandler<PlaceOrderCommand, Guid>
{
    private readonly IOrderRepository _orderRepository;
    private readonly IUnitOfWork _unitOfWork;

    public PlaceOrderCommandHandler(
        IOrderRepository orderRepository,
        IUnitOfWork unitOfWork)
    {
        _orderRepository = orderRepository;
        _unitOfWork = unitOfWork;
    }

    public async Task<Guid> Handle(
        PlaceOrderCommand request,
        CancellationToken cancellationToken)
    {
        var order = Order.Create(request.CustomerId);

        foreach (var item in request.Items)
        {
            order.AddItem(
                item.ProductName,
                item.Quantity,
                new Money(item.Price, item.Currency));
        }

        order.Confirm();

        _orderRepository.Add(order);
        await _unitOfWork.SaveChangesAsync(cancellationToken);

        return order.Id;
    }
}
csharp
class=class="code-string">"code-comment">// Repository arayuzu Applicationclass="code-string">'da tanimlanir, Infrastructure'da degil
public interface IOrderRepository
{
    Task<Order?> GetByIdAsync(Guid id, CancellationToken ct = default);
    void Add(Order order);
    void Remove(Order order);
}

public interface IUnitOfWork
{
    Task<int> SaveChangesAsync(CancellationToken ct = default);
}

Handler bir tarif gibi okunur: siparis olustur, urunleri ekle, onayla, kaydet. Is kurallari degisirse domain entity'yi degistirirsiniz. Is akisi degisirse handler'i degistirirsiniz. Veritabani degisirse burada hicbir sey etkilenmez.

Infrastructure

Infrastructure katmani, application katmaninda tanimlanan sozlesmelerin teknik uygulamalarini icerir. Entity Framework, harici API istemcileri, mesaj kuyruklari ve dosya depolama burada yastiyor.

csharp
public class OrderRepository : IOrderRepository
{
    private readonly AppDbContext _context;

    public OrderRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task<Order?> GetByIdAsync(
        Guid id, CancellationToken ct = default)
    {
        return await _context.Orders
            .Include(o => o.Items)
            .FirstOrDefaultAsync(o => o.Id == id, ct);
    }

    public void Add(Order order)
    {
        _context.Orders.Add(order);
    }

    public void Remove(Order order)
    {
        _context.Orders.Remove(order);
    }
}
csharp
class=class="code-string">"code-comment">// Altyapi servislerinin kaydi
public static class DependencyInjection
{
    public static IServiceCollection AddInfrastructure(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        services.AddDbContext<AppDbContext>(options =>
            options.UseNpgsql(
                configuration.GetConnectionString(class="code-string">"Default")));

        services.AddScoped<IOrderRepository, OrderRepository>();
        services.AddScoped<IUnitOfWork>(sp =>
            sp.GetRequiredService<AppDbContext>());

        return services;
    }
}

Presentation

Controller'lar veya minimal API endpoint'leri yalnizca HTTP konulariyla ilgilenir: istek deserializasyonu, komut/sorgu gonderimi ve yanit seklillendirme. Hicbir is mantigi burada yer almamalidir.

csharp
[ApiController]
[Route(class="code-string">"api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly ISender _sender;

    public OrdersController(ISender sender)
    {
        _sender = sender;
    }

    [HttpPost]
    public async Task<IActionResult> PlaceOrder(
        PlaceOrderRequest request,
        CancellationToken ct)
    {
        var command = new PlaceOrderCommand(
            request.CustomerId, request.Items);

        var orderId = await _sender.Send(command, ct);

        return CreatedAtAction(
            nameof(GetOrder),
            new { id = orderId },
            new { id = orderId });
    }

    [HttpGet(class="code-string">"{id:guid}")]
    public async Task<IActionResult> GetOrder(Guid id, CancellationToken ct)
    {
        var query = new GetOrderByIdQuery(id);
        var result = await _sender.Send(query, ct);

        return result is null ? NotFound() : Ok(result);
    }
}

Neden Gercek Ekiplerde Ise Yariyor

  • **Tahmin edilebilir sinirlar kazara baglantilari azaltir.** Her gelistirici veritabani kodunun `Infrastructure`'a, is kurallarinin `Domain`'e ait oldugunu bildiginde, kod incelemeleri daha hizli ve odakli olur.
  • **Birim testleri daha hizli ve ucuz hale gelir.** Domain ve application katmanlarinin altyapi bagimliligi yoktur, bu yuzden testler veritabani veya container olmadan milisaniyeler icinde calisir.
  • **Altyapi degisiklikleri daha dusuk etki alanina sahiptir.** PostgreSQL'den CosmosDB'ye gecmek, repository'leri yeniden yazmak demektir, is mantigi degil. Bu degisikligi bir production sistemde application katmaninda sifir degisiklikle gerceklestirdim.
  • **Ekipler arasi paralel gelistirme kolaylasir.** Bir ekip odeme entegrasyonunu yaparken digeri siparis is akisini gelistirebilir; arayuz sinirinda bulusurlar.
  • Dikey Dilimler (Vertical Slices) vs Katmanli: Hangisini Ne Zaman Secmeli

    Bu konu her mimari tartismasinda gundeme gelir ve durustce soylemeliyim: bu iki yaklasim birbirini dislamaz.

    Geleneksel katmanli mimari kodu teknik kaygiya gore organize eder. Tum controller'lar bir klasorde, tum servisler digerinde, tum repository'ler ucuncusunde. Bu, katmanlarin ic tutarliligi yuksek oldugunda ise yarar, ancak buyuk projelerde tek bir ozelligi anlamak icin bes klasor arasinda gezinmeniz gerekir.

    Dikey dilim mimarisi kodu ozellige gore organize eder. "Siparis Ver" ile ilgili her sey tek bir klasorde yastiyor: endpoint, handler, validator, veri erisimi. Her dilim bagimsiz ve kendi icinde butundur.

    Kurumsal .NET projelerinde mimarlik yaparken hibrit bir yaklasim kullaniyorum: makro yapi ve proje sinirlari icin Clean Architecture katmanlari, her katmanin icinde ise ozellige dayali klasorler. Yukardaki proje yapisi zaten bunu yansitiyor. `Orders/Commands/PlaceOrder/` klasoru, application katmani icindeki dikey bir dilimdir.

    | Yon | Saf Katmanli | Saf Dikey Dilim | Hibrit (Onerilen) |

    |-----|-------------|-----------------|-------------------|

    | Ozellik kesfedilebilirligi | Dusuk | Yuksek | Yuksek |

    | Capraz kesen ilgiler | Kolay | Konvansiyon gerektirir | Behaviour'lar ile yonetilir |

    | Ekip olceklenmesi | Orta | Yuksek | Yuksek |

    | Yeniden duzenleme guvenligi | Yuksek (testlerle) | Orta | Yuksek |

    | Ogrenme egrisi | Dusuk | Orta | Orta |

    Temel fikir: projeler arasi Clean Architecture sinirlariyla baslayin, ardinca dosyalari bu projelerin icinde ozellige gore organize edin. Katmanlarin bagimlilik guvenligini, dilimlerin gezinilebilirligiyle birlikte elde edersiniz.

    Her Katmani Test Etmek

    Her katmanin farkli bir test stratejisi vardir. Bunu dogru yapmak, saniyeler icinde calisan bir test suitesi ile yirmi dakika suren bir test suitesi arasindaki farkdir.

    Domain Testleri

    Domain testleri saf birim testleridir. Mock framework'u gerekmez, test container'i gerekmez, kurulum gerekmez. Sadece entity'yi olusturun ve davranisi dogrulayin.

    csharp
    public class OrderTests
    {
        [Fact]
        public void Confirm_WithItems_ShouldSetStatusToConfirmed()
        {
            var order = Order.Create(Guid.NewGuid());
            order.AddItem(class="code-string">"Widget", class="code-number">2, new Money(class="code-number">10.00m, class="code-string">"USD"));
    
            order.Confirm();
    
            Assert.Equal(OrderStatus.Confirmed, order.Status);
        }
    
        [Fact]
        public void Confirm_WithoutItems_ShouldThrow()
        {
            var order = Order.Create(Guid.NewGuid());
    
            Assert.Throws<InvalidOperationException>(() => order.Confirm());
        }
    
        [Fact]
        public void AddItem_ShouldRecalculateTotal()
        {
            var order = Order.Create(Guid.NewGuid());
    
            order.AddItem(class="code-string">"Widget", class="code-number">3, new Money(class="code-number">10.00m, class="code-string">"USD"));
    
            Assert.Equal(class="code-number">30.00m, order.TotalAmount.Amount);
        }
    }

    Application Testleri

    Application testleri kullanim senaryosu orkestrasyon'unu dogrular. Repository ve unit of work arayuzlerini mocklayin, ardindan handler'in bunlari dogru cagirdigini dogrulayin.

    csharp
    public class PlaceOrderCommandHandlerTests
    {
        [Fact]
        public async Task Handle_ValidOrder_ShouldPersistAndReturnId()
        {
            var repo = new Mock<IOrderRepository>();
            var uow = new Mock<IUnitOfWork>();
            var handler = new PlaceOrderCommandHandler(repo.Object, uow.Object);
    
            var command = new PlaceOrderCommand(
                Guid.NewGuid(),
                new List<OrderItemDto>
                {
                    new(class="code-string">"Widget", class="code-number">2, class="code-number">15.00m, class="code-string">"USD")
                });
    
            var orderId = await handler.Handle(command, CancellationToken.None);
    
            repo.Verify(r => r.Add(It.Is<Order>(o =>
                o.Items.Count == class="code-number">1 && o.Status == OrderStatus.Confirmed)),
                Times.Once);
            uow.Verify(u => u.SaveChangesAsync(It.IsAny<CancellationToken>()),
                Times.Once);
            Assert.NotEqual(Guid.Empty, orderId);
        }
    }

    Infrastructure Testleri

    Infrastructure testleri gercek bir veritabani kullanir; genellikle in-memory provider veya Testcontainers ornegi. Bunlar, EF Core mapping'lerinizin ve sorgularinizin dogru calistigini dogrulayan entegrasyon testleridir.

    csharp
    public class OrderRepositoryTests : IDisposable
    {
        private readonly AppDbContext _context;
    
        public OrderRepositoryTests()
        {
            var options = new DbContextOptionsBuilder<AppDbContext>()
                .UseInMemoryDatabase(Guid.NewGuid().ToString())
                .Options;
            _context = new AppDbContext(options);
        }
    
        [Fact]
        public async Task GetByIdAsync_ExistingOrder_ShouldReturnWithItems()
        {
            var order = Order.Create(Guid.NewGuid());
            order.AddItem(class="code-string">"Widget", class="code-number">1, new Money(class="code-number">9.99m, class="code-string">"USD"));
            _context.Orders.Add(order);
            await _context.SaveChangesAsync();
    
            var repo = new OrderRepository(_context);
            var result = await repo.GetByIdAsync(order.Id);
    
            Assert.NotNull(result);
            Assert.Single(result!.Items);
        }
    
        public void Dispose() => _context.Dispose();
    }

    API / Entegrasyon Testleri

    Uctan uca testler `WebApplicationFactory` kullanarak tam boru hattini ayaga kaldirir ve HTTP davranisini dogrular.

    csharp
    public class OrdersEndpointTests
        : IClassFixture<WebApplicationFactory<Program>>
    {
        private readonly HttpClient _client;
    
        public OrdersEndpointTests(WebApplicationFactory<Program> factory)
        {
            _client = factory.CreateClient();
        }
    
        [Fact]
        public async Task PlaceOrder_ShouldReturn201()
        {
            var request = new { CustomerId = Guid.NewGuid(),
                Items = new[] { new { ProductName = class="code-string">"Widget",
                    Quantity = class="code-number">1, Price = class="code-number">9.99, Currency = class="code-string">"USD" } } };
    
            var response = await _client.PostAsJsonAsync(class="code-string">"/api/orders", request);
    
            Assert.Equal(HttpStatusCode.Created, response.StatusCode);
        }
    }

    Sik Yapilan Mimari Hatalar

    Bunlar danismanlik calismalarinda ve production kod tabanlarinda defalarca gordugum kaliplardir. Her biri baslangicta zararsiz gorunur ama zamanla ciddi sorunlara donusur.

    1. Controller'da Is Mantigi

    En yaygin ihlal. Indirim hesaplayan, stok seviyelerini dogrulayan veya e-posta gonderen bir controller, domain ve application katmanlarinin isini yapiyor demektir. Sonuc: HTTP sunucusu ayaga kaldirmadan test edilemeyen ve ikinci bir endpoint ayni davranisa ihtiyac duydugunda kopyalanan mantik.

    2. Kansiz Domain Modeli (Anemic Domain Model)

    Public setter'lari olan property torbalari olarak kalan entity'ler ve tum davranisi iceren "servis" siniflari. Bu, domain katmanina sahip olmanin amacini yok eder. `OrderService.Confirm(order)` varsa, kendinize `order.Confirm()` neden yok diye sorun.

    3. Altyapiyi Application'a Sizdirmak

    Komut handler'inizin `IOrderRepository` yerine dogrudan `DbContext`'e referans verdigi an, kullanim senaryonuzu Entity Framework'e baglamissiniz demektir. Bu, test etmeyi zorlastirir ve baska bir veri deposuna gecisi, is mantigi yeniden yazilmadan neredeyse imkansiz kilar.

    4. Her Seyi Asiri Soyutlamak

    Tek bir komut handler'in yetecegi yerde `IOrderService`, `IOrderManager`, `IOrderProcessor` ve `IOrderHandler` olusturmak. Her arayuz, bir seyin takas edilmesi veya izole test edilmesi gerektigi icin var olmalidir. Tuketicisi olmayan soyutlamalar gurultudur.

    5. Bagimlilik Kuralini Ihlal Etmek

    Bagimlilik kurali, ic katmanlarin dis katmanlari bilmemesi gerektigini soyler. `Domain` projenizde `using Microsoft.EntityFrameworkCore` ifadesi varsa, bir seyler yanlis gitmistir. Bunu CI'da otomatik olarak uygulamak icin mimari testler (NetArchTest veya ArchUnitNET) kullanin.

    csharp
    [Fact]
    public void Domain_ShouldNotReference_Infrastructure()
    {
        var result = Types.InAssembly(typeof(Order).Assembly)
            .ShouldNot()
            .HaveDependencyOn(class="code-string">"MyApp.Infrastructure")
            .GetResult();
    
        Assert.True(result.IsSuccessful);
    }

    6. CQRS'yi Gereken Yerde Kullanmamak

    Okuma ve yazma icin ayni modeli kullanmak, sismis DTO'lara ve garip sorgulara yol acar. Okuma modellerini (optimize edilmis projeksiyonlar) yazma modellerinden (zengin domain entity'leri) ayirmak her iki tarafi da onemli olcude basitlestirir. CQRS'den faydalanmak icin event sourcing'e ihtiyaciniz yoktur.

    Uygulama Tavsiyeleri

  • **Erken asiri muhendislikten kacinin.** Iki projeyle (API + Domain) baslayin ve uc veya daha fazla kullanim senaryosu ayrimayi haklicikardiginda Application ve Infrastructure'i ayirin.
  • **Kullanim senaryolarini acik ve kucuk tutun.** Bir komut handler'i tek bir sey yapmalidir. Bir handler elli satiri asiyorsa, muhtemelen cok fazla is yapiyor demektir.
  • **Teknik mega-klasorler yerine ozellige dayali klasorleri tercih edin.** `Orders/Commands/PlaceOrder/` adinda bir klasor, kirk dosya iceren `Handlers/` adinda bir klasorden cok daha kolay gezinilir.
  • **Sinirlari uygulamak icin mimari testler ekleyin.** Insanlar kurallari unutur. CI unutmaz.
  • **Capraz kesen ilgiler icin MediatR pipeline behaviour'lari kullanin.** Dogrulama, loglama ve islem yonetimi behaviour'larda olmali, her handler'a dagitilmamali.
  • Sonuc

    Clean Architecture, urun kapsamlniz ve ekip buyuklugunuz buyudukce en degerli hale gelir. Kisa vadeli kucuk bir seremoni karsiliginda uzun vadeli istikrar ve netlik saglar. Katmanli yapi bir deli gomllegi degildir; kod tabaninizi gezilebilir ve dagitimlarinizi ongorebilebilir tutan bir korkuluklar setidir.

    Kurumsal .NET projelerinde mimarlik yaparken, bu yapiyi benimseyen ekiplerin baslangic ogrenme egrisinden sonra surekli olarak daha hizli teslimat yaptigini gordum. Cunku her gelistirici yeni kodu tam olarak nereye koyacagini ve mevcut mantigi nerede bulacagini biliyordu.

    Proje mimarinizi birlikte tasarlayabiliriz.

    İlgili Makaleler

    Flutter Projeniz mi Var?

    iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.

    İletişime Geç