CQRS ve MediatR ile Esnek Kod

Komut (write) ve sorgu (read) modellerini ayırarak ölçeklenebilirliği artıran CQRS deseni ve .NET'te MediatR kütüphanesi ile uygulanması anlatılır.

CQRS ve MediatR ile Esnek Kod

CQRS ve MediatR: Read/Write Sorumluluklarını Ayırarak Esnek Kod Bazı Oluşturmak

CQRS (Command Query Responsibility Segregation), yani Komut-Sorgu Sorumluluk Ayrışımı, aslında çok eski bir prensiptir: Bir metot ya sorgu (okuma) yapar, ya da komut (yazma) yapar; ikisini aynı anda yapmaz. Ancak günümüzde CQRS, bu kuralı mimari boyuta taşıyarak Okuma (Read) ve Yazma (Write) modellerini fiziksel olarak ayırmayı önerir. Bu ayrım, özellikle karmaşık iş mantığı ve yüksek ölçeklenebilirlik gereken sistemlerde hayat kurtarır. .NET ekosisteminde bu deseni uygulamanın en yaygın yolu ise MediatR kütüphanesidir.


1. CQRS Nedir ve Neden Kullanılır?

Klasik N-Tier (Katmanlı) Mimari:

  • Bir OrderService sınıfı içinde GetOrderById (sorgu) ve CreateOrder (komut) birlikte bulunur.

  • Aynı Order entity'si (ve aynı veritabanı tablosu) hem okuma hem yazma için kullanılır.

  • Sorun: İş mantığı büyüdükçe, bu service sınıfı devleşir. Okuma odaklı optimizasyonlar (ör. cache, özel index) yazmayı (insert/update) yavaşlatabilir. Farklı ekipler aynı entity üzerinde çakışır.

CQRS ile:

  • Komut (Command): Sistemi değiştirir (Ekle, Güncelle, Sil). Yan etki (side-effect) vardır. Geriye bir şey döndürmez (veya sadece ID döndürür). İş kuralları (validasyon, domain logic) burada uygulanır.

  • Sorgu (Query): Sistemi okur, asla değiştirmez. Hiçbir iş kuralı içermez (sadece veriyi getirir). Performans için özel okuma modelleri (Read Models) ve veritabanı görünümleri (view) kullanılabilir.

Fiziksel Ayrım (Full CQRS): Komut ve sorgu için ayrı veritabanları (Command DB - Write, Query DB - Read) kullanılır. Eventual Consistency (Nihai Tutarlılık) ile senkronize edilir. Bu, mikroservislerde çok yaygındır.


2. MediatR: CQRS'yi .NET'te Uygulamanın En Kolay Yolu

MediatR, uygulama içinde aracı (mediator) desenini implemente eden bir kütüphanedir. Sizin doğrudan servis çağırmak yerine, istek (request) göndermenizi ve bu isteği karşılayan handler (işleyici)'ın devreye girmesini sağlar. Bu sayede Controller ile Business Logic arasında gevşek bağlantı (loose coupling) oluşur.

Kurulum (NuGet): MediatR ve MediatR.Extensions.Microsoft.DependencyInjection


3. Adım Adım CQRS + MediatR Uygulaması

Adım 1: Sorgu (Query) - Okuma İşlemi

csharp

// Query (Sorgu) sınıfı - Ne istediğini belirtir
public class GetOrderQuery : IRequest<OrderDto>
{
    public int OrderId { get; set; }
}

// Query Handler (Sorgu İşleyici) - Veriyi nasıl getireceğini bilir
public class GetOrderQueryHandler : IRequestHandler<GetOrderQuery, OrderDto>
{
    private readonly AppDbContext _context;
    private readonly IMapper _mapper;

    public GetOrderQueryHandler(AppDbContext context, IMapper mapper)
    {
        _context = context;
        _mapper = mapper;
    }

    public async Task<OrderDto> Handle(GetOrderQuery request, CancellationToken cancellationToken)
    {
        var order = await _context.Orders
            .AsNoTracking() // Sadece okuma, takip etme!
            .FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken);

        if (order is null)
            throw new NotFoundException($"Order {request.OrderId} bulunamadı.");

        return _mapper.Map<OrderDto>(order);
    }
}

Adım 2: Komut (Command) - Yazma İşlemi

csharp

// Command (Komut) sınıfı - Ne yapılacağını belirtir (Emir)
public class CreateOrderCommand : IRequest<int> // Geriye yeni ID dönsün
{
    public string CustomerName { get; set; }
    public List<OrderItemDto> Items { get; set; }
}

// Command Handler (Komut İşleyici) - İş kurallarını uygular
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, int>
{
    private readonly AppDbContext _context;
    private readonly ILogger<CreateOrderCommandHandler> _logger;

    public CreateOrderCommandHandler(AppDbContext context, ILogger<CreateOrderCommandHandler> logger)
    {
        _context = context;
        _logger = logger;
    }

    public async Task<int> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
    {
        // 1. Validasyon (FluentValidation ile yapılabilir)
        if (string.IsNullOrWhiteSpace(request.CustomerName))
            throw new ValidationException("Müşteri adı boş olamaz.");

        // 2. Domain entity oluştur
        var order = new Order
        {
            CustomerName = request.CustomerName,
            CreatedAt = DateTime.UtcNow,
            Items = request.Items.Select(i => new OrderItem
            {
                ProductName = i.ProductName,
                Quantity = i.Quantity,
                UnitPrice = i.UnitPrice
            }).ToList()
        };

        // 3. Veritabanına kaydet
        _context.Orders.Add(order);
        await _context.SaveChangesAsync(cancellationToken);

        // 4. Event fırlat (opsiyonel)
        // await _mediator.Publish(new OrderCreatedEvent(order.Id), cancellationToken);

        _logger.LogInformation("Yeni sipariş oluşturuldu. ID: {OrderId}", order.Id);

        return order.Id;
    }
}

Adım 3: Controller'dan Kullanım (Süper Temiz)

csharp

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

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

    [HttpGet("{id}")]
    public async Task<ActionResult<OrderDto>> GetOrder(int id)
    {
        var query = new GetOrderQuery { OrderId = id };
        var result = await _mediator.Send(query);
        return Ok(result);
    }

    [HttpPost]
    public async Task<ActionResult<int>> CreateOrder(CreateOrderCommand command)
    {
        var orderId = await _mediator.Send(command);
        return CreatedAtAction(nameof(GetOrder), new { id = orderId }, orderId);
    }
}

Gördüğünüz gibi: Controller'da hiçbir iş mantığı yok. Sadece gelen isteği (query/command) MediatR'a gönderiyor. Tüm işler handler'larda toplanıyor.


4. Pipeline Behaviors (Aspect Oriented Programming - AOP)

MediatR'nin en güçlü özelliklerinden biri Pipeline Behavior'lardır. Her query/command için ortak yapılacak işlemleri (loglama, validasyon, performans ölçümü, transaction yönetimi) tek bir yerde toplayabilirsiniz.

Örnek: Validasyon Pipeline (FluentValidation ile)

csharp

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())
        {
            var context = new ValidationContext<TRequest>(request);
            var results = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
            var failures = results.SelectMany(r => r.Errors).Where(f => f != null).ToList();

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

        return await next(); // Validasyon geçtiyse handler'ı çalıştır
    }
}

Kayıt:

csharp

builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));

Örnek: Transaction Pipeline (Unit of Work)

csharp

public class TransactionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly AppDbContext _context;

    public TransactionBehavior(AppDbContext context) => _context = context;

    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
    {
        // Sadece Command'ler için transaction uygula (IRequest<TResponse> yerine ICommand<TResponse> kontrol edilebilir)
        if (request is not ICommand) // Özel bir marker interface olabilir
            return await next();

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

5. CQRS Her Yere Uygun mu? (Ne Zaman Kullanmalı?)

Uygun Olduğu Senaryolar Uygun Olmadığı Senaryolar
Karmaşık iş mantığı (Domain Driven Design) Basit CRUD uygulamaları (ör. Admin paneli)
Okuma ve yazma trafiğinin çok farklı olduğu sistemler (ör. sosyal medya: çok okuma, az yazma) Küçük ekipler veya prototipler
Event Sourcing (Olay Kaynağı) ile birlikte kullanılacaksa Performansın kritik olduğu ve ek katman (MediatR) maliyetinin kabul edilemediği ultra düşük gecikme sistemleri (ör. high-frequency trading)
Farklı ekiplerin okuma ve yazma modellerini ayrı ayrı geliştireceği büyük projeler  

6. Sık Yapılan Hatalar ve Çözümleri

Hata Çözüm
Komut içinde sorgu yapmak (Command returns data) Komut sadece ID veya Unit (void) dönsün. Veri döndürecekse bu bir sorgudur (Query).
Handler içinde başka Handler çağırmak Handler'lar birbirini çağırmasın. Ortak iş mantığı varsa ayrı bir servis (Domain Service) oluştur.
Her şeyi MediatR ile yapmak (Aşırı Kullanım) Basit veri getirme işlemleri için doğrudan Repository çağırmak daha hızlıdır. MediatR'ı sadece iş mantığı yoğun olan yerde kullanın.
CQRS = MediatR zannetmek MediatR bir araçtır (tool), CQRS bir mimari desendir. MediatR olmadan da CQRS uygulanabilir (örn. doğrudan service sınıflarıyla).
Full CQRS (Eventual Consistency) zorunluluğu Çoğu proje için aynı veritabanında farklı modeller (table/view) kullanmak yeterlidir. Full CQRS (ayrı DB) sadece gerçekten ihtiyaç duyulduğunda uygulanır.

Sonuç:

CQRS ve MediatR, beraberinde temiz, test edilebilir ve sürdürülebilir bir kod tabanı getirir. Her bir işlem (command/query) tek bir sorumluluğa (Single Responsibility) sahiptir. Pipeline Behaviors ile cross-cutting concern'ler (validasyon, log, transaction) tek bir yerde toplanır.

Ancak unutmayın: Bu desen, basit uygulamalarda aşırı mühendislik (over-engineering) olarak değerlendirilebilir. Projenizin büyüklüğünü ve ekip tecrübesini göz önünde bulundurarak karar verin. Eğer karar verdiyseniz, MediatR ile CQRS, .NET dünyasında bu mimariyi uygulamanın en olgun ve en çok tercih edilen yoludur.

Tüm yazılar