.NET 6 Web API, CQRS ve MediatR kütüphanesi

Merhaba, bu yazıda beraber CQRS (Command Query Responsibility Segregation) ve Mediator tasarım prensipleri ve ile ilgili çalışmalar yapacağız.

MediatR kütüphanesi iki temel prensip üzerinde kurulmuş bir kütüphanedir. Bunlar; CQRS ve Mediator.

Projenin kodlarına aşağıdaki bağlantıdan ulaşabilirsiniz. https://github.com/onurkarakus/CqrsMediatorPattern

Öncelikle CQRS nedir? biraz onu anlatmaya çalışacağım.

CQRS adından da anlaşıldığı gibi komut ve sorguların ayrılması prensibidir.
Genel olarak kullandığımız yapılarda CRUD işlemlerini aslında tek bir repository üzerinden yönetmeye çalışıyoruz. CQRS bu yapıyı bölme üzerine kurulu bir tasarım mantığı getiriyor.

Komut nesneleri veriler üzerinden değişiklik yapan (CUD), sorgular ise veriler üzerinde okuma işlemini (R) gerçekleştiren nesneler olarak tanımlanmaktadır.

CQRS prensibinin çalışma mantığı

Gördüğünüz gibi bu prensip uygulama içinde okuma ve komut işlemlerinin tamamen ayrılmasını destekler. Burada ayırma işlemi için belirli bir sınır veya yöntem kesin olarak belirtilmemiştir. İstenildiği zaman bir model, uygulama veya veri tabanı bazında da ayırma işlemi yapılabilir. Tabii ki bu kurallar uygulamanın boyutu ve karmaşıklığına göre değişebilmektedir.

Bu prensibin kısa tanımı ve temel prensibi aşağıdaki gibi tanımlanabilir.

“Bir metot bir nesnenin durumunu değiştirmelidir veya bir değer döndürmelidir. İkisini birlikte yapmamalıdır.”

Avantajları :

  • Uygulama içerisinde kullanıcılara verileri sunmak için kullanacağımız ViewModel nesnelerinin veri modellerinden oluşturulması (map) işlemlerinde karmaşıklığın önüne geçmektedir.
  • Çok sayıda veri tabanı işlemi içeren bir uygulama tasarlayacaksanız performansa önem vermek zorunda kalacaksınız demektir. Böyle bir durumda okuma ve yazma işlemlerinin ayrı nesneler olması bu işlemlerin ayrı ayrı kontrol altına alınması, yönetilmesi ve performans çalışmalarının yapılması anlamına gelmektedir.
  • Yazma ve okuma işlemleri ayrı şekilde ölçeklenebilir olduğundan dolayı daha az işlem kilitlenmeleri (db lock vb.) olacaktır.
  • Uygulama kodlarının açık ve okunabilir olmasını sağlar.
  • Takıma yeni başlayacak olan yazılımcıların projeye adaptasyonunu hızlandırabilmektedir.

Dezavantajları :

  • Eğer küçük ölçekli bir uygulama geliştirecekseniz CQRS prensibinin uyarlaması sizin için yorucu olacaktır.
  • Sadece CQRS prensibini kullanmak çok fazla kod geliştirme ihtiyacı oluşturacağından dolayı projenin geliştirme süreleri uzayacaktır.

Peki Mediator nedir ?

Mediator Tasarım Deseni nesneler arasındaki bağımlılıkları azaltmamız için bizlere olanak sağlayan bir prensiptir. En rahat kullanılan sınıf bağımlılıkları en az olan sınıftır mantığından yola çıkarak bu tasarım kalıbı ile kullanacağımız nesnelerin aynı ara yüzü uyarlar. Bu prensip ise nesnelerin iletişimlerinin birbirleri ile değil bu ara yüz üzerinden yapmalarını sağlamayı amaçlar.

En rahat kullanılan sınıf bağımlılıkları en az olan sınıftır mantığından yola çıkarak bu tasarım kalıbı ile kullanacağımız nesnelerin aynı ara yüzü uyarlaması ile iletişimlerinin birbirleri ile değil bu ara yüz üzerinden yapmalarını sağlamayı amaçlar.

Bu desen sayesinde kullandığımız nesneler birbirleri bağımlı olmak yerine iletişimi bir aracı ile sağlarlar.

Genel olarak araştırdığınız zaman verilen örneklerin en meşhuru havaalanı, kula ve uçak örneğidir. Uçakların zorunlu olmadıkları sürece birbirleri ile konuşmayarak kule ile konuşmaları bu prensibe basit bir örnek olabilir.

MediatR kütüphanesi ise Mediator tasarım desenini uygulamamız için bize kolaylık sağlayan bir kütüphanedir.

Şimdi bir proje üzerinden önce CQRS tasarım deseni örneğini incelemeye çalışalım. Ardından da MediatR kütüphanesi ile projemizi Mediator tasarım deseni ile daha kullanışlı hale getirelim.

Yeni bir ASP.NET Core Web API projesi oluşturarak isimlendirelim.

CqrsMediatorPattern isimli proje için oluşturma ekranı

Projemiz ilk oluştuğu zaman hazır olacak gelen dosyaları temizleyelim.

Senaryomuz basit. Bir kitap bilgilerini içeren uygulamamız ve bu kitapları türlerini tutacağımız bir enum değerimiz olsun. Uygulama üzerinden CRUD işlemlerini gerçekleştirelim.

Bunun için Domain -> Entities, Domain -> Enum isminde klasörler oluşturarak nesne tanımlarımızı oluşturalım.

Üç adet yeni nesne tanımımız mevcut.

Kitap bilgilerimizi tutacağımız Book.cs

using CqrsMediatorPattern.Domain.Enums;

namespace CqrsMediatorPattern.Domain.Entities;

public class Book
{
public long Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }    
public BookGenre BookGenre { get; set; }
public DateTime Created { get; set; }
public DateTime Updated { get; set; }
}

Kitap türlerini tutacağımız enum tipinde olan BookGenre.cs

namespace CqrsMediatorPattern.Domain.Enums;

public enum BookGenre
{
    None = 0,
    Horror = 1,
    Fable = 2,
    Fiction = 3,
    Mystery = 4
}

Veri tabanı işlemlerimiz için projemize Entity Framework Core paketlerini eklememiz gerekmekte. Bunun için aşağıdaki paketleri paket yöneticisi (Nuget Packet Manager) , CLI veya paket referans (PacketReference) olarak yükleyebilirsiniz.

Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Tools
Microsoft.EntityFrameworkCore
Nuget paket yöneticisi üzerinden paketlerin eklemesi

Şimdi de Dbcontext sınıfımızı tasarlayabiliriz. Data -> Context isimli bir klasör açarak Dbcontext sınıfımız orada tasarlıyoruz. Sınıfımızın ismi BookDbContext olacak şekilde bir düzenleme yapıyoruz.

BookDbContext.cs
using CqrsMediatorPattern.Domain.Entities;
using Microsoft.EntityFrameworkCore;

namespace CqrsMediatorPattern.Data.Context;

public class BookDbContext : DbContext
{
public BookDbContext(DbContextOptions<BookDbContext> options) : base(options)
    {

    }

public DbSet<Book> Books { get; set; }    
}

appsettings.json dosyamızda ConnectionString bilgilerini güncellememiz gerekiyor. Bilgileri aşağıdaki gibi ekliyoruz.

{kullanici_adiniz} : Sql Server için belirlenen kullanıcı adı
{şifreniz} : Sql Server için belirlenen şifreniz

{
"ConnectionString": {
"SqlDbConnectionString": "Data Source=.\\SQLEXPRESS; Initial Catalog=CqrsMediatr_example;Persist Security Info=True;User Id={kullanici_adiniz};Password={şifreniz};MultipleActiveResultSets=true;TrustServerCertificate=True"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

Ardından Program.cs sınıfımızda oluşturmuş olduğumuz DbContext nesnesini register etmemiz gerekiyor. Bunun için Program.cs sınıfında aşağıdaki gibi eklemeyi yapıyoruz.

using CqrsMediatorPattern.Data.Context;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddDbContextPool<BookDbContext>(
    options => options.UseSqlServer(builder.Configuration["ConnectionString:SqlDbConnectionString"]));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseAuthorization();

app.MapControllers();

app.Run();

Dah asonra veri tabanımızı ve tabloları oluşturmak için aşağıdaki komutları sırasıyla Package Manager Console üzerinde çalıştırıyoruz.

Add-Migration Init
Update-Database
Add-Migration
Update-Database

Ardından Sql Server üzerinden kontrol ettiğimizde veri tabanı ve tabloların oluşmuş olduğu görebiliriz.

Şimdi CQRS için geliştirmelerimizi yapabiliriz. İlk olarak CQRS prensibini hayata geçireceğiz. İyi ve kötü yanlarını kod olarak gördükten sonra Mediator prensibinin sağladığı yararları ve MediatR kütüphanesinin kullanımına bakacağız.

Öncelikle CQRS isminde bir klasör açarak Commands, Handlers ve Queries bilgilerimizi burada oluşturuyoruz.

CQRS klasörü ve içeriği

Commands : Uygulama içinde yapılacak olan tüm komut (CUD) işlemlerinin tanımlandığı klasör olacaktır.

Queries : Uygulama içinde yapılacak olan tüm sorgu (R) işlemlerinin tanımlandığı klasör olacaktır.

Handler : Tüm komut ve sorguları ortak noktada işleyecek ve kullanıcılara uygun olacak cevabı dönecek olan modellerimizin tanımlandığı klasör olacaktır.

Şimdi Commands klasörü içindeki nesnelerimizi tasarlayalım.

CreateBookCommandRequest.cs ve CreateBookCommandResponse.cs

using CqrsMediatorPattern.Domain.Entities;
using CqrsMediatorPattern.Domain.Enums;

namespace CqrsMediatorPattern.CQRS.Commands.Request;

public class CreateBookCommandRequest
{
public string? Title { get; set; }
public string? Description { get; set; }    
public BookGenre? BookGenre { get; set; }
}
namespace CqrsMediatorPattern.CQRS.Commands.Response;

public class CreateBookCommandResponse
{
public bool IsSuccess { get; set; }
public int CreatedBookId { get; set; }
}

DeleteBookCommandRequest.cs ve DeleteBookCommandResponse.cs

namespace CqrsMediatorPattern.CQRS.Commands.Request;

public class DeleteBookCommandRequest
{
public int BookId { get; set; }
}
namespace CqrsMediatorPattern.CQRS.Commands.Response;

public class DeleteBookCommandResponse
{
public bool IsSuccess { get; set; }
}
Command nesnelerimiz eklendikten sonra son durum

Şimdi Queries klasörü içindeki nesnelerimizi tasarlayalım.

GetAllBooksQueryRequest.cs ve GetAllBookQueryResponse.cs

namespace CqrsMediatorPattern.CQRS.Queries.Request;

public class GetAllBooksQueryRequest
{

}
using CqrsMediatorPattern.Domain.Enums;

namespace CqrsMediatorPattern.CQRS.Queries.Response;

public class GetAllBooksQueryResponse
{
public long Id { get; set; }
public string? Title { get; set; }
public string? Description { get; set; }
public BookGenre BookGenre { get; set; }
public DateTime Created { get; set; }
public DateTime Updated { get; set; }
}

Şimdi de sıra Handlers klasörüne geldi. Burada komut (Command) ve sorgu (Query) isteklerini işleyeceğimiz yapıları geliştireceğiz.

CommandHandlers

CreateBookCommandHandler.cs

using CqrsMediatorPattern.CQRS.Commands.Request;
using CqrsMediatorPattern.CQRS.Commands.Response;
using CqrsMediatorPattern.Data.Context;
using CqrsMediatorPattern.Domain.Entities;

namespace CqrsMediatorPattern.CQRS.Handlers.CommandHandlers;

public class CreateBookCommandHandler
{
private readonly BookDbContext _dbContext;

public CreateBookCommandHandler(BookDbContext dbContext)
    {
this._dbContext = dbContext;
    }

public CreateBookCommandResponse CreateBook(CreateBookCommandRequest request)
    {
        _ = _dbContext.Books.Add(new Book
        {
            BookGenre = request.BookGenre.Value,
            Created = DateTime.Now,
            Description = request.Description,
            Title = request.Title
        });

var idInformation = _dbContext.SaveChanges();

return new CreateBookCommandResponse { IsSuccess= true, CreatedBookId = idInformation };
    }
}

DeleteBookCommandHandler.cs

using CqrsMediatorPattern.CQRS.Commands.Request;
using CqrsMediatorPattern.CQRS.Commands.Response;
using CqrsMediatorPattern.Data.Context;

namespace CqrsMediatorPattern.CQRS.Handlers.CommandHandlers;

public class DeleteBookCommandHandler
{
private readonly BookDbContext _dbContext;

public DeleteBookCommandHandler(BookDbContext dbContext)
    {
this._dbContext = dbContext;
    }

public DeleteBookCommandResponse DeleteBook(DeleteBookCommandRequest request)
    {
var findBookResult = _dbContext.Books.FirstOrDefault(p => p.Id == request.BookId);

if (findBookResult == null)
        {
return new DeleteBookCommandResponse { IsSuccess = false };
        }

        _dbContext.Books.Remove(findBookResult);

        _ = _dbContext.SaveChanges();

return new DeleteBookCommandResponse { IsSuccess = true };
    }
}

QueryHandlers

GetAllBooksQueryHandler.cs

using CqrsMediatorPattern.CQRS.Queries.Request;
using CqrsMediatorPattern.CQRS.Queries.Response;
using CqrsMediatorPattern.Data.Context;

namespace CqrsMediatorPattern.CQRS.Handlers.QueryHandlers;

public class GetAllBooksQueryHandler
{
private readonly BookDbContext _dbContext;

public GetAllBooksQueryHandler(BookDbContext dbContext)
    {
this._dbContext = dbContext;
    }

public List<GetAllBooksQueryResponse> GetAllBooks(GetAllBooksQueryRequest request)
    {
return _dbContext.Books.Select(x => new GetAllBooksQueryResponse()
        {
            BookGenre = x.BookGenre,
            Created = x.Created,
            Description = x.Description,
            Id = x.Id,
            Title = x.Title,
            Updated = x.Updated
        }).ToList();
    }
}

Temel olarak CQRS prensibi ile sınıflarımızı geliştirmiş olduk. Şimdi Handler olarak geliştirdiğimiz sınıfları servis olarak uygulamamıza eklememiz ve ardından Controller sınıfımızı geliştirmemiz gerekiyor. Program.cs sınıfımızın son hali aşağıdaki gibi olacaktır.

using CqrsMediatorPattern.CQRS.Handlers.CommandHandlers;
using CqrsMediatorPattern.CQRS.Handlers.QueryHandlers;
using CqrsMediatorPattern.Data.Context;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddDbContextPool<BookDbContext>(
    options => options.UseSqlServer(builder.Configuration["ConnectionString:SqlDbConnectionString"]));

builder.Services.AddTransient<CreateBookCommandHandler>();
builder.Services.AddTransient<DeleteBookCommandHandler>();
builder.Services.AddTransient<GetAllBooksQueryHandler>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseAuthorization();

app.MapControllers();

app.Run();

Şimdi de Controller sınıfımızı geliştirelim. Controllers klasörümüzün içine BookController isminde bir API Controller oluşturuyoruz.

BookController.cs sınıfımızın son hali de aşağıdaki gibi olacaktır.

using CqrsMediatorPattern.CQRS.Commands.Request;
using CqrsMediatorPattern.CQRS.Commands.Response;
using CqrsMediatorPattern.CQRS.Handlers.CommandHandlers;
using CqrsMediatorPattern.CQRS.Handlers.QueryHandlers;
using CqrsMediatorPattern.CQRS.Queries.Request;
using Microsoft.AspNetCore.Mvc;

namespace CqrsMediatorPattern.Controllers;

[Route("api/[controller]")]
[ApiController]
public class BookController : ControllerBase
{
readonly CreateBookCommandHandler createBookCommandHandler;
readonly DeleteBookCommandHandler deleteBookCommandHandler;
readonly GetAllBooksQueryHandler getAllBooksQueryHandler;

public BookController(CreateBookCommandHandler createBookCommandHandler, DeleteBookCommandHandler deleteBookCommandHandler, GetAllBooksQueryHandler getAllBooksQueryHandler)
    {
this.createBookCommandHandler = createBookCommandHandler;
this.deleteBookCommandHandler = deleteBookCommandHandler;
this.getAllBooksQueryHandler = getAllBooksQueryHandler;
    }

    [HttpGet]
public IActionResult Get([FromQuery] GetAllBooksQueryRequest request)
    {
var allBooks = getAllBooksQueryHandler.GetAllBooks(request);

return Ok(allBooks);
    }        

    [HttpPost]
public IActionResult Post([FromBody] CreateBookCommandRequest request)
    {
        CreateBookCommandResponse response = createBookCommandHandler.CreateBook(request);

return Ok(response);
    }

    [HttpDelete("{id}")]
public IActionResult Delete([FromQuery] DeleteBookCommandRequest request)
    {
        DeleteBookCommandResponse response = deleteBookCommandHandler.DeleteBook(request);
return Ok(response);
    }
}

Ardından projemizi çalıştırıp Swagger üzerinden test edebiliriz.

İlk önce bir kitap ekleyerek ardından liste olarak kitapları alacağız.

Yeni Ktap Ekleme
Yeni Kitap Ekleme Sonucu
Kitap Listesi
Kitap Listesi Sonucu
Veri tabanında bulunan kayıt bilgisi

Gördüğünüz gibi BookController üzerinden isteklerimizi göndererek Command ve Query nesnelerimizi ayırıp işlemlerimizi farklı akışlarla yapabildik.

Biz tek bir nesne ve tablo için bu işlemleri yerine getirdik. Fakat proje büyüdükçe Command ve Query nesne sayılarımız artacak. Servis olarak uygulamamıza eklediğimiz sınıfılar fazlalaşacak ve özellik Controller sınıfıların DI işlemleri problem olmaya başlayacak gibi görünüyor.

Bir kötü durum da gelen istek (Request) bilgisinin cevap (Response) bilgisini bizim belirlememiz. Gelen isteğe göre uygun olan Handler bilgisinin de yazılımcı tarafında belirlenmesi gibi işler de projenin geliştirme aşamalarında bizlere zorluk çıkaracak, karmaşıklığı artıracaktır.

Böyle bir durumda da Mediator prensibi bize destek olacak ve MediatR kütüphanesi ile bu prensibi ekleyerek daha kolay ve daha rahat bir şekilde CQRS nesnelerimizi kullanabileceğiz. Böylelik Command, Query ve Handler nesnelerimizin davranışlarına uygun şekilde çalışmalarını ve yönetilmelerini sağlayabileceğiz.

Gelin beraber MediatR geliştirmelerini hazırlamaya başlayalım.

İlk önce MediatR kütüphanesini projemize ekleyerek başlıyoruz.

MediatR kütüphanesini projemize eklemek için aşağıdaki paketleri paket yöneticisi (Nuget Packet Manager) , CLI veya paket referans (PacketReference) olarak yükleyebilirsiniz.

MediatR
MediatR.Extensions.Microsoft.DependencyInjection
Nuger paket manager ile MediatR yüklenmesi

Ardından Program.cs sınıfımıza servis kayıt işlemini gerçekleştiriyoruz.

using CqrsMediatorPattern.CQRS.Handlers.CommandHandlers;
using CqrsMediatorPattern.CQRS.Handlers.QueryHandlers;
using CqrsMediatorPattern.Data.Context;
using MediatR;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddDbContextPool<BookDbContext>(
    options => options.UseSqlServer(builder.Configuration["ConnectionString:SqlDbConnectionString"]));

builder.Services.AddTransient<CreateBookCommandHandler>();
builder.Services.AddTransient<DeleteBookCommandHandler>();
builder.Services.AddTransient<GetAllBooksQueryHandler>();

builder.Services.AddMediatR(typeof(Program));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseAuthorization();

app.MapControllers();

app.Run();

MediatR kütüphanesi bize iki adet ara yüz sunmaktadır. Bu ara yüzler sayesinde Request ve RequestHanler nesnelerimizi belirleyerek Mediator prensibine uygun olarak sorumluluklarının almalarını sağlayabilmektedir.

Bu ara yüzler aşağıdaki gibidir.

IRequest :

namespace MediatR
{
//
// Summary:
//     Marker interface to represent a request with a void response
public interface IRequest : IRequest<Unit>, IBaseRequest
    {
    }
}

IRequestHandler :

namespace MediatR
{
//
// Summary:
//     Defines a handler for a request
//
// Type parameters:
//   TRequest:
//     The type of request being handled
//
//   TResponse:
//     The type of response from the handler
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse>
    {
//
// Summary:
//     Handles a request
//
// Parameters:
//   request:
//     The request
//
//   cancellationToken:
//     Cancellation token
//
// Returns:
//     Response from the request
Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken);
    }
}

Şimdi de daha önce oluşturduğumuz tüm Command, Query ve Handler sınıflarımızı MediatR için yenidne düzenleyelim. Burada yapacağımız CommandRequest nesnelerinin IRequest, Handler nesnelerimizin ise IRequestHandler ara yüzlerini uyarlamalarının sağlamak olacaktır.

CreateBookCommandRequest.cs

using CqrsMediatorPattern.CQRS.Commands.Response;
using CqrsMediatorPattern.Domain.Enums;
using MediatR;

namespace CqrsMediatorPattern.CQRS.Commands.Request;

public class CreateBookCommandRequest: IRequest<CreateBookCommandResponse>
{
public string? Title { get; set; }
public string? Description { get; set; }
public BookGenre? BookGenre { get; set; }
}

DeleteBookCommandRequest.cs

using CqrsMediatorPattern.CQRS.Commands.Response;
using MediatR;

namespace CqrsMediatorPattern.CQRS.Commands.Request;

public class DeleteBookCommandRequest: IRequest<DeleteBookCommandResponse>
{
public int BookId { get; set; }
}

GetAllBooksQueryRequest.cs

using CqrsMediatorPattern.CQRS.Queries.Response;
using MediatR;

namespace CqrsMediatorPattern.CQRS.Queries.Request;

public class GetAllBooksQueryRequest:IRequest<List<GetAllBooksQueryResponse>>
{

}

Şimdi de Handler nesnelerimizin değişikliklerini yapalım. Handler nesneleri için IRequestHandler<request,response> şeklinde bir ara yüz uyarlaması yapacağız. Ardından ara yüz ile birlikte gleen Handler metodunu ekleyeceğiz. Burada fark yeni eklenen Handle metodunun async olarak çalışacak olmasıdır.

CreateBookCommandHandler.cs

using CqrsMediatorPattern.CQRS.Commands.Request;
using CqrsMediatorPattern.CQRS.Commands.Response;
using CqrsMediatorPattern.Data.Context;
using CqrsMediatorPattern.Domain.Entities;
using MediatR;

namespace CqrsMediatorPattern.CQRS.Handlers.CommandHandlers;

public class CreateBookCommandHandler: IRequestHandler<CreateBookCommandRequest, CreateBookCommandResponse>
{
private readonly BookDbContext _dbContext;

public CreateBookCommandHandler(BookDbContext dbContext)
    {
this._dbContext = dbContext;
    }    

public Task<CreateBookCommandResponse> Handle(CreateBookCommandRequest request, CancellationToken cancellationToken)
    {
        _ = _dbContext.Books.Add(new Book
        {
            BookGenre = request.BookGenre.Value,
            Created = DateTime.Now,
            Description = request.Description,
            Title = request.Title
        });

var idInformation = _dbContext.SaveChanges();

return Task.FromResult(new CreateBookCommandResponse { IsSuccess = true, CreatedBookId = idInformation });
    }
}

DeleteBookCommandHandler.c

using CqrsMediatorPattern.CQRS.Commands.Request;
using CqrsMediatorPattern.CQRS.Commands.Response;
using CqrsMediatorPattern.Data.Context;
using MediatR;

namespace CqrsMediatorPattern.CQRS.Handlers.CommandHandlers;

public class DeleteBookCommandHandler : IRequestHandler<DeleteBookCommandRequest, DeleteBookCommandResponse>
{
private readonly BookDbContext _dbContext;

public DeleteBookCommandHandler(BookDbContext dbContext)
    {
this._dbContext = dbContext;
    }

public Task<DeleteBookCommandResponse> Handle(DeleteBookCommandRequest request, CancellationToken cancellationToken)
    {
var findBookResult = _dbContext.Books.FirstOrDefault(p => p.Id == request.BookId);

if (findBookResult == null)
        {
return Task.FromResult(new DeleteBookCommandResponse { IsSuccess = false });
        }

        _dbContext.Books.Remove(findBookResult);

        _ = _dbContext.SaveChanges();

return Task.FromResult(new DeleteBookCommandResponse { IsSuccess = true });
    }
}

GetAllBooksQueryHandler.cs

using CqrsMediatorPattern.CQRS.Queries.Request;
using CqrsMediatorPattern.CQRS.Queries.Response;
using CqrsMediatorPattern.Data.Context;
using MediatR;

namespace CqrsMediatorPattern.CQRS.Handlers.QueryHandlers;

public class GetAllBooksQueryHandler: IRequestHandler<GetAllBooksQueryRequest, List<GetAllBooksQueryResponse>>
{
private readonly BookDbContext _dbContext;

public GetAllBooksQueryHandler(BookDbContext dbContext)
{
this._dbContext = dbContext;
    }    

public Task<List<GetAllBooksQueryResponse>> Handle(GetAllBooksQueryRequest request, CancellationToken cancellationToken)
    {
return Task.FromResult(_dbContext.Books.Select(x => new GetAllBooksQueryResponse()
        {
            BookGenre = x.BookGenre,
            Created = x.Created,
            Description = x.Description,
            Id = x.Id,
            Title = x.Title,
            Updated = x.Updated
        }).ToList());
    }
}

Şimdi ise BookController sınıfımızı da MediatR’a uygun hale getiriyoruz. Burada yapacağımız değişiklik MediatR’ın bizim için snıflarımızı yönlendirmesi adın Controller sınıfına kendisini inject ederek yönlendirme işlemini ona bırakacağız.

BookController.cs

using CqrsMediatorPattern.CQRS.Commands.Request;
using CqrsMediatorPattern.CQRS.Commands.Response;
using CqrsMediatorPattern.CQRS.Queries.Request;
using MediatR;
using Microsoft.AspNetCore.Mvc;

namespace CqrsMediatorPattern.Controllers;

[Route("api/[controller]")]
[ApiController]
public class BookController : ControllerBase
{
readonly IMediator mediator;

public BookController(IMediator mediator)
    {
this.mediator = mediator;
    }

    [HttpGet]
public async Task<IActionResult> Get([FromQuery] GetAllBooksQueryRequest request)
    {
var allBooks = await mediator.Send(request);

return Ok(allBooks);
    }        

    [HttpPost]
public async Task<IActionResult> Post([FromBody] CreateBookCommandRequest request)
    {
        CreateBookCommandResponse response = await mediator.Send(request);

return Ok(response);
    }

    [HttpDelete("{id}")]
public async Task<IActionResult> Delete([FromQuery] DeleteBookCommandRequest request)
    {
        DeleteBookCommandResponse response = await mediator.Send(request);

return Ok(response);
    }
}

Görüldüğü gibi daha önceden eklemiş olduğumuz Handler sınıflarımızın injection bilgilerini kaldırdık. Controller metotlarımızı async olacak şekilde yeniden düzenledik ve tüm işi mediator ismi ile eklediğimiz nesneye bıraktık.

Son olarak daha önce Program.cs sınıfımızında kayıt ettiğimiz Handler servislerimizi de kaldırabiliriz.

Program.cs

using CqrsMediatorPattern.CQRS.Handlers.CommandHandlers;
using CqrsMediatorPattern.CQRS.Handlers.QueryHandlers;
using CqrsMediatorPattern.Data.Context;
using MediatR;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddDbContextPool<BookDbContext>(
    options => options.UseSqlServer(builder.Configuration["ConnectionString:SqlDbConnectionString"]));

builder.Services.AddMediatR(typeof(Program));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseAuthorization();

app.MapControllers();

app.Run();

Uygulamamızı çalıştırıp kitapları listelediğimiz zaman sorunsuz bir şekilde kitapların geldiğini göreceğiz.

Bu yazıda CQRS prensibinin büyük ölçekli uygulamaları oluştururken bize performans, yönetim ve geliştirme noktalarında sağladığı kolaylıkları inceledik. İyi yanlarının yanında kötü yanları ile de gelen CQRS için MediatR kütüphanesi ile nasıl kolay çözümler üretebileceğimize Mediator prensibini de inceleyerek incelemeye çalıştık.

CQRS yapısı istenildiği zaman tek başına istenildiğinde ise Meditor ile birlikte kullanılabilemekte ve bizlere geliştirme konusunda kolaylıklar sağlamaktadır.

Başka bir incelemede görüşmek üzere…

Projenin Github adresine aşağıdaki adresten ulaşabilirsiniz.

https://github.com/onurkarakus/CqrsMediatorPattern