2026'da .NET Clean Architecture: CQRS, MediatR ve Mülakat Soruları

CQRS ve MediatR ile .NET Clean Architecture rehberi. Kod örnekleri, tasarım kalıpları ve .NET geliştiricileri için mülakat soruları.

2026'da .NET Clean Architecture: CQRS, MediatR ve Mülakat Soruları

.NET'te Clean Architecture, iş mantığını altyapı katmanlarından ayırarak uygulamaların test edilmesini, bakımını ve ölçeklenmesini kolaylaştırır. CQRS (Command Query Responsibility Segregation) ve MediatR ile birleştirildiğinde, bu mimari kalıp kurumsal uygulamalar oluşturmak için yapılandırılmış bir yaklaşım sunar ve mülakatlar sırasında sıkça değerlendirilen konular arasında yer alır.

Mülakat İçin Kritik Bilgi

Clean Architecture soruları, senior .NET mülakatlarının %70'inde karşımıza çıkar. Mülakatçılar adayların bağımlılık kuralını açıklamasını bekler: bağımlılıklar içe doğru işaret eder ve domain katmanının harici framework'lere hiçbir bağımlılığı yoktur.

Clean Architecture Katmanları ve Bağımlılık Kuralı

ASP.NET Core 10 için Ardalis Clean Architecture şablonu, kodu dört eş merkezli katmana organize eder. Her katmanın belirli sorumlulukları ve katı bağımlılık kuralları vardır.

csharp
// Core/Domain Layer - No external dependencies
// src/Core/Domain/Entities/Order.cs
namespace Core.Domain.Entities;

public class Order
{
    public Guid Id { get; private set; }
    public string CustomerEmail { get; private set; }
    public List<OrderLine> Lines { get; private set; } = new();
    public OrderStatus Status { get; private set; }
    public DateTime CreatedAt { get; private set; }

    // Domain logic encapsulated in the entity
    public void AddLine(Product product, int quantity)
    {
        if (Status != OrderStatus.Draft)
            throw new InvalidOperationException("Cannot modify a submitted order");

        var existingLine = Lines.FirstOrDefault(l => l.ProductId == product.Id);
        if (existingLine is not null)
        {
            existingLine.IncreaseQuantity(quantity);
            return;
        }

        Lines.Add(new OrderLine(product.Id, product.Price, quantity));
    }

    public void Submit()
    {
        if (!Lines.Any())
            throw new InvalidOperationException("Cannot submit an empty order");

        Status = OrderStatus.Submitted;
    }
}

Domain katmanı entity'ler, değer nesneleri ve domain servislerini içerir. Veritabanları, HTTP protokolü veya harici framework'ler hakkında hiçbir bilgiye sahip değildir.

CQRS Kalıbı: Okuma ve Yazma İşlemlerinin Ayrılması

CQRS, işlemleri Command'lar (durum değiştiren yazma işlemleri) ve Query'ler (veri döndüren okuma işlemleri) olarak ikiye ayırır. Bu ayrım, her yolun bağımsız olarak ölçeklenmesine ve optimize edilmesine olanak tanır.

csharp
// UseCases Layer - Commands and Queries
// src/UseCases/Orders/Commands/CreateOrder/CreateOrderCommand.cs
namespace UseCases.Orders.Commands.CreateOrder;

public record CreateOrderCommand(
    string CustomerEmail,
    List<OrderLineDto> Lines
) : IRequest<Result<Guid>>;

public record OrderLineDto(Guid ProductId, int Quantity);

Command'lar minimum veri döndürür, genellikle sadece başarı göstergesi veya yeni oluşturulan ID. Query'ler ise tüketici için optimize edilmiş DTO'lar döndürür.

src/UseCases/Orders/Queries/GetOrderById/GetOrderByIdQuery.cscsharp
namespace UseCases.Orders.Queries.GetOrderById;

public record GetOrderByIdQuery(Guid OrderId) : IRequest<Result<OrderDetailsDto>>;

public record OrderDetailsDto(
    Guid Id,
    string CustomerEmail,
    string Status,
    decimal TotalAmount,
    List<OrderLineDetailsDto> Lines
);

MediatR 14: Pipeline Behaviors ve Handler Uygulaması

MediatR 14.2, CQRS için mesajlaşma altyapısını sağlar. Her command veya query tam olarak bir handler'a sahiptir, bu da tek sorumluluk ilkesini zorunlu kılar.

src/UseCases/Orders/Commands/CreateOrder/CreateOrderHandler.cscsharp
namespace UseCases.Orders.Commands.CreateOrder;

public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Result<Guid>>
{
    private readonly IOrderRepository _orderRepository;
    private readonly IProductRepository _productRepository;
    private readonly IUnitOfWork _unitOfWork;

    public CreateOrderHandler(
        IOrderRepository orderRepository,
        IProductRepository productRepository,
        IUnitOfWork unitOfWork)
    {
        _orderRepository = orderRepository;
        _productRepository = productRepository;
        _unitOfWork = unitOfWork;
    }

    public async Task<Result<Guid>> Handle(
        CreateOrderCommand request,
        CancellationToken cancellationToken)
    {
        // Load products to validate and get prices
        var productIds = request.Lines.Select(l => l.ProductId).ToList();
        var products = await _productRepository
            .GetByIdsAsync(productIds, cancellationToken);

        if (products.Count != productIds.Count)
            return Result.NotFound("One or more products not found");

        // Create order using domain logic
        var order = new Order(request.CustomerEmail);

        foreach (var line in request.Lines)
        {
            var product = products.First(p => p.Id == line.ProductId);
            order.AddLine(product, line.Quantity);
        }

        await _orderRepository.AddAsync(order, cancellationToken);
        await _unitOfWork.SaveChangesAsync(cancellationToken);

        return Result.Success(order.Id);
    }
}

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

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

Kesişen Endişeler İçin Pipeline Behaviors

MediatR'daki pipeline behavior'lar, handler'ları ek kodla kirletmeden loglama, validasyon, önbellek ve transaction yönetimini ele alır.

src/UseCases/Common/Behaviors/ValidationBehavior.cscsharp
namespace UseCases.Common.Behaviors;

public class ValidationBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    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)
    {
        if (!_validators.Any())
            return await next();

        var context = new ValidationContext<TRequest>(request);

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

        var failures = validationResults
            .SelectMany(r => r.Errors)
            .Where(f => f is not null)
            .ToList();

        if (failures.Any())
            throw new ValidationException(failures);

        return await next();
    }
}

Behavior'lar çalıştırılmaları gereken sırada kaydedilir. Validasyon genellikle ilk sırada çalışır, ardından loglama ve son olarak transaction yönetimi gelir.

src/Web/Program.cscsharp
builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(typeof(CreateOrderCommand).Assembly);
    cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
    cfg.AddOpenBehavior(typeof(LoggingBehavior<,>));
    cfg.AddOpenBehavior(typeof(TransactionBehavior<,>));
});

Altyapı Katmanı: Repository Uygulaması

Altyapı katmanı, Core katmanında tanımlanan arayüzleri uygular. Entity Framework Core repository'leri, domain işlemlerini veritabanı çağrılarına dönüştürür.

src/Infrastructure/Data/Repositories/OrderRepository.cscsharp
namespace Infrastructure.Data.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
            .Include(o => o.Lines)
            .FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
    }

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

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

Mülakat Soruları: Senior Developer'ların Bilmesi Gerekenler

Senior .NET pozisyonları için teknik mülakatlar sıklıkla Clean Architecture soruları içerir. Mülakatçıların değerlendirdiği kalıplar şunlardır:

S: Domain katmanı neden harici framework'lere bağımlı değildir?

Domain katmanı, altyapı değişikliklerinden bağımsız olarak sabit kalması gereken iş kurallarını içerir. Domain Entity Framework'e bağımlıysa, Dapper'a veya farklı bir veritabanına geçmek iş mantığını değiştirmeyi gerektirir. Bağımlılık kuralı, altyapının ana davranışı etkilemeden değiştirilebilecek bir detay olmasını sağlar.

S: CQRS ne zaman gereksiz karmaşıklık yaratır?

CQRS, basit CRUD uygulamalarının ihtiyaç duymadığı karmaşıklık ekler. Okuma ve yazma modelleri neredeyse aynıysa, ayrı ölçekleme ihtiyacı yoksa ve ekip küçükse, daha basit bir mimari uygundur. CQRS, okuma modelleri yazma modellerinden önemli ölçüde farklı olduğunda, event sourcing gerektiğinde veya okuma ve yazma yükleri bağımsız ölçekleme gerektirdiğinde parlak performans gösterir.

S: Birden fazla aggregate arasındaki transaction'lar nasıl yönetilir?

csharp
// Transaction behavior wraps the entire handler execution
public class TransactionBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IUnitOfWork _unitOfWork;

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

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        // Commands that modify multiple aggregates use explicit transactions
        if (request is not ICommand)
            return await next();

        await using var transaction = await _unitOfWork
            .BeginTransactionAsync(cancellationToken);

        try
        {
            var response = await next();
            await transaction.CommitAsync(cancellationToken);
            return response;
        }
        catch
        {
            await transaction.RollbackAsync(cancellationToken);
            throw;
        }
    }
}

Birden fazla aggregate'i kapsayan işlemler için mümkün olduğunda domain event'ler ile eventual consistency kullanılmalıdır. Aggregate'ler arası senkron transaction'lar potansiyel bir tasarım sorununa işaret eder: aggregate'ler birbirine ait olabilir.

Ayrı Query Modelleri ile Okuma Tarafı Optimizasyonu

Performans önemli olduğunda query'ler domain katmanını tamamen atlar. Dapper veya ham SQL ile doğrudan veritabanı erişimi, tam entity graflarını somutlaştırma yükünü önler.

src/Infrastructure/Queries/GetOrderByIdQueryHandler.cscsharp
namespace Infrastructure.Queries;

public class GetOrderByIdQueryHandler
    : IRequestHandler<GetOrderByIdQuery, Result<OrderDetailsDto>>
{
    private readonly IDbConnection _connection;

    public GetOrderByIdQueryHandler(IDbConnection connection)
    {
        _connection = connection;
    }

    public async Task<Result<OrderDetailsDto>> Handle(
        GetOrderByIdQuery request,
        CancellationToken cancellationToken)
    {
        const string sql = """
            SELECT o.Id, o.CustomerEmail, o.Status, o.CreatedAt,
                   SUM(ol.UnitPrice * ol.Quantity) AS TotalAmount
            FROM Orders o
            LEFT JOIN OrderLines ol ON ol.OrderId = o.Id
            WHERE o.Id = @OrderId
            GROUP BY o.Id, o.CustomerEmail, o.Status, o.CreatedAt
            """;

        var order = await _connection.QueryFirstOrDefaultAsync<OrderDetailsDto>(
            new CommandDefinition(sql, new { request.OrderId }, cancellationToken: cancellationToken));

        return order is null
            ? Result.NotFound($"Order {request.OrderId} not found")
            : Result.Success(order);
    }
}

Pratik yapmaya başla!

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

Clean Architecture Mülakat Hazırlığı: Temel Çıkarımlar

  • Bağımlılık kuralı, kaynak kod bağımlılıklarının yalnızca içe doğru işaret edebileceğini belirtir. Domain katmanı veritabanları, framework'ler veya sunum mekanizmaları hakkında bilgi sahibi değildir.
  • CQRS, command'ları (durum değişiklikleri) query'lerden (veri alımı) ayırır. Command'lar domain modeli ve validasyon üzerinden geçer. Query'ler performans için domain entity'lerini atlayabilir.
  • MediatR 14 istekleri handler'lara yönlendirir. Pipeline behavior'lar validasyon, loglama ve transaction'lar gibi kesişen endişeleri kompoze edilebilir bir şekilde ele alır.
  • Repository arayüzleri Core katmanında bulunur. Uygulamalar altyapıda bulunur. Bu ters çevirme, iş mantığına dokunmadan EF Core'u Dapper ile değiştirmeye olanak tanır.
  • Mülakat yanıtları trade-off'ları içermelidir. Clean Architecture başlangıçta karmaşıklık ekler ancak birden fazla geliştirici ile büyük, uzun ömürlü uygulamalarda karşılığını verir.
  • SharpSkill'deki ASP.NET Core Clean Architecture modülü, Specification, Result nesneleri ve Domain Events dahil ek kalıpları kapsar.
Günün meydan okuması

.NET kodundaki hatayı bulabilir misin?

Gerçek bir kod parçası, gizli bir hata, günde bir deneme. Denemek için hesap gerekmez.

Anthony Fillion-Maillet

Yazan:

Anthony Fillion-Maillet

SharpSkill kurucusu

10 yılı aşkın süredir fullstack geliştirici. SharpSkill’i yönetiyor ve burada yayımlanan her şeyden sorumlu.

15 Eylül 2026 tarihinde güncellendi

Paylaş

İlgili makaleler