TB
← Tüm yazılar

Mediator deseni pratikte

Mediator pattern'in CQRS ile birlikte ASP.NET Core uygulamalarinda nasil konumlandirildigini, pipeline davranislari ve test stratejileriyle birlikte ele aliyoruz.

Mediator deseni, bir istek gonderen ile istegi isleyen arasindaki dogrudan bagimliligi ortadan kaldirarak, uygulama katmaninda tek bir giriş noktasi uzerinden is akisini yonetmeyi hedefler. .NET ekosisteminde MediatR kutuphanesi bu desenin en yaygın uygulamasidir ve CQRS (Command Query Responsibility Segregation) ile birlikte kullanıldığında güçlü bir mimari omurga oluşturur. Pratikte başarı, deseni doğru sinirlarla uygulamaktan gecer: her handler tek sorumluluk tasimali, pipeline davranislari olculu kullanılmalı ve domain mantigi handler icinde degil alt katmanlarda kalmalidir.

Mediator ve CQRS iliskisi

CQRS, okuma ve yazma modellerini ayirir. Mediator ise bu komut ve sorgularin HTTP katmanindan application katmanina iletilmesini sağlar. Tipik akis su sekildedir:

  1. Controller veya Minimal API endpoint istegi alir.
  2. DTO, MediatR uzerinden Command veya Query nesnesine donusturulur.
  3. Ilişkili Handler çalışır ve sonuç doner.
  4. Sonuç HTTP yanitina map edilir.

Bu yapı, endpoint'lerin ince kalmasini sağlar. Örneğin siparis oluşturma endpoint'i sadece await mediator.Send(new CreateOrderCommand(...)) çağrısı yapar; is kurallari handler ve domain servislerinde toplanir.

Handler tasarımı

Iyi bir handler küçük, odaklı ve test edilebilir olmalidir. Asagidaki prensipler rehberlik eder:

  • Tek istek tipi: Her handler yalnizca bir Command veya Query implement eder.
  • Idempotent yazma: Tekrarlanan komutlar tutarlı sonuç uretmelidir.
  • Transaction sınırı: Bir handler bir is birimi (unit of work) kapsamında calismalidir.
  • Side effect kontrolu: E-posta, mesaj kuyrugu gibi yan etkiler ayri pipeline veya domain event ile tetiklenmelidir.

Örnek komut handler:

public sealed record CreateProductCommand(string Name, decimal Price) : IRequest<int>;

public sealed class CreateProductHandler : IRequestHandler<CreateProductCommand, int>
{
    private readonly IProductRepository _repo;
    private readonly IUnitOfWork _uow;

    public CreateProductHandler(IProductRepository repo, IUnitOfWork uow)
    {
        _repo = repo;
        _uow = uow;
    }

    public async Task<int> Handle(CreateProductCommand request, CancellationToken ct)
    {
        var entity = Product.Create(request.Name, request.Price);
        await _repo.AddAsync(entity, ct);
        await _uow.SaveChangesAsync(ct);
        return entity.Id;
    }
}

Pipeline davranislari

MediatR pipeline'i, handler calismadan once ve sonra devreye giren cross-cutting concern'ler icin kullanılır. Yaygın pipeline'lar:

  • LoggingBehavior: Istek ve sure loglanir.
  • ValidationBehavior: FluentValidation ile komut dogrulanir.
  • TransactionBehavior: Handler transaction icinde calistirilir.
  • PerformanceBehavior: Yavas handler'lar tespit edilir.

Validation pipeline ornegi handler'a ulasmadan once hatayi yakalar:

public sealed 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 ct)
    {
        if (!_validators.Any())
            return await next();

        var context = new ValidationContext<TRequest>(request);
        var failures = _validators
            .Select(v => v.Validate(context))
            .SelectMany(r => r.Errors)
            .Where(f => f != null)
            .ToList();

        if (failures.Count != 0)
            throw new ValidationException(failures);

        return await next();
    }
}

Pipeline sayisi arttikca her istegin maliyeti artar. Gereksiz pipeline eklemekten kacinilmali; sadece gercekten merkezi olan davranislar pipeline'a alinmalidir.

Query ve Command ayrimi

Query'ler yan etkisiz olmali ve mümkün oldugunca optimize edilmelidir. Command'lar ise durum degistirir ve genellikle transaction gerektirir. Query handler'larda DTO projeksiyonu dogrudan veritabanında yapilmali; domain entity'leri gereksiz yere yuklenmemelidir.

Örnek query:

public sealed record GetProductListQuery(int Page, int PageSize) : IRequest<PagedResult<ProductListItemDto>>;

public sealed class GetProductListHandler
    : IRequestHandler<GetProductListQuery, PagedResult<ProductListItemDto>>
{
    private readonly AppDbContext _db;

    public GetProductListHandler(AppDbContext db) => _db = db;

    public async Task<PagedResult<ProductListItemDto>> Handle(
        GetProductListQuery request, CancellationToken ct)
    {
        var query = _db.Products.AsNoTracking()
            .OrderBy(p => p.Name)
            .Select(p => new ProductListItemDto(p.Id, p.Name, p.Price));

        return await query.ToPagedResultAsync(request.Page, request.PageSize, ct);
    }
}

Notification ve domain event

MediatR'in INotification mekanizmasi, bir olay gerceklestiginde birden fazla handler'in calismasini sağlar. Domain event'ler genellikle SaveChanges sonrasi yayinlanir. Örneğin siparis olusturuldugunda stok güncelleme, e-posta gonderme ve analitik kaydi ayri notification handler'lari ile yapilabilir.

Dikkat edilmesi gereken nokta: notification handler'larindaki hatalar ana işlemi etkilememeli mi? Bu is kuralina baglidir. Outbox pattern ile event'ler once veritabanına yazilir, arka planda güvenli şekilde islenir.

ASP.NET Core entegrasyonu

MediatR kaydi genellikle assembly taramasi ile yapilir:

builder.Services.AddMediatR(cfg =>
    cfg.RegisterServicesFromAssembly(typeof(CreateProductCommand).Assembly));
builder.Services.AddValidatorsFromAssembly(typeof(CreateProductCommand).Assembly);
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));

Minimal API'de endpoint handler icinde mediator inject edilir. Controller'da da ayni şekilde constructor injection kullanılır. HTTP katmanı ile application katmanı arasinda AutoMapper veya manual mapping tercih edilebilir; ancak mapping mantigi da ayri profile veya extension metotlarda tutulmalidir.

Test yaklasimi

Handler birim testleri mock repository ile yazilir. Pipeline davranislari ayri test edilir. Entegrasyon testlerinde gerçek MediatR container'i kullanmak, DI kayıt hatalarini erken yakalar.

Test piramidi onerisi:

  1. Handler birim testleri (en cok)
  2. Pipeline birim testleri
  3. HTTP entegrasyon testleri (az sayida, kritik akislar)

Anti-pattern'ler

Mediator kullaniminda kacinilmasi gereken durumlar:

  • Handler icinde baska handler cagirmak (zincirleme bağımlılık)
  • God handler: tek handler'da onlarca sorumluluk
  • Query icinde yazma işlemi yapmak
  • Pipeline'i is mantigi tasimak icin kullanmak
  • Her private metot icin ayri Command oluşturmak (asiri parcalama)

Mediator bir organizasyon araciidir; domain modelinin yerini almaz. Zengin domain modeli ve aggregate sınırları korunmalidir.

Performans dusunceleri

MediatR'in kendi overhead'i düşüktür ancak asiri pipeline, reflection tabanli validator taramasi ve gereksiz mapping maliyet olusturabilir. Yüksek throughput gerektiren okuma endpoint'lerinde dogrudan repository veya read model kullanımı dusunulebilir; her seyi mediator uzerinden gecirmek zorunlu degildir.

Source generator tabanli MediatR alternatifleri veya Wolverine gibi framework'ler daha düşük allocation profili sunabilir. Ancak ekip yetkinligi ve ekosistem olgunlugu secimde belirleyici olmalidir.

Modül ve bounded context sınırları

Her bounded context kendi command/query assembly'sine sahip olabilir. Context'ler arasi iletişim dogrudan handler çağrısı yerine integration event veya ACL (Anti-Corruption Layer) uzerinden yapilmalidir. Aksi halde modül bağımlılıkları gizlice artar.

Pratik karar cercevesi

Mediator deseni su durumlarda güçlü bir secimdir: orta ve büyük ölçekli ASP.NET Core uygulamalari, CQRS veya clean architecture benimsenen projeler, cok sayida use case'in olan domain'ler ve pipeline ile merkezi validasyon veya loglama ihtiyaci. Küçük CRUD uygulamalarinda MediatR ek karmasiklik getirebilir; pragmatik olmak önemlidir.

Doğru uygulandiginda Mediator, HTTP yuzeyini inceltir, use case'leri açıkça adlandirir ve test edilebilirligi artirir. Yanlış uygulandiginda ise gereksiz indirection katmanı haline gelir. Pratikte başarı, handler boyutunu kontrol etmek, pipeline'i olculu kullanmak ve domain mantigini doğru katmanda tutmakla gelir.

MediatR alternatifleri ve seçim kriterleri

Wolverine, Mediator (source generator) ve MassTransit gibi alternatifler farklı trade-off'lar sunar. Wolverine mesajlasma ve handler birlestirmesi sağlar; source generator tabanli çözümler reflection maliyetini azaltir. Mevcut ekip MediatR'a hakimse ve performans yeterliyse goc maliyeti dusunulmelidir.

Seçim kriterleri: ekip deneyimi, performans profili, pipeline ihtiyaci, lisans ve topluluk destegi. Küçük projede MediatR'siz dogrudan application service de geçerli bir secimdir.

Exception handling ve Result pattern

Handler'larda exception firlatmak yerine Result<T> donmek bazi ekiplerde tercih edilir. HTTP katmanı Result'i Problem Details'e map eder. Bu yaklaşım control flow'u açık tutar ancak her handler'da Result wrapping ek kod getirir. Exception middleware ile merkezi hata yönetimi daha az tekrarli olabilir.

Mapping ve HTTP adaptasyonu

HTTP DTO ile Command arasindaki dönüşüm Mediator dışında tutulmalidir. Manual extension metotlari veya Mapster gibi hafif mapper'lar tercih edilebilir. AutoMapper büyük projelerde profil yönetimi getirir ancak startup maliyeti ve debug zorlugu değerlendirilmelidir. Mapping logic'in handler icine gomulmesi okunabilirligi dusurur.

Request pipeline'da correlation ID ve kullanıcı kimligi Command'a enrich edilebilir. Bu bilgi audit log ve yetkilendirme kontrollerinde kullanılır. Ancak HttpContext'e handler icinden dogrudan erişim yerine scoped ICurrentUser servisi tercih edilir.

Idempotency ve tekrar deneme

HTTP POST tekrarlarinda cift kayıt olusmamasi icin idempotency key header'i Command'e tasınmalidir. Handler veritabanında idempotency tablosu kontrol ederek ayni key ile gelen ikinci istegi önceki sonucu donerek tamamlayabilir. Bu özellikle odeme ve siparis komutlarinda zorunludur.