CQRS & Mediator Pattern in .Net Core

Yine yeni yeniden bir karantina yazısından daha herkese merhaba 👽

Bu yazıda CQRS’in çözüm getirdiği problemlerden, Mediator pattern ve MediatR kütüphanesinden bahsedeceğim.

Problem

Geleneksel mimarilerde, veritabanını sorgulamak ve güncellemek için aynı veri modeli kullanılır. Bu yaklaşım temel CRUD işlemleri için iyi çalışır. Fakat uygulama karmaşıklığı arttıkça, çok fazla sorumluluğa sahip aşırı kompleks modeller elde edilebilir. Bu durum da beraberinde bazı problemler getirir:

❌Read ve write işlemlerinde aynı model kullanıldığından mevcut işlem için gerekli olmayan alanların da güncellenmesi gerekir.

❌İşlemler aynı veri kümesi üzerinde paralel olarak gerçekleştirildiğinde sistem üzerindeki lock olasıkları artar.

❌Sorguların karmaşıklığının veri erişim katmanında oluşturduğu yük nedeniyle performans üzerinde olumsuz etkilere neden olabilir.

❌Read ve write işlemleri performans ve ölçeklenebilirlik gereksinimleri açısından farklılık gösterir.

Solution

CQRS (Command Query Responsibility Segregation), her eylemi net bir şekilde ayırıp kendi response modellerini döndürerek; write işlemlerini read işlerinden ayırmaya olanak tanıyan bir tasarım desenidir. Verileri güncellemek için command’ları, okumak için query’leri kullanır. Command ve query’ler için bazı detaylar:

✔️Command’lar veri odaklı değil, göreve dayalı olmalıdır. (“set ReservationStatus to Reserved” yerine “Book hotel room”)

✔️Command’lar sync yerine async işlenmek için bir queue’da yer alabilirler.

✔️Query’ler veritabanındaki veriler üzerinde bir değişiklik yapmaz. Query, herhangi bir domain bilgisi içermeyen bir DTO döndürür.

Ayrı command ve query modellerine sahip olmak, tasarımı ve uygulamayı sadeleştirir. Sağladığı avantajların yanında bazı implementasyon zorluklarını da unutmamak gerekir:

🔸 Complexity: CQRS’nin temel fikri basittir. Ancak, Event Sourcing pattern içeren uygulamalarda daha karmaşık bir tasarıma neden olabilir.

🔸Messaging: CQRS mesajlaşma gerektirmese de, command’ları işlemek ve update event’lerini yayınlamak için mesajlaşmanın kullanılması yaygındır. Bu durumda, uygulamanın mesaj hatalarını veya duplicate mesajları handle etmesi gerekir.

🔸Eventual consistency: Read ve write veritabanları ayrılırsa, okunan veriler güncel olmayabilir.

Farklı modelleri yönetmek zorunda kalmanın getirdiği karmaşıklık konusunda yardımcı olabilecek bir tasarım deseni: Mediator pattern.

Mediator, nesneler arasındaki karmaşık bağımlılıkları azaltmaya yarayan bir tasarım desenidir. Nesneler arasındaki doğrudan iletişimi kısıtlar ve yalnızca bir mediator nesnesi aracılığıyla işbirliği yapmaya zorlar. Nesneler sadece meditor objesini bilir ve onunla iletişime geçer.

Havaalanı kontrol alanına yaklaşan veya alandan çıkan pilotlar birbirleriyle doğrudan iletişim kurmazlar. Bunun yerine, uçak pistine yakın bir yerde yüksek bir kulede bulunan hava trafik kontrolörü ile konuşurlar. Hava trafik kontrolörü olmadan pilotların, birbirleri ile konuşup iniş önceliklerini tartışarak havalimanının çevresindeki her uçağın farkında olmaları gerekirdi. Bu muhtemelen uçak kazası istatistiklerini arttırırdı.

MediatR, mediator deseninin open source bir uygulamasıdır. Sync veya async kalıplar kullanarak mesaj oluşturmaya, event’ler oluşturmaya/dinlemeye olanak tanır.

İmplementasyonu gerçekleştirmek için iki parçaya ihtiyacımız olacak: yapmamız gerekeni içeren bir message/request ve mesajı işlemek için logic içeren handler. MediatR’da bunun için IRequest ve IRequestHandler’ı kullanırız.

Yazının devamında bu kavramları, tamamını Github’ta paylaştığım Ticket Management Application kodları üzerinden anlatacağım.

Uygulama içerisindeki bazı nesneler/controller tüm kategorilerin bir listesine erişmek istemektedir. Bu nesneler arası bir iletişim, dolayısıyla bir mesaj olmalıdır. Döndürülecek türde parametre alan, generic IRequest interface’ini uygulayan bir query sınıfı oluştururuz. Bu yukarıda bahsettiğim message kısmıdır:

Peki nasıl handle edilecek? Yukarıda oluşturduğumuz IRequest türü için bir request handler yazmamız gerekecek. Mesaj alındığında interface içerisinde bulunan Handle() metodu çağrılacak ve bu örnekte tüm kategorilerin listesini tutan repository kullanılarak CategoryListViewModel dönülecektir.

Peki bu request ve handler’ı nasıl kullanacağız? Tek yapmamız gereken controller’da mediator’ı inject edip query’i göndermek.

CQRS mimarisinin temelinden ve MediatR ile nasıl uygunabileceğinden bahsetmeye çalıştım. Vakit ayırıp okuduğunuz için teşekkür ederim.

Sağlıklı günler! 🙏

Sources

Architecting ASP.NET Core Applications: Best Practices By Gill Cleeren