Clean Architecture, Robert C. Martin (Uncle Bob) tarafından önerilen, yazılım mimarisinde bağımlılıkları yönetme ve sürdürülebilir kod yazma prensiplerini içeren bir yaklaşımdır. Bu rehberde .NET projelerinde Clean Architecture'ın nasıl uygulanacağını kapsamlı bir şekilde öğreneceksiniz.

Clean Architecture Nedir?

Frameworks & Drivers
Interface Adapters
Application Business Rules
Enterprise Business Rules

Temel Amaç

Clean Architecture'ın temel amacı iş mantığını dış dünyadan izole etmektir. Bu sayede:

  • Framework bağımsızlığı sağlanır
  • Test edilebilirlik artar
  • UI değişikliklerinden etkilenmez
  • Veritabanı teknolojisinden bağımsız olur

Bağımlılık Kuralı

En Önemli Kural

İç katmanlar dış katmanları bilemez! Bağımlılık yönü her zaman dıştan içe doğru olmalıdır.

.NET Clean Architecture Katmanları

Domain Layer

Core

İş kurallarının kalbi - En merkezi katman

İçerdikleri:

Entities Value Objects Domain Events Specifications Business Rules
Örnek: Product, Customer, Order sınıfları

Application Layer

Use Cases

Uygulama iş kuralları - Domain'i orkestra eder

İçerdikleri:

Use Cases Commands & Queries Handlers DTOs Interfaces
Örnek: CreateProductCommand, GetCustomerQuery

Infrastructure Layer

External

Dış dünya bağlantıları - Framework implementasyonları

İçerdikleri:

Repositories DbContext External APIs File System Email Services
Örnek: ProductRepository, EmailService

Presentation Layer

UI

Kullanıcı arayüzü - Dış dünya ile etkileşim

İçerdikleri:

Controllers Views ViewModels API Endpoints Middleware
Örnek: ProductController, CustomerViewModel

Bağımlılık Akışı

Presentation
Application
Domain
Infrastructure implements interfaces
Infrastructure

Proje Yapısı

MyProject.Solution
src
MyProject.Domain
Entities/
ValueObjects/
Events/
MyProject.Application
Features/
Interfaces/
DTOs/
MyProject.Infrastructure
Repositories/
Data/
Services/
MyProject.API
Controllers/
Middleware/
Program.cs
tests
MyProject.UnitTests
MyProject.IntegrationTests

Kod Örnekleri

Domain Layer - Entity Örneği

Domain/Entities/Product.cs C#
public class Product : BaseEntity
{
    public string Name { get; private set; }
    public decimal Price { get; private set; }
    public string Description { get; private set; }
    public bool IsActive { get; private set; }
    
    private readonly List<DomainEvent> _events = new();
    public IReadOnlyCollection<DomainEvent> Events => _events.AsReadOnly();

    private Product() { } // EF Core için

    public Product(string name, decimal price, string description)
    {
        if (string.IsNullOrEmpty(name))
            throw new ArgumentException("Product name cannot be empty");
            
        if (price <= 0)
            throw new ArgumentException("Price must be greater than zero");

        Name = name;
        Price = price;
        Description = description;
        IsActive = true;
        
        _events.Add(new ProductCreatedEvent(this));
    }

    public void UpdatePrice(decimal newPrice)
    {
        if (newPrice <= 0)
            throw new ArgumentException("Price must be greater than zero");

        var oldPrice = Price;
        Price = newPrice;
        
        _events.Add(new ProductPriceUpdatedEvent(this, oldPrice, newPrice));
    }

    public void Deactivate()
    {
        IsActive = false;
        _events.Add(new ProductDeactivatedEvent(this));
    }
}

Application Layer - Use Case Örneği

Application/Features/Products/Commands/CreateProduct.cs C#
public record CreateProductCommand(
    string Name,
    decimal Price,
    string Description
) : ICommand<ProductDto>;

public class CreateProductCommandHandler : ICommandHandler<CreateProductCommand, ProductDto>
{
    private readonly IProductRepository _productRepository;
    private readonly IUnitOfWork _unitOfWork;
    private readonly IMapper _mapper;
    private readonly ILogger<CreateProductCommandHandler> _logger;

    public CreateProductCommandHandler(
        IProductRepository productRepository,
        IUnitOfWork unitOfWork,
        IMapper mapper,
        ILogger<CreateProductCommandHandler> logger)
    {
        _productRepository = productRepository;
        _unitOfWork = unitOfWork;
        _mapper = mapper;
        _logger = logger;
    }

    public async Task<ProductDto> Handle(CreateProductCommand request, CancellationToken cancellationToken)
    {
        _logger.LogInformation("Creating product with name: {Name}", request.Name);

        // Business rule validation
        var existingProduct = await _productRepository.GetByNameAsync(request.Name);
        if (existingProduct != null)
            throw new BusinessRuleException("Product with this name already exists");

        // Create domain entity
        var product = new Product(request.Name, request.Price, request.Description);

        // Save to repository
        await _productRepository.AddAsync(product);
        await _unitOfWork.SaveChangesAsync(cancellationToken);

        _logger.LogInformation("Product created successfully with ID: {Id}", product.Id);

        return _mapper.Map<ProductDto>(product);
    }
}

Infrastructure Layer - Repository Örneği

Infrastructure/Repositories/ProductRepository.cs C#
public class ProductRepository : IProductRepository
{
    private readonly ApplicationDbContext _context;

    public ProductRepository(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<Product?> GetByIdAsync(int id)
    {
        return await _context.Products
            .FirstOrDefaultAsync(p => p.Id == id);
    }

    public async Task<Product?> GetByNameAsync(string name)
    {
        return await _context.Products
            .FirstOrDefaultAsync(p => p.Name == name);
    }

    public async Task<IEnumerable<Product>> GetActiveProductsAsync()
    {
        return await _context.Products
            .Where(p => p.IsActive)
            .ToListAsync();
    }

    public async Task AddAsync(Product product)
    {
        await _context.Products.AddAsync(product);
    }

    public void Update(Product product)
    {
        _context.Products.Update(product);
    }

    public void Delete(Product product)
    {
        _context.Products.Remove(product);
    }
}

Presentation Layer - Controller Örneği

API/Controllers/ProductsController.cs C#
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IMediator _mediator;

    public ProductsController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpPost]
    public async Task<ActionResult<ProductDto>> CreateProduct(CreateProductCommand command)
    {
        try
        {
            var result = await _mediator.Send(command);
            return CreatedAtRoute(nameof(GetProduct), new { id = result.Id }, result);
        }
        catch (BusinessRuleException ex)
        {
            return BadRequest(ex.Message);
        }
    }

    [HttpGet("{id}", Name = nameof(GetProduct))]
    public async Task<ActionResult<ProductDto>> GetProduct(int id)
    {
        var query = new GetProductByIdQuery(id);
        var result = await _mediator.Send(query);
        
        if (result == null)
            return NotFound();
            
        return Ok(result);
    }

    [HttpGet]
    public async Task<ActionResult<IEnumerable<ProductDto>>> GetProducts()
    {
        var query = new GetActiveProductsQuery();
        var result = await _mediator.Send(query);
        return Ok(result);
    }
}

CQRS ve MediatR Entegrasyonu

CQRS (Command Query Responsibility Segregation)

CQRS, okuma ve yazma işlemlerini ayrı modeller halinde organize etme yaklaşımıdır. Clean Architecture ile mükemmel uyum sağlar.

CQRS + Clean Architecture Avantajları

Ayrım (Separation)

Okuma ve yazma işlemleri birbirinden bağımsız

Ölçeklenebilirlik

Her işlem türü bağımsız olarak optimize edilebilir

Daha Temiz Kod

Her handler tek bir sorumluluğa sahip

Test Edilebilirlik

Her use case ayrı ayrı test edilebilir

MediatR Konfigürasyonu

Program.cs C#
// MediatR registration
builder.Services.AddMediatR(cfg => {
    cfg.RegisterServicesFromAssembly(typeof(CreateProductCommand).Assembly);
});

// FluentValidation registration
builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddValidatorsFromAssembly(typeof(CreateProductCommandValidator).Assembly);

// AutoMapper registration
builder.Services.AddAutoMapper(typeof(ProductMappingProfile).Assembly);

// Repository registrations
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();

Domain Driven Design (DDD) Entegrasyonu

DDD Temel Kavramları

Entity

Benzersiz kimliği olan ve yaşam döngüsü boyunca takip edilen nesneler

Örnek: Customer, Product, Order

Value Object

Kimliği olmayan, değerleriyle tanımlanan değişmez nesneler

Örnek: Money, Email, Address

Aggregate

İş kuralları açısından bir bütün olarak ele alınan nesne grubu

Örnek: Order + OrderItems

Domain Service

Tek bir entity'ye ait olmayan domain mantığını içeren servisler

Örnek: PricingService, InventoryService

Value Object Örneği

Domain/ValueObjects/Money.cs C#
public class Money : ValueObject
{
    public decimal Amount { get; private set; }
    public string Currency { get; private set; }

    private Money() { } // EF Core için

    public Money(decimal amount, string currency)
    {
        if (amount < 0)
            throw new ArgumentException("Amount cannot be negative");
            
        if (string.IsNullOrEmpty(currency))
            throw new ArgumentException("Currency cannot be empty");

        Amount = amount;
        Currency = currency;
    }

    public static Money operator +(Money a, Money b)
    {
        if (a.Currency != b.Currency)
            throw new InvalidOperationException("Cannot add different currencies");
            
        return new Money(a.Amount + b.Amount, a.Currency);
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return Amount;
        yield return Currency;
    }

    public override string ToString() => $"{Amount:C} {Currency}";
}

Domain Events

Domain/Events/ProductCreatedEvent.cs C#
public class ProductCreatedEvent : DomainEvent
{
    public Product Product { get; }

    public ProductCreatedEvent(Product product)
    {
        Product = product;
    }
}

public class ProductCreatedEventHandler : INotificationHandler<ProductCreatedEvent>
{
    private readonly IEmailService _emailService;
    private readonly ILogger<ProductCreatedEventHandler> _logger;

    public ProductCreatedEventHandler(IEmailService emailService, ILogger<ProductCreatedEventHandler> logger)
    {
        _emailService = emailService;
        _logger = logger;
    }

    public async Task Handle(ProductCreatedEvent notification, CancellationToken cancellationToken)
    {
        _logger.LogInformation("Product created: {ProductName}", notification.Product.Name);
        
        // Send notification email to administrators
        await _emailService.SendProductCreatedNotificationAsync(notification.Product);
    }
}

Test Stratejisi

Test Piramidi

Unit Tests 70%
Integration Tests 20%
E2E Tests 10%

Test Örnekleri

Unit Test Örneği
Tests/Domain/ProductTests.cs C#
public class ProductTests
{
    [Fact]
    public void CreateProduct_WithValidData_ShouldCreateSuccessfully()
    {
        // Arrange
        var name = "Test Product";
        var price = 100m;
        var description = "Test Description";

        // Act
        var product = new Product(name, price, description);

        // Assert
        product.Name.Should().Be(name);
        product.Price.Should().Be(price);
        product.Description.Should().Be(description);
        product.IsActive.Should().BeTrue();
        product.Events.Should().ContainSingle(e => e is ProductCreatedEvent);
    }

    [Theory]
    [InlineData("")]
    [InlineData(null)]
    public void CreateProduct_WithInvalidName_ShouldThrowException(string invalidName)
    {
        // Act & Assert
        var action = () => new Product(invalidName, 100m, "Description");
        action.Should().Throw<ArgumentException>();
    }
}
Integration Test Örneği
Tests/Application/CreateProductCommandHandlerTests.cs C#
public class CreateProductCommandHandlerTests : IntegrationTestBase
{
    [Fact]
    public async Task Handle_WithValidCommand_ShouldCreateProduct()
    {
        // Arrange
        var command = new CreateProductCommand("Test Product", 100m, "Description");

        // Act
        var result = await Mediator.Send(command);

        // Assert
        result.Should().NotBeNull();
        result.Name.Should().Be(command.Name);
        
        var productInDb = await Context.Products.FirstOrDefaultAsync(p => p.Id == result.Id);
        productInDb.Should().NotBeNull();
    }

    [Fact]
    public async Task Handle_WithDuplicateName_ShouldThrowException()
    {
        // Arrange
        var existingProduct = new Product("Existing Product", 50m, "Description");
        Context.Products.Add(existingProduct);
        await Context.SaveChangesAsync();

        var command = new CreateProductCommand("Existing Product", 100m, "New Description");

        // Act & Assert
        var action = async () => await Mediator.Send(command);
        await action.Should().ThrowAsync<BusinessRuleException>();
    }
}

Clean Architecture Avantajları

Test Edilebilirlik

Her katman bağımsız olarak test edilebilir

Unit testler kolay Mocking basit TDD uyumlu

Sürdürülebilirlik

Kod değişiklikleri lokalize edilmiş

Kolay refactoring Düşük coupling Yüksek cohesion

Esneklik

Framework ve teknoloji değişikliklerine açık

Database değiştirilebilir UI framework değiştirilebilir 3rd party entegrasyonlar kolay

Ölçeklenebilirlik

Büyük ekipler paralel çalışabilir

Feature team'ler Paralel geliştirme Microservice'e geçiş kolay

İş Odaklılık

Domain mantığı merkeze alınmış

Business rules açık Domain expert'ler anlayabilir Ubiquitous language

Bağımsızlık

Framework ve external dependency'lerden bağımsız

Framework agnostic Database agnostic UI agnostic

Dikkat Edilmesi Gerekenler

Karmaşıklık

Küçük projeler için over-engineering olabilir

Proje büyüklüğüne göre karar verin

Öğrenme Eğrisi

Yeni geliştiriciler için öğrenme süreci gerekli

Ekip eğitimine yatırım yapın

İlk Kurulum

Başlangıçta daha fazla kod yazmanız gerekir

Template'ler ve code generator'lar kullanın

Ekip Uyumu

Tüm ekibin aynı prensipleri takip etmesi gerekli

Code review süreçleri ve coding standard'lar belirleyin

En İyi Pratikler

Interface Segregation

Repository interface'lerini küçük ve odaklanmış tutun

Rich Domain Model

Entity'ler sadece data container değil, behavior içermeli

Fail Fast

Validation'ları mümkün olduğunca erken yapın

Dependency Injection

Constructor injection kullanın, service locator anti-pattern'den kaçının

Sonuç

.NET Clean Architecture, sürdürülebilir, test edilebilir ve esnek uygulamalar geliştirmek için güçlü bir yaklaşımdır. SOLID prensiplerine dayanan bu mimari, özellikle orta ve büyük ölçekli projeler için büyük avantajlar sağlar.

Ana Çıkarımlar

  • Domain mantığını merkeze alın ve koruyun
  • Bağımlılık yönünü her zaman dıştan içe doğru tutun
  • CQRS ve MediatR ile use case'leri net bir şekilde ayrın
  • DDD prensipleriyle domain modeling yapın
  • Kapsamlı test stratejisi uygulayın
  • Proje büyüklüğüne göre complexity seviyesini ayarlayın

Sonraki Adımlar

  • Küçük bir proje ile Clean Architecture pratik yapın
  • MediatR ve FluentValidation kütüphanelerini keşfedin
  • DDD pattern'lerini daha detaylı öğrenin
  • Entity Framework Core ile Clean Architecture entegrasyonu yapın
  • Microservices mimarisine geçiş stratejilerini araştırın