Bu yazıda CQRS ve mediator design patternleri ele alıyor olacağız, öncelikle tanımlamalar ve ana fikirden bahsedip, avantaj ve dezavantajlarına değindikten sonra örnek bir proje ile sonlandırıyor olacağız.
CQRS açılımı “Command Query Responsibility Segregation” olan temel anlamda uygulama üzerinde command ve querylerin ayrılmasını hedef alan tasarım modelidir diyebiliriz. Peki ya nedir bu command ve queryden kasıt dediğinizi duyar gibiyim, hemen şöyle açıklayalım, bir web uygulamasının temel çalışma prensibini kabaca tabir edecek olursak; client tarafından gelen isteklerin sonucunda server tarafından uygun yanıtların verilmesi işlemidir, yine biliyoruz ki client tarafından gelen istekler veri kaynağından verileri okumaya(read) ve veri yazmaya(create, update, delete) yönelik olarak ayırmamız gayet mümkündür. Bu bilgiler ışığında command ve queryleri şöyle tanımlıyoruz;
Command: Verikaynağı üzerinde, veri ekleme, güncelleme ve silme işlemlerini kapsayan isteklerdir.(Create/Insert, Update, Delete)
Query: Veri kaynağından, verileri okumaya yönelik işlemleri kapsayan isteklerdir. (Read)
Bu tanımlamalardan yola çıkarak; veri okumave yazma isteklerinin ayrıştırılarak işlenmesi ve yönetilmesini ana fikrine dayanan tasarım kalıbını CQRS olarak tanımlayabiliriz. “Peki iyi güzel command ve queryleri ayırmanın avantajı nedir? Neden böyle bir işe girerek kompleks bir mimariye yada yapıya sahip olmak isteyelimki?” diye düşünebilirsiniz hemen bununda cevabını şöylece somut bir örnek üzerinden verelim; bir kütüphane yönetimi için web api geliştirdiğinizi düşünün;
“Book” ismindeki entitymiz
public class Book
{
public Guid Id { get; set; }
public int LibraryId { get; set; }
public string Name { get; set; }
public string AuthorName { get; set; }
public string Language { get; set; }
public int Pages { get; set; }
}
Book entitymiz üzerindeki işlemleri gerçekleştirmek için, (okuma, yazma, güncelleme veya silme) viewmodellar veya dtolar üzerinden işlenecek veya okunacak fieldları belirterek şöyle oluşturmuş olalım; (örnek senaryoda BookCreateDto yeni bir kitap eklemek için, BookReadDto ilgili kitabın ayrıntılarını göstermek için kullanıldığı varsayılmıştır.)
public class BookCreateDto
{
public int LibraryId { get; set; }
public string Name { get; set; }
public string AuthorName { get; set; }
public string Language { get; set; }
public int Pages { get; set; }
}
public class BookDetailDto
{
public string Name { get; set; }
public string AuthorName { get; set; }
public string Language { get; set; }
public int Pages { get; set; }
}
BookService üzerindeki kitap detaylarını getirme ve yeni bir kitap oluşturma işlemleri şöyle gerçekleştiriliyor;
public class BookService
{
private readonly AppDbContext _dbContext;
public BookService(AppDbContext dbContext)
{
_dbContext = dbContext;
}
public BookDetailDto GetBookDetails(Guid bookId)
{
var bookDetails = _dbContext.Books.FirstorDefault(x => x.Id == bookId).Select(b => new BookReadDto
{
Name = b.Name,
AuthorName = b.AuthorName,
Language = b.Language,
Pages = b.Pages
});
return bookDetails;
}
public void CreateBook(BookCreateDto book)
{
Book bookModel = new Book
{
LibraryId = book.LibraryId,
Name = book.Name,
AuthorName = book.AuthorName,
Language = book.Language,
Pages = book.Pages
};
_dbContext.Books.Add(bookModel);
}
}
Böylelikle ilgili dtolarımızı oluşturup BookService sınıfı üzerinden kitap detaylarını getirme ve yeni bir kitap ekleme işlemlerini yazmış olduk, tabi bunları yaparken her seferinde Book entitymiz ile ilgili işlemlere ait oluşturduğumuz dtolar arasında mapping işlemlerini yapmış olduk. Az miktarda business işlemi içeren uygulamalarda mapping işlemlerini yukarıdaki gibi gerçekleştirerek ilgili aksiyonları almak herhangi bir sorun olmayacaktır. Ancak fazlaca business ve entity içeren orta/büyük çaplı bir uygulama üzerinde çalıştığınızı düşündüğünüzde ortaya çıkacak dto/viewmodel sayısını düşünebiliyor musunuz veya object mapping işlemlerinin karmaşıklığını ve yönetimini? Böyle bir senaryoda yapılan geliştirme maliyetlerinin gittikçe artacağı ve uygulamanın büyüdükçe kompleksleşeceği su götürmez bir gerçek gibi duruyor. Tamda bu noktada CQRS design pattern ile çalışmak büyük bir avantaj sağlayacaktır. Çünkü business işlemleri ayrı sınıflarda gerçekleştirileceği için kodda karmaşıklığın önüne geçip clean code’un önünü açacaktır ve geliştirme maliyetleri azalacaktır. Diğer taraftan veri kaynağından, okuma ve yazma işlevlerini tamamen ayırdığımız için ölçeklenebilirlik için ciddi bir adım atılmış olacaktır. Hatta bir adım ötede okuma ve yazma işlemleri için farklı veritabanı teknolojilerilerinin avantajlarından faydalanarak (örneğin veri yazmak MSSQL kullanılırken, veri okumak için MongoDb kullanılabilir.) için daha yüksek performans, optimizasyon ve esneklik sağlayacaktır. CQRS design patternin avantaj ve dezavantajlarını sıralayacak olursak;
Avantajlar
· Okuma ve yazma işlevleri ayrıldığı için en iyi verim sağlanacak veri tabanı teknolojilerinden yaralanmak mümkün olacaktır.
· Sunucuların ölçeklendirilmesi daha verimli şekilde gerçekleştirilebilir.
· Takımlar veya geliştiriciler arasındaki iş dağılımı daha kolay sağlanabilir ve yönetilebilir.
· Object mapping işlemlerinin karmaşasını azaltarak yönetimini kolaylaştırır, bu sayede
kodda karmaşıklığın önüne geçilmiş olur.
· Güvenlik ve yetkilendirme işlemlerinin yönetimini kolaylaştırır ve daha esnek bir
yönetim fırsatı sunar.
Dezavantajlar
· Farklı teknolojilerdeki veri tabanlarının kullanımı yanında farklı uzmanlıklarının
olmasını gerektireceği gibi bakım maliyetlerini de artıracaktır.
· Okuma ve yazma işlemleri için ayrı veri tabanları kullanıldığında yazma veri
tabanına, yazma işlemi gerçekleştirildiğinde, okuma veri tabanındaki verinin de
güncellenmesi gerektiğinden veri tutarlığını sağlamak ve yönetmek nispeten zor
olacaktır.
· CQRS mantığı basit olmasına rağmen, gerçek dünyada çoğu zaman event sourcing ile birlikte kullanıldığından karmaşıklık artacaktır.
Bir örnek ve kod üzerinden ilerleyecek olursak, öncelikle senaryomuz; ürün yönetimin gerçekleştirildiği bir web api projesinde CQRS impelementasyonunu inceliyor olacağız, bunun için örnekte .Net core 3.1 web api projesi oluşturdum. Nihayetinde ulaşmak istediğimiz nokta yukarıdaki diagramdaki gibi olacaktır.
Başlangıcı yaptığımıza göre Entities adında bir klasör oluşturacak içesine Product entityi şöyle ekleyelim;
public class Product
{
public Guid Id { get; set; }
public string Name { get; set; }
public int Quantity { get; set; }
public decimal Amount { get; set; }
}
Command, querylerimizi oluşturmak üzere CQRS adında bir klasör oluşturarak aşağıdaki görseldeki gibi yapılandıralım.
Product listesini kullanıcıya dönen CreateProductCommand isimli command’imize ait request ve responseları şöyle oluşturalım.
public class CreateProductCommandRequest
{
public string Name { get; set; }
public int Quantity { get; set; }
public decimal Amount { get; set; }
}
public class CreateProductCommandResponse
{
public bool IsSuccess {get; set;}
}
Burada kısaca ürün oluşturmak için kullanıcıdan isim, miktar ve fiyat verilerini bekliyoruz ve işlem bittikten sonra ürün ekleme işleminin başarılı veya başarısız olma durumunu geri dönmek istiyoruz. Ürün eklemeye ilişkin request ve response sınıflarını oluşturduktan sonra, ürün ekleme işlemini sağlayacak CreateProductCommandHandler sınıfımızı ise şöyle oluşturabiliriz.
public class CreateProductCommandHandler
{
public CreateProductCommandResponse CreateProduct(CreateProductCommandRequest request)
{
_context.Add(new Product()
{
Id = Guid.NewGuid(),
Name = request.Name,
Amount = request.Amount,
Quantity = request.Quantity
});
return new CreateProductCommandResponse() { IsSuccess = true };
}
}
CreateProductCommandHandler sınıfındaki CreateProduct() isimli metodumuzun geri dönüş tipi CreateProductCommandResponse ve metodda parametre olarak CreateProductCommandResponse gelmekte metod içinde ise dbcontext yardımıyla veri tabanına gelen veriyi yazıyoruz, daha sonrasında ise başarılı olduğuna ilişkin dönüşümüzü gerçekleştiriyoruz. Veri tabanına yeniproductımızı eklediğiyip kullanıcıya dönecek responseumuzu oluşturduğumuza göre göre artık roductsController controllerimizi oluşturup kullanıcı Product için post işlemini söyle yazabiliriz.
[ApiController]
[Route("api/[controller]")]
public class ProductsController: ControllerBase
{
private CreateProductCommandHandler _createProductCommandHandler;
public ProductsController(CreateProductCommandHandler createProductCommandHandler)
{
_createProductCommandHandler = createProductCommandHandler;
}
[HttpPost]
public IActionResult Post([FromForm]CreateProductCommandRequest command)
{
CreateProductCommandResponse result = _createProductCommandHandler.CreateProduct(command);
return Ok(result);
}
}
ProductControllerımız içinde ürün oluşturmak için yazdığımız post metodunda kullanıcıdan gelen ve daha önce modellediğimiz CreateProductCommandRequest ile gelen data, CreateProductCommandHandler sınıfı içindeki CreateProduct() metoduyla veri tabanına kaydediliyor ve CreateProductCommandResponse metoduyla kullanıcıya gerçekleştirilen işlemin sonucuyla ilgili geri dönüş sağlanıyor. İlgili handler sınıfımızı constructor injection ile controller içinde çağırdığımızdan instance’ının IoC containerda tutulup controller çalıştığında çalışması için Startup.cs içinde şöyle eklememiz gerekmektedir.
services.AddTransient<CreateProductCommandHandler>();
Bu noktada uygulamayı çalıştırmak istediğinizde handler içindeki _context.Add() metodunu yorum satırı haline getirmeniz gerekiyor çünkü herhangi bir veri tabanı bağlantısı veya context nesnesi oluşturulmadı sadece örnek teşkil etmesi açısıyla orada yer alıyor. Bunun dışında uygulamayı çalıştırdığınızda sorunsuz bir şekilde ürün ekleme işlemini gerçekleştirip karşılığında true mesajını görebilirsiniz. Fakat, controller içinde handle sınıflarını dependency injection yardımıyla çağırdık ve kullandık herhangi bir problem yaşamadık şimdi product controller içinde bir çok işlemi (isme göre ürünü getir, idye göre ürünü getir, eklenme tarihine göre ürünü getir, ürünü sil, ürün ekle, ürün fiyatını güncelle, ürün miktarını güncelle gibi… ) gerçekleştiriyor olabilirdik. Bu durumda her işlem için ilgili handler sınıfını ilgili contoller içine inject etmemiz gerekecek ve controllerin işlev sayısı arttıkça inject edilen handle sınıflarıda artacağından kompleks bir hal almaya başlayacaktır.
Bu noktada sorunumuzu çözmek için güzel bir pattern ile daha tanışmamız gerekiyor; namı diğer Mediator pattern, mediator pattern başka bir yazının konusu olacağından olmadığından kısaca bahsedecek olursak; Mediator kelimesi Türkçe anlamında “aracı” kavramını ifade etmektedir, aslında bu kelime mediator pattern çözüme kavuşturulacak problemi çok iyi ifade etmekte, sıklıkla verilen bir örnek üzerinden gidecek olursak, bilirsinizki hava limanlarında iniş veya kalkış yapacak uçaklar her zaman gerekli izinleri ve işlemleri kule ile konuşarak hallederler. Bu sayede hava limanı ve pistlerdeki tüm operasyon tek bir noktadan kontrol edilerek karmaşanın ve hatanın önüne geçilmiş olur, aksini durumda kule gibi bir kontrol mekanizması olmadığı bir düşünecek olursak muhtemelen pilotlar arasında iniş, kalkış için sıra tartışması bile yaşanacaktı ve tam bir kaos ortamı olıuşacaktı. Az önce CQRS tekniğini kullanarak yazdığımız ürün yönetim uygulamasına tekrar göz atacak olursak ProductsController sınıfımız içinde farklı görevleri yerine getirmek için bir çok handler’i dependency injection ile inject ettik ve bu handler’larımızı IoC container içine ekledik, şimdilik karmaşıklık ve OLID ilkelerine bağlılık konusunda çok sorun oluşturmasa da proje büyüdükçe onlarca handleri controller içine inject etmek veya IoC container içinde kaos içinde sınıf referanslarının tutulması asla istemeyeceğimiz bir durumken, yaptığımız örnekteki controller içinde dependency injectionların fazla sayıda olduğunu ve bir şeylerin yanlış gittiğini görmek mümkündür. İşlevinden ve ana fikrinden bahsettikten sonra mediator design pattern ve cqrs kullanarak örnek bir kütüphane/kitap projesi yazalım bu sayede mediator pattern kullanıldığında ve kullanılmadığında aradaki farkları görmüş olalım.
Mediator patterni .net core web apimize impelemente ederken işimizi ciddi manada kolaylaştıran MediatR kütüphanesinden yardım alacağız. Hemen projemize başlayalım;
CQRSDemo adında yeni bir web api projesi oluşturarak işe başlayalım, web api projesi oluştuktan sonra projemize .net komut satırı yardımıyla aşağıdaki komutları kullanarak veya web api üzerine sağ tıklayıp “Manage Nuget Packages” menüsü altından, MediatR ve Automapper’i ekleyelim.
dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
dotnet add package Automapper
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
MediatR ve Automapperi kullanabilmek için Startup.cs altındaki ConfigureServices metodu altına şöyle ekleyelim;
services.AddMediatR(typeof(Startup));
services.AddAutoMapper(typeof(Startup));
ardından mapping işlemlerini tanımlamak için projenin kök dizininde MappingProfiles adında klasör oluşturalım ve bu klasör altına AutomapperProfile sınıfımızı ekleyelim, bu işlemleri yaptıktan sonra klasör yapımız şöyle görünecektir.
Oluşturduğumuz Automapper sınıfını ise şöyle yazabiliriz.
public class AutomapperProfile: Profile
{
public AutomapperProfile()
{
}
}
Automapper sınıfı içinde automapper kullanarak mapping konfigürasyonunu sağlamak için, Profile sınıfından kalıtarak bir constructor oluşturalım şimdilik sınıfımızı böyle bırakabiliriz, yeri geldiğinde mapping ile ilgili işlemleri yazıyor olacağız.
Kütüphaneler projemize eklendikten ve konfigürasyonları gerçekleştirdikten sonra Entity sınıflarını içinde tutacağımız Entities klasörünü oluşturalım “Book” entitymiz
public class Book
{
public Guid BookId { get; set; }
public int LibraryId { get; set; }
public string Name { get; set; }
public string AuthorName { get; set; }
public int Quantity { get; set; }
}
Daha sonra CQRS patternimizi implemente etmek üzere CQRS adında bir klasör oluşturalım bir önceki örneğimizdeki yapıyı bir adım daha ileri taşıyarak command ve queryler için response ve requestleri ayrı klasörler altında oluşturalım, sonuç olarak elde edeceğimiz klasör yapısı şu şekilde olacaktır.
Sonraki aşamamızda Services adında bir klasör daha oluşturalım ve içine FakeService ismindeki sınıfımızı ekleyelim buradaki sınıfımızın amacı örnek projede herhangi bir veri tabanı kullanmadığımızdan kitaplara ilişkin işlemleri gerçekleştiren ve hali hazırda bulunan örnek kitap
listesini statik olarak bellekte tutan bir yapı oluşturmak bunun için FakeService sınıfımızı şöyle yapılandırabiliriz. FakeRepository veya FakeDao olarakta adlandırabiliriz fakat burada hem veri kaynağı olduğundan hemde veri işlemleri bu sınıf üzerinden gerçekleştirildiğinden FakeService olarak isimlendirilmiştir.
public class FakeService
{
public List<Book> Books {get;set; }
public FakeService()
{
Books = GetAllBooks();
}
public static List<Book> GetAllBooks()
{
var books = new List<Book>
{
new Book{ BookId= Guid.NewGuid(), LibraryId = 1, Name = "Harry Petersburg", AuthorName = "Dostoyevski", Quantity = 50 },
new Book{ BookId= Guid.NewGuid(), LibraryId = 1, Name = "Exception", AuthorName = "Fartin Mowler", Quantity = 70 },
new Book{ BookId= Guid.NewGuid(), LibraryId = 1, Name = "Anna Karenina 1", AuthorName = "Tolstoy", Quantity = 50 },
new Book{ BookId= Guid.NewGuid(), LibraryId = 1, Name = "Anna Karenina 2", AuthorName = "Tolstoy", Quantity = 50 },
new Book{ BookId= Guid.NewGuid(), LibraryId = 1, Name = "Anna Karenina 3", AuthorName = "Tolstoy", Quantity = 50 }
};
return books;
}
public void Delete(Guid bookId)
{
var book = Books.FirstOrDefault(x => x.BookId == bookId);
if (book != null)
Books.Remove(book);
}
public Book GetByName(string name)
{
return Books.FirstOrDefault(n => n.Name.ToUpper().Contains(name.ToUpper()));
}
public void Add(Book book)
{
Books.Add(book);
}
}
Oluşturduğumuz FakeService sınıfımızı daha sonra dependency injection ile kullanacağımızdan Startup.cs içindeki ConfigureServices metodu altından IoC’a şöyle ekleyelim;
Services.AddSingleton<FakeService>();
Bir sonraki adımda Controllers klasörü altında BooksController isimli controller sınıfımızı şöyle oluşturabiliriz.
[ApiController]
[Route("api/[controller]")]
public class BooksController : ControllerBase
{
[HttpGet]
public async Task<IActionResult> Get()
{
return Ok();
}
}
Yazdığımız controller içinde kitapları listelemek ve yeni bir kitap eklemek için 2 metod içermekte, artık biliyoruzki HttpGet metodları bir read işlemi gerçekleştirdiğinden query, HttpPost işlemi ise create işlemi gerçekleştirdiğinden command olarak adlandırılıyor. Şimdi CQRS klaösrü altında command ve querylerimizi oluşturmaya başlayabiliriz.
CQRS/Queries/Request klasörü altında yer alacak ve HttpGet metoduna gelen işlemleri karşılayacak
GetAllBooksQueryRequest sınıfımızı MediatR ile mediator implementasyonu için IRequest interface’inden kalıtım alarak şöyle oluşturabiliriz.
public class GetAllBooksQueryRequest: IRequest<List<GetAllBooksQueryResponse>>
{
}
HttpGet işlemiyle veri kaynağındaki tüm kitapları listelemek için client ve dolayısıyla controller tarafından herhangi bir parametre gelmeyeceğinden herhangi bir property eklemeden bırakıyoruz.
IRequest<List<GetAllBooksQueryResponse>> interfaceini implemente ederek GetAllBooksQueryRequest olarak gelen isteklere karşılık bir List<GetAllBooksQueryResponse> döneceğini belirtiyoruz. Gelin şimdi de bu request karşısında dönecek GetAllBookQueryResponse sınıfını CQRS/QueriesRequest klasörü altında şöyle oluşturalım.
public class GetAllBooksQueryResponse
{
public string Name { get; set; }
public string AuthorName { get; set; }
public int Quantity { get; set; }
}
request ve response ile ilgili sınıflarımızı oluşturduktan sonra bu işlemleri handle edecek olan GetAllBooksQueryHandler sınıfımızı CQRS/Handlers/Query klasörü altında şöyle oluşturalım;
public class GetAllBooksQueryHandler : IRequestHandler<GetAllBooksQueryRequest, List<GetAllBooksQueryResponse>>
{
private readonly FakeService _bookService;
private readonly IMapper _mapper;
public GetAllBooksQueryHandler(FakeService bookService, IMapper mapper)
{
_bookService = bookService;
_mapper = mapper;
}
Task<List<GetAllBooksQueryResponse>> Handle(GetAllBooksQueryRequest request, CancellationToken cancellationToken)
{
var books = _bookService.Books.ToList();
var response = _mapper.Map<List<GetAllBooksQueryResponse>>(books);
return Task.FromResult(response);
}
}
Oluşturduğumuz sınıfa IRequestHandler<GetallBooksQueryRequest, List<GetAllBooksQueryResponse>> interfaceini az önce oluşturduğumuz request ve response tipleri vererek implemente ettiğimizde Handle metodumuz oluşacaktır. Bu noktada handle metodu içinde servis tarafında oluşturduğumuz kitap datasını getiriyoruz ve automapper ile servis tarafından gelen kitap listemizi, List<GetAllBooksQueryResponse> olan az önce belirlediğimiz response tipine map ediyoruz ve geri gönüyoruz. Mapping işleminin gerçekleştirilebilmesi için daha önce oluşturduğumuz MappingProfiles klasörü altındaki AutomapperProfile sınıfındaki constructor içinde şöyle ekliyoruz.
CreateMap<Book, GetAllBooksQueryResponse>();
Bu sayede automapper Book türündeki datayı GetAllBooksQueryRequest içindeki ilgili alanlarla eşleştirip mapleyecektir. Ayrıca dikkat etmemiz gereken bir nokta mapleyeceğimiz modeldeki propery
isimlerinin entitymizdeki property isimleri ile aynı olması gerektiğidir, birbirine karşılık gelen propertyler için modelde farklı bir isimlendirme yaparsanız automapper default konfigürasyonda bunu tanımayacaktır. Farklı propertyleri map etmek için, aşağıdaki konfigürasyonu kullanabiliriz.
CreateMap<entity, model>().ForMember(entity => entity.Name, model => model.MapFrom(x => x.DisplayName))
bu örneğe göre: entitymizde “Name” olarak isimlerindirilen modelin modelimizdeki DisplayName’e karşılık geldiğini belirtmiş oluyoruz. Daha fazla bilgi için Automapper dokümantasyonuna göz atabilirsiniz.
Artık BooksController sınıfımıza son halini verebiliriz;
[ApiController]
[Route("api/[controller]")]
public class BooksController : ControllerBase
{
private readonly IMediator _mediator;
public BooksController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet]
public async Task<IActionResult> Get()
{
var query = new GetAllBooksQueryRequest();
var result = await _mediator.Send(query);
return Ok(result);
}
}
Meditor patterni kullanmak için gerekli olan IMediator interfacei yardımıyla dependency injection ile inject ediyoruz ve client tarafından “https://localhost:[portnumarası]/api/books” gelen HttpGet isteklerine karşılık, tüm kitapları dönmek için oluşturduğumuz GetAllBooksQueryRequest’i mediator’un sen metoduyla gönderiyoruz ve gönderdiğimiz request GetlAllBooksQueryHandler tarafından handle edilip ilgili işlemler(business) ve mapping işlemleri gerçekleştirilerek List<GetAllBooksQueryResponse> tipinde response olarak dönülüyor.
Postman ile apimizi test edecek olursak
Beklediğimiz gibi BooksController içindeki Get metodunu çağırdığımızda bir GetAllBooksQueryRequest oluşuyor ve query handle edilerek geriye List<GetAllBooksQueryResponse> dönüyor.
Create, update, delete işlemlerinide aynı şekilde gerçekleştirebilirdik fakat yazıyı daha fazla uzatmamak adına ve temel noktalara değindiğimi düşündüğümden burada sonlandıracağım.
Aşağıdaki bağlantıdan daha fazla örnek içeren projeyi inceleyebilir ve bu makaleyi yazarken yararlandığım kaynaklara ulaşabilirsiniz.
Bir sonraki yazıda görüşünceye dek sağlıcakla kalınız.
Source Code: https://github.com/kdrkrgz/CQRSwithMediatorDemo
Sources:
https://martinfowler.com/bliki/CQRS.html
https://docs.microsoft.com/tr-tr/azure/architecture/patterns/cqrs
https://www.gencayyildiz.com/blog/cqrs-pattern-nedir-mediatr-kutuphanesi-ile-nasil-uygulanir/
https://www.programmingwithwolfgang.com/cqrs-in-asp-net-core-3-1/
Be First to Comment