.NET ile Clean Architecture: Pratik Rehber

C# ile .NET'te Clean Architecture'a hâkim olun. SOLID prensiplerini, katman ayrımını ve sürdürülebilir uygulamalar için uygulama desenlerini öğrenin.

.NET ve C# ile Clean Architecture rehberi

Robert C. Martin (Uncle Bob) tarafından popülerleştirilen Clean Architecture, kodu iş mantığı uygulamanın merkezinde olacak şekilde, framework'lerden ve uygulama detaylarından bağımsız olarak organize eder. Bu mimari yaklaşım .NET uygulamaları için test edilebilirlik, sürdürülebilirlik ve ölçeklenebilirliği güvence altına alır. Bu rehber ASP.NET Core ile pratik bir uygulamayı sunar.

Neden Clean Architecture?

İş mantığını altyapı koduyla karıştıran uygulamalar hızla bakımı zorlaşan bir hâle gelir. Clean Architecture, teknik detayların iş çekirdeğini etkilemeden değiştirilebilmesini sağlayan sıkı bir ayrım dayatır.

Clean Architecture'ın Temel Prensipleri

Clean Architecture bağımlılık tersine çevirme prensibine dayanır: iç katmanlar dış katmanları bilmez. İş alanı izole kalır ve web framework'ü ya da veritabanı gibi teknik tercihlerden bağımsız olarak gelişebilir.

text
┌─────────────────────────────────────────────────────────┐
│                     Presentation                        │
│  (Controllers, Razor Pages, Blazor, API Endpoints)     │
├─────────────────────────────────────────────────────────┤
│                    Infrastructure                       │
│    (EF Core, External APIs, File System, Email)        │
├─────────────────────────────────────────────────────────┤
│                      Application                        │
│       (Use Cases, Commands, Queries, DTOs)             │
├─────────────────────────────────────────────────────────┤
│                        Domain                           │
│     (Entities, Value Objects, Domain Services)          │
└─────────────────────────────────────────────────────────┘

Bağımlılıklar her zaman içeriye doğru akar: Presentation → Infrastructure → Application → Domain. Domain başka hiçbir projeye referans vermez.

Clean Architecture İçin .NET Proje Yapısı

Proje organizasyonu farklı katmanları yansıtır. Her katman, Visual Studio çözümünde ayrı bir projeye karşılık gelir ve sorumlulukların fiziksel olarak ayrılmasını sağlar.

bash
# terminal
# Çözüm yapısını oluştur
dotnet new sln -n CleanArchitecture

# Her katman için proje oluştur
dotnet new classlib -n CleanArchitecture.Domain -o src/CleanArchitecture.Domain
dotnet new classlib -n CleanArchitecture.Application -o src/CleanArchitecture.Application
dotnet new classlib -n CleanArchitecture.Infrastructure -o src/CleanArchitecture.Infrastructure
dotnet new webapi -n CleanArchitecture.Api -o src/CleanArchitecture.Api

# Projeleri çözüme ekle
dotnet sln add src/CleanArchitecture.Domain
dotnet sln add src/CleanArchitecture.Application
dotnet sln add src/CleanArchitecture.Infrastructure
dotnet sln add src/CleanArchitecture.Api

# Proje referanslarını yapılandır
cd src/CleanArchitecture.Application
dotnet add reference ../CleanArchitecture.Domain

cd ../CleanArchitecture.Infrastructure
dotnet add reference ../CleanArchitecture.Application

cd ../CleanArchitecture.Api
dotnet add reference ../CleanArchitecture.Infrastructure

Bu yapı Domain'in bağımsız kalmasını ve bağımlılıkların Clean Architecture'ın Dependency Rule'una uymasını garanti eder.

Domain Katmanı: İş Çekirdeği

Domain katmanı iş varlıklarını, Value Object'leri ve repository arabirimlerini içerir. Bu katmanda hiçbir dış bağımlılığa izin verilmez.

Domain/Entities/Order.cscsharp
namespace CleanArchitecture.Domain.Entities;

// Kimliği ve iş yaşam döngüsü olan varlık
public class Order
{
    // Benzersiz sipariş tanımlayıcısı
    public Guid Id { get; private set; }

    // Müşteri referansı (e-posta için Value Object)
    public string CustomerEmail { get; private set; }

    // Kalem koleksiyonu (one-to-many ilişkisi)
    private readonly List<OrderItem> _items = new();
    public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();

    // Sipariş durumu (iş enum'u)
    public OrderStatus Status { get; private set; }

    // Hesaplanmış toplam tutar
    public decimal TotalAmount => _items.Sum(i => i.TotalPrice);

    // Takip tarihleri
    public DateTime CreatedAt { get; private set; }
    public DateTime? ShippedAt { get; private set; }

    // Özel kurucu, factory yönteminin kullanılmasını zorunlu kılar
    private Order() { }

    // Geçerli bir sipariş oluşturmak için factory yöntemi
    public static Order Create(string customerEmail)
    {
        // Oluşturma sırasında iş kurallarının doğrulanması
        if (string.IsNullOrWhiteSpace(customerEmail))
            throw new DomainException("Müşteri e-posta adresi zorunludur.");

        if (!IsValidEmail(customerEmail))
            throw new DomainException("Geçersiz e-posta formatı.");

        return new Order
        {
            Id = Guid.NewGuid(),
            CustomerEmail = customerEmail,
            Status = OrderStatus.Pending,
            CreatedAt = DateTime.UtcNow
        };
    }

    // İş yöntemi: bir kalem ekle
    public void AddItem(Product product, int quantity)
    {
        // İş kuralı: gönderilmiş bir sipariş değiştirilemez
        if (Status == OrderStatus.Shipped)
            throw new DomainException("Gönderilmiş bir sipariş değiştirilemez.");

        if (quantity <= 0)
            throw new DomainException("Adet pozitif olmalıdır.");

        // Ürünün zaten var olup olmadığını kontrol et
        var existingItem = _items.FirstOrDefault(i => i.ProductId == product.Id);
        if (existingItem != null)
        {
            existingItem.IncreaseQuantity(quantity);
        }
        else
        {
            _items.Add(OrderItem.Create(this, product, quantity));
        }
    }

    // İş yöntemi: siparişi onayla
    public void Confirm()
    {
        if (Status != OrderStatus.Pending)
            throw new DomainException("Yalnızca bekleyen siparişler onaylanabilir.");

        if (!_items.Any())
            throw new DomainException("Sipariş en az bir kalem içermelidir.");

        Status = OrderStatus.Confirmed;
    }

    // İş yöntemi: siparişi gönder
    public void Ship()
    {
        if (Status != OrderStatus.Confirmed)
            throw new DomainException("Sipariş gönderilmeden önce onaylanmalıdır.");

        Status = OrderStatus.Shipped;
        ShippedAt = DateTime.UtcNow;
    }

    private static bool IsValidEmail(string email) =>
        email.Contains('@') && email.Contains('.');
}

Order varlığı iş kurallarını kapsüller ve iç durumunu korur. Değişiklikler, değişmezleri doğrulayan iş yöntemlerinden geçmek zorundadır.

Domain/Entities/OrderItem.cscsharp
namespace CleanArchitecture.Domain.Entities;

// Kendi kimliğine sahip alt varlık
public class OrderItem
{
    public Guid Id { get; private set; }
    public Guid OrderId { get; private set; }
    public Guid ProductId { get; private set; }
    public string ProductName { get; private set; }
    public decimal UnitPrice { get; private set; }
    public int Quantity { get; private set; }

    // Satır toplam fiyatının hesaplanması
    public decimal TotalPrice => UnitPrice * Quantity;

    private OrderItem() { }

    // Doğrulama içeren factory yöntemi
    public static OrderItem Create(Order order, Product product, int quantity)
    {
        return new OrderItem
        {
            Id = Guid.NewGuid(),
            OrderId = order.Id,
            ProductId = product.Id,
            ProductName = product.Name,
            UnitPrice = product.Price,
            Quantity = quantity
        };
    }

    // Adedi artırma yöntemi
    public void IncreaseQuantity(int additionalQuantity)
    {
        if (additionalQuantity <= 0)
            throw new DomainException("Eklenecek adet pozitif olmalıdır.");

        Quantity += additionalQuantity;
    }
}
Domain/Enums/OrderStatus.cscsharp
namespace CleanArchitecture.Domain.Enums;

// Sipariş durumlarının numaralandırılması
public enum OrderStatus
{
    Pending = 0,      // Onay bekliyor
    Confirmed = 1,    // Onaylandı, gönderime hazır
    Shipped = 2,      // Gönderildi
    Delivered = 3,    // Teslim edildi
    Cancelled = 4     // İptal edildi
}
Value Objects

Money, Address veya Email gibi Value Object'ler kendi kimliği olmayan iş kavramlarını kapsüller. Eşitlikleri referansa değil, değerlere dayanır. Bu yaklaşım alanın ifade gücünü artırır.

Domain İçindeki Repository Arabirimleri

Repository arabirimleri Domain'de tanımlanır, ancak uygulamaları Infrastructure'da yer alır. Bu desen bağımlılık tersine çevirme prensibine uyar.

Domain/Interfaces/IOrderRepository.cscsharp
namespace CleanArchitecture.Domain.Interfaces;

// Order repository arabirimi
public interface IOrderRepository
{
    // Tanımlayıcıya göre getirme
    Task<Order?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);

    // Kalemler dahil getirme
    Task<Order?> GetByIdWithItemsAsync(Guid id, CancellationToken cancellationToken = default);

    // Müşteri e-postasına göre getirme
    Task<IEnumerable<Order>> GetByCustomerEmailAsync(
        string email,
        CancellationToken cancellationToken = default);

    // Yeni sipariş ekleme
    Task AddAsync(Order order, CancellationToken cancellationToken = default);

    // Mevcut siparişi güncelleme
    Task UpdateAsync(Order order, CancellationToken cancellationToken = default);

    // Sipariş silme
    Task DeleteAsync(Guid id, CancellationToken cancellationToken = default);
}
Domain/Interfaces/IUnitOfWork.cscsharp
namespace CleanArchitecture.Domain.Interfaces;

// Transaksiyonel yönetim için Unit of Work deseni
public interface IUnitOfWork : IDisposable
{
    // UoW üzerinden erişilen repository'ler
    IOrderRepository Orders { get; }
    IProductRepository Products { get; }

    // Tüm değişikliklerin atomik kaydı
    Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);

    // Açık transaksiyon yönetimi
    Task BeginTransactionAsync(CancellationToken cancellationToken = default);
    Task CommitTransactionAsync(CancellationToken cancellationToken = default);
    Task RollbackTransactionAsync(CancellationToken cancellationToken = default);
}

Unit of Work deseni birden fazla repository üzerindeki işlemleri tek bir transaksiyon içinde koordine eder.

.NET mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

Application Katmanı: Use Case Orkestrasyonu

Application katmanı uygulama mantığını (use case'ler), DTO'ları ve dış servis arabirimlerini içerir. Alan ile dış dünya arasındaki etkileşimleri orkestre eder.

Application/Orders/Commands/CreateOrderCommand.cscsharp
using MediatR;

namespace CleanArchitecture.Application.Orders.Commands;

// Sipariş oluşturma niyetini temsil eden command
public record CreateOrderCommand(
    string CustomerEmail,
    List<OrderItemDto> Items
) : IRequest<Guid>;

// Sipariş kalemleri için DTO
public record OrderItemDto(
    Guid ProductId,
    int Quantity
);
Application/Orders/Commands/CreateOrderCommandHandler.cscsharp
using MediatR;
using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Domain.Interfaces;

namespace CleanArchitecture.Application.Orders.Commands;

// Use case'i uygulayan handler
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>
{
    private readonly IUnitOfWork _unitOfWork;

    public CreateOrderCommandHandler(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    public async Task<Guid> Handle(
        CreateOrderCommand request,
        CancellationToken cancellationToken)
    {
        // Order varlığını factory yöntemiyle oluştur
        var order = Order.Create(request.CustomerEmail);

        // Kalemleri siparişe ekle
        foreach (var item in request.Items)
        {
            // Ürünü repository'den getir
            var product = await _unitOfWork.Products
                .GetByIdAsync(item.ProductId, cancellationToken);

            if (product == null)
                throw new NotFoundException($"Ürün {item.ProductId} bulunamadı.");

            // Varlığın iş yöntemini kullan
            order.AddItem(product, item.Quantity);
        }

        // Repository üzerinden kalıcı hale getir
        await _unitOfWork.Orders.AddAsync(order, cancellationToken);
        await _unitOfWork.SaveChangesAsync(cancellationToken);

        return order.Id;
    }
}

MediatR, handler'ları controller'lardan ayırmak için Mediator desenini uygular. Her command'ın işleme alınmasından sorumlu tek bir handler vardır.

Application/Orders/Queries/GetOrderByIdQuery.cscsharp
using MediatR;

namespace CleanArchitecture.Application.Orders.Queries;

// ID ile siparişi getirmek için query
public record GetOrderByIdQuery(Guid OrderId) : IRequest<OrderDto?>;

// Sipariş için yanıt DTO'su
public record OrderDto(
    Guid Id,
    string CustomerEmail,
    string Status,
    decimal TotalAmount,
    DateTime CreatedAt,
    DateTime? ShippedAt,
    List<OrderItemResponseDto> Items
);

// Yanıttaki kalemler için DTO
public record OrderItemResponseDto(
    Guid Id,
    string ProductName,
    decimal UnitPrice,
    int Quantity,
    decimal TotalPrice
);
Application/Orders/Queries/GetOrderByIdQueryHandler.cscsharp
using MediatR;
using CleanArchitecture.Domain.Interfaces;

namespace CleanArchitecture.Application.Orders.Queries;

public class GetOrderByIdQueryHandler : IRequestHandler<GetOrderByIdQuery, OrderDto?>
{
    private readonly IOrderRepository _orderRepository;

    public GetOrderByIdQueryHandler(IOrderRepository orderRepository)
    {
        _orderRepository = orderRepository;
    }

    public async Task<OrderDto?> Handle(
        GetOrderByIdQuery request,
        CancellationToken cancellationToken)
    {
        // Kalemler dahil getir
        var order = await _orderRepository
            .GetByIdWithItemsAsync(request.OrderId, cancellationToken);

        if (order == null)
            return null;

        // DTO'ya eşle (manuel projeksiyon)
        return new OrderDto(
            Id: order.Id,
            CustomerEmail: order.CustomerEmail,
            Status: order.Status.ToString(),
            TotalAmount: order.TotalAmount,
            CreatedAt: order.CreatedAt,
            ShippedAt: order.ShippedAt,
            Items: order.Items.Select(i => new OrderItemResponseDto(
                Id: i.Id,
                ProductName: i.ProductName,
                UnitPrice: i.UnitPrice,
                Quantity: i.Quantity,
                TotalPrice: i.TotalPrice
            )).ToList()
        );
    }
}

Commands/Queries ayrımı CQRS desenini (Command Query Responsibility Segregation) takip eder ve okuma ile yazma işlemlerini bağımsız olarak optimize eder.

FluentValidation ile Doğrulama

Command'ların doğrulanması Application katmanında, handler çalıştırılmadan önce gerçekleşir.

Application/Orders/Validators/CreateOrderCommandValidator.cscsharp
using FluentValidation;
using CleanArchitecture.Application.Orders.Commands;

namespace CleanArchitecture.Application.Orders.Validators;

public class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
{
    public CreateOrderCommandValidator()
    {
        // Zorunlu e-posta ve geçerli format
        RuleFor(x => x.CustomerEmail)
            .NotEmpty().WithMessage("Müşteri e-posta adresi zorunludur.")
            .EmailAddress().WithMessage("Geçersiz e-posta formatı.");

        // En az bir kalem zorunlu
        RuleFor(x => x.Items)
            .NotEmpty().WithMessage("Sipariş en az bir kalem içermelidir.");

        // Her kalemin doğrulanması
        RuleForEach(x => x.Items).ChildRules(item =>
        {
            item.RuleFor(i => i.ProductId)
                .NotEmpty().WithMessage("Ürün tanımlayıcısı zorunludur.");

            item.RuleFor(i => i.Quantity)
                .GreaterThan(0).WithMessage("Adet pozitif olmalıdır.")
                .LessThanOrEqualTo(100).WithMessage("Maksimum adet: 100.");
        });
    }
}
Application/Common/Behaviors/ValidationBehavior.cscsharp
using FluentValidation;
using MediatR;

namespace CleanArchitecture.Application.Common.Behaviors;

// Otomatik doğrulama için pipeline behavior
public class ValidationBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        // Doğrulayıcı yoksa devam et
        if (!_validators.Any())
            return await next();

        // Tüm doğrulayıcıları çalıştır
        var context = new ValidationContext<TRequest>(request);

        var validationResults = await Task.WhenAll(
            _validators.Select(v => v.ValidateAsync(context, cancellationToken)));

        // Hataları topla
        var failures = validationResults
            .SelectMany(r => r.Errors)
            .Where(f => f != null)
            .ToList();

        // Hata varsa istisna fırlat
        if (failures.Any())
            throw new ValidationException(failures);

        return await next();
    }
}

ValidationBehavior her handler'dan önce otomatik olarak çalışır ve yalnızca geçerli command'ların iş mantığına ulaşmasını sağlar.

Infrastructure Katmanı: Teknik Uygulama

Infrastructure katmanı, Domain ve Application'da tanımlanan arabirimlerin somut uygulamalarını sağlar.

Infrastructure/Persistence/AppDbContext.cscsharp
using Microsoft.EntityFrameworkCore;
using CleanArchitecture.Domain.Entities;

namespace CleanArchitecture.Infrastructure.Persistence;

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
    }

    public DbSet<Order> Orders => Set<Order>();
    public DbSet<OrderItem> OrderItems => Set<OrderItem>();
    public DbSet<Product> Products => Set<Product>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Assembly'deki yapılandırmaları uygula
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
    }
}
Infrastructure/Persistence/Configurations/OrderConfiguration.cscsharp
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using CleanArchitecture.Domain.Entities;

namespace CleanArchitecture.Infrastructure.Persistence.Configurations;

public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> builder)
    {
        // Tablo ve birincil anahtar
        builder.ToTable("Orders");
        builder.HasKey(o => o.Id);

        // Özellikler
        builder.Property(o => o.CustomerEmail)
            .IsRequired()
            .HasMaxLength(256);

        builder.Property(o => o.Status)
            .IsRequired()
            .HasConversion<string>();  // Okunabilir string olarak sakla

        // OrderItems ile ilişki
        builder.HasMany(o => o.Items)
            .WithOne()
            .HasForeignKey(i => i.OrderId)
            .OnDelete(DeleteBehavior.Cascade);

        // E-posta araması için indeks
        builder.HasIndex(o => o.CustomerEmail);

        // Özel _items alanına erişim
        builder.Navigation(o => o.Items)
            .UsePropertyAccessMode(PropertyAccessMode.Field);
    }
}
Infrastructure/Repositories/OrderRepository.cscsharp
using Microsoft.EntityFrameworkCore;
using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Domain.Interfaces;
using CleanArchitecture.Infrastructure.Persistence;

namespace CleanArchitecture.Infrastructure.Repositories;

public class OrderRepository : IOrderRepository
{
    private readonly AppDbContext _context;

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

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

    public async Task<Order?> GetByIdWithItemsAsync(
        Guid id,
        CancellationToken cancellationToken = default)
    {
        // Kalemleri eager loading ile getir
        return await _context.Orders
            .Include(o => o.Items)
            .FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
    }

    public async Task<IEnumerable<Order>> GetByCustomerEmailAsync(
        string email,
        CancellationToken cancellationToken = default)
    {
        return await _context.Orders
            .Include(o => o.Items)
            .Where(o => o.CustomerEmail == email)
            .OrderByDescending(o => o.CreatedAt)
            .ToListAsync(cancellationToken);
    }

    public async Task AddAsync(
        Order order,
        CancellationToken cancellationToken = default)
    {
        await _context.Orders.AddAsync(order, cancellationToken);
    }

    public Task UpdateAsync(
        Order order,
        CancellationToken cancellationToken = default)
    {
        // EF Core değişiklikleri otomatik olarak izler
        _context.Orders.Update(order);
        return Task.CompletedTask;
    }

    public async Task DeleteAsync(
        Guid id,
        CancellationToken cancellationToken = default)
    {
        await _context.Orders
            .Where(o => o.Id == id)
            .ExecuteDeleteAsync(cancellationToken);
    }
}
Leaky Abstractions'a Dikkat

Repository'ler IQueryable'ı doğrudan dışarı vermemelidir, aksi takdirde üst katmanlarda EF Core'a bir bağımlılık oluşur. Net parametrelere sahip özel yöntemler tercih edilmelidir.

Unit of Work Uygulaması

Infrastructure/Persistence/UnitOfWork.cscsharp
using Microsoft.EntityFrameworkCore.Storage;
using CleanArchitecture.Domain.Interfaces;
using CleanArchitecture.Infrastructure.Repositories;

namespace CleanArchitecture.Infrastructure.Persistence;

public class UnitOfWork : IUnitOfWork
{
    private readonly AppDbContext _context;
    private IDbContextTransaction? _transaction;

    // Repository'lerin lazy yüklenmesi
    private IOrderRepository? _orderRepository;
    private IProductRepository? _productRepository;

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

    // İhtiyaç anında oluşturma (lazy initialization)
    public IOrderRepository Orders =>
        _orderRepository ??= new OrderRepository(_context);

    public IProductRepository Products =>
        _productRepository ??= new ProductRepository(_context);

    public async Task<int> SaveChangesAsync(
        CancellationToken cancellationToken = default)
    {
        return await _context.SaveChangesAsync(cancellationToken);
    }

    public async Task BeginTransactionAsync(
        CancellationToken cancellationToken = default)
    {
        _transaction = await _context.Database
            .BeginTransactionAsync(cancellationToken);
    }

    public async Task CommitTransactionAsync(
        CancellationToken cancellationToken = default)
    {
        if (_transaction == null)
            throw new InvalidOperationException("Aktif transaksiyon yok.");

        await _transaction.CommitAsync(cancellationToken);
        await _transaction.DisposeAsync();
        _transaction = null;
    }

    public async Task RollbackTransactionAsync(
        CancellationToken cancellationToken = default)
    {
        if (_transaction == null)
            throw new InvalidOperationException("Aktif transaksiyon yok.");

        await _transaction.RollbackAsync(cancellationToken);
        await _transaction.DisposeAsync();
        _transaction = null;
    }

    public void Dispose()
    {
        _transaction?.Dispose();
        _context.Dispose();
    }
}

Dependency Injection Yapılandırması

Servislerin kaydı her katmanda extension method'lar aracılığıyla yapılır ve ardından Program.cs içinde orkestre edilir.

Infrastructure/DependencyInjection.cscsharp
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using CleanArchitecture.Domain.Interfaces;
using CleanArchitecture.Infrastructure.Persistence;
using CleanArchitecture.Infrastructure.Repositories;

namespace CleanArchitecture.Infrastructure;

public static class DependencyInjection
{
    public static IServiceCollection AddInfrastructure(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        // Entity Framework yapılandırması
        services.AddDbContext<AppDbContext>(options =>
            options.UseSqlServer(
                configuration.GetConnectionString("DefaultConnection"),
                b => b.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName)));

        // Unit of Work kaydı (Scoped)
        services.AddScoped<IUnitOfWork, UnitOfWork>();

        // Gerekirse ayrı ayrı repository'ler
        services.AddScoped<IOrderRepository, OrderRepository>();
        services.AddScoped<IProductRepository, ProductRepository>();

        return services;
    }
}
Application/DependencyInjection.cscsharp
using FluentValidation;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using CleanArchitecture.Application.Common.Behaviors;

namespace CleanArchitecture.Application;

public static class DependencyInjection
{
    public static IServiceCollection AddApplication(this IServiceCollection services)
    {
        var assembly = typeof(DependencyInjection).Assembly;

        // Handler'larıyla birlikte MediatR'ı kaydet
        services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(assembly));

        // FluentValidation doğrulayıcılarını kaydet
        services.AddValidatorsFromAssembly(assembly);

        // Pipeline behavior'lar (çalışma sırası önemlidir)
        services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));

        return services;
    }
}
Api/Program.cscsharp
using CleanArchitecture.Application;
using CleanArchitecture.Infrastructure;

var builder = WebApplication.CreateBuilder(args);

// Katmanları kaydet
builder.Services.AddApplication();
builder.Services.AddInfrastructure(builder.Configuration);

// API yapılandırması
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

Presentation Katmanı: API Controller'ları

Controller'lar işi MediatR'a delege eden sade adaptörlerdir.

Api/Controllers/OrdersController.cscsharp
using MediatR;
using Microsoft.AspNetCore.Mvc;
using CleanArchitecture.Application.Orders.Commands;
using CleanArchitecture.Application.Orders.Queries;

namespace CleanArchitecture.Api.Controllers;

[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly IMediator _mediator;

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

    [HttpPost]
    [ProducesResponseType(typeof(Guid), StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> Create(
        [FromBody] CreateOrderCommand command,
        CancellationToken cancellationToken)
    {
        // Tam delegasyon MediatR'a
        var orderId = await _mediator.Send(command, cancellationToken);

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

    [HttpGet("{id:guid}")]
    [ProducesResponseType(typeof(OrderDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetById(
        Guid id,
        CancellationToken cancellationToken)
    {
        var order = await _mediator.Send(
            new GetOrderByIdQuery(id),
            cancellationToken);

        if (order == null)
            return NotFound();

        return Ok(order);
    }

    [HttpPost("{id:guid}/confirm")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Confirm(
        Guid id,
        CancellationToken cancellationToken)
    {
        await _mediator.Send(new ConfirmOrderCommand(id), cancellationToken);
        return NoContent();
    }

    [HttpPost("{id:guid}/ship")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Ship(
        Guid id,
        CancellationToken cancellationToken)
    {
        await _mediator.Send(new ShipOrderCommand(id), cancellationToken);
        return NoContent();
    }
}

Controller'lar ince kalır ve iş mantığı içermez, yalnızca HTTP eşlemesi yaparlar.

Application Katmanının Birim Testleri

Clean Architecture, bağımlılıkların izole edilmesi sayesinde birim testleri kolaylaştırır.

Tests/Application/CreateOrderCommandHandlerTests.cscsharp
using Moq;
using Xunit;
using CleanArchitecture.Application.Orders.Commands;
using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Domain.Interfaces;

namespace CleanArchitecture.Tests.Application;

public class CreateOrderCommandHandlerTests
{
    private readonly Mock<IUnitOfWork> _unitOfWorkMock;
    private readonly CreateOrderCommandHandler _handler;

    public CreateOrderCommandHandlerTests()
    {
        _unitOfWorkMock = new Mock<IUnitOfWork>();
        _handler = new CreateOrderCommandHandler(_unitOfWorkMock.Object);
    }

    [Fact]
    public async Task Handle_ValidCommand_CreatesOrderAndReturnsId()
    {
        // Arrange
        var product = new Product { Id = Guid.NewGuid(), Name = "Test", Price = 100 };

        _unitOfWorkMock.Setup(x => x.Products.GetByIdAsync(
            It.IsAny<Guid>(),
            It.IsAny<CancellationToken>()))
            .ReturnsAsync(product);

        _unitOfWorkMock.Setup(x => x.Orders.AddAsync(
            It.IsAny<Order>(),
            It.IsAny<CancellationToken>()))
            .Returns(Task.CompletedTask);

        _unitOfWorkMock.Setup(x => x.SaveChangesAsync(It.IsAny<CancellationToken>()))
            .ReturnsAsync(1);

        var command = new CreateOrderCommand(
            CustomerEmail: "test@example.com",
            Items: new List<OrderItemDto>
            {
                new(ProductId: product.Id, Quantity: 2)
            });

        // Act
        var result = await _handler.Handle(command, CancellationToken.None);

        // Assert
        Assert.NotEqual(Guid.Empty, result);
        _unitOfWorkMock.Verify(x => x.Orders.AddAsync(
            It.Is<Order>(o => o.CustomerEmail == "test@example.com"),
            It.IsAny<CancellationToken>()), Times.Once);
        _unitOfWorkMock.Verify(x => x.SaveChangesAsync(
            It.IsAny<CancellationToken>()), Times.Once);
    }

    [Fact]
    public async Task Handle_ProductNotFound_ThrowsNotFoundException()
    {
        // Arrange
        _unitOfWorkMock.Setup(x => x.Products.GetByIdAsync(
            It.IsAny<Guid>(),
            It.IsAny<CancellationToken>()))
            .ReturnsAsync((Product?)null);

        var command = new CreateOrderCommand(
            CustomerEmail: "test@example.com",
            Items: new List<OrderItemDto>
            {
                new(ProductId: Guid.NewGuid(), Quantity: 1)
            });

        // Act & Assert
        await Assert.ThrowsAsync<NotFoundException>(
            () => _handler.Handle(command, CancellationToken.None));
    }
}

Moq mock'ları handler'ın bağımlılıklarından izole edilmesini sağlayarak yalnızca orkestrasyon mantığının test edilmesine olanak tanır.

Sonuç

.NET ile Clean Architecture, kurumsal uygulamalar için sağlam bir yapı sunar. Domain, Application, Infrastructure ve Presentation katmanları arasındaki sorumlulukların sıkı ayrımı, uzun vadede sürdürülebilir ve test edilebilir bir kod garanti eder.

Clean Architecture .NET Kontrol Listesi

  • ✅ Dış bağımlılığı olmayan, izole Domain
  • ✅ Kapsüllenmiş iş mantığına sahip varlıklar
  • ✅ Domain içinde repository arabirimleri
  • ✅ Commands/Queries (CQRS) ile use case'ler
  • ✅ FluentValidation ve Pipeline Behavior'lar ile doğrulama
  • ✅ Domain arabirimlerini uygulayan Infrastructure
  • ✅ MediatR'a delege eden ince controller'lar
  • ✅ Mock'larla izole birim testleri

Pratik yapmaya başla!

Mülakat simülatörleri ve teknik testlerle bilgini test et.

Bu mimariye yapılan ilk yatırım, orta ve büyük projelerde hızla geri dönüş sağlar; teknik gereksinim değişiklikleri (veritabanı geçişi, framework değişikliği) yalnızca dış katmanları etkiler ve iş çekirdeği bozulmadan kalır.

Etiketler

#dotnet
#clean architecture
#csharp
#aspnet core
#design patterns

Paylaş

İlgili makaleler