Yükleniyor

.NET ile CQRS Design Pattern Kullanımı ve CRUD Operasyonları

Blog Kategorileri

.Net Core Mimariler
Tasarım Desenleri
ORM Araçları
API Geliştirme
Web Geliştirme
Veritabanları
24 Aralık 2025 Çarşamba
.NET ile CQRS Design Pattern Kullanımı ve CRUD Operasyonları

Merhaba,

Modern yazılım mimarilerinde uygulamalar büyüdükçe, iş kuralları karmaşıklaştıkça ve performans beklentileri arttıkça; okuma ve yazma işlemlerini tek bir model üzerinden yönetmek hem zorlaşır hem de sürdürülebilirliğini kaybetmeye başlar. İşte tam bu noktada CQRS (Command Query Responsibility Segregation) devreye girer.

CQRS, bir sistemdeki veri okuma ve yazma sorumluluklarının birbirinden ayrılmasını öneren bir mimari yaklaşımdır. Bu sayede kodun sorumluluk alanları netleşir, ölçeklenebilirlik ve performans açısından önemli avantajlar elde edilir.

Aşağıda CQRS’in temel bileşenlerini kısaca açıklayalım:

Command Nedir?

Command, sistemde veri üzerinde değişiklik yapılmasını sağlayan işlemleri temsil eder. Ekleme (Insert), güncelleme (Update) ve silme (Delete) gibi CRUD operasyonları bu kapsamdadır. Command nesneleri, ilgili işlemler için gerekli olan verileri taşır ve genellikle herhangi bir dönüş değeri beklemeksizin iş mantığının çalıştırılmasını amaçlar.

Query Nedir?

Query, sistemden veri okumaya yönelik işlemleri ifade eder. Select işlemleri için gerekli parametreleri içerir ve yalnızca veri sorgulama amacıyla kullanılır. Query tarafında sistem durumunda (state) herhangi bir değişiklik yapılmaması prensip haline getirilmiştir.

Result Nedir?

Result, bir Query çalıştırıldıktan sonra elde edilen verinin tüketiciye geri döndürülen halidir. Sorgu sonucunda üretilen tekil veya çoğul veri setleri Result nesneleri aracılığıyla taşınır.

Handler Nedir?

Handler, Command veya Query nesnelerinin işlenmesinden sorumlu olan bileşendir. Başka bir ifadeyle; ilgili operasyonun iş mantığının uygulandığı, repository veya servis katmanlarıyla etkileşime girilerek gerekli CRUD işlemlerinin gerçekleştirildiği yapıdır. CommandHandler ve QueryHandler olarak ayrı sorumluluklar şeklinde tanımlanması, mimarinin temiz ve izole kalmasına katkı sağlar.

Klasör Yapısı

CQRS/
├── Commands/
├── Queries/
├── Handlers/
└── Results/

Her klasörün altında çalışılacak entity’nin adı bir alt klasör olarak yer alır ve böylece folder structure düzenli bir şekilde korunur.

Örneğin Results > CategoryResults > GetCategoryQueryResult

.NET Core Örneği

Projenin Oluşturulması

.NET 10 için yeni bir Web API projesi oluşturdum. Siz dilerseniz farklı bir .NET sürümü de tercih edebilirsiniz. Proje adı olarak CQRSDesignPattern kullandım. CQRS yaklaşımını daha iyi anlayabilmek için öncelikle veritabanı bağlantımızı yapılandırmamız gerekiyor. Bunun için gerekli paketleri projeye dahil edelim.

Paketlerin Kurulması

Temel olarak ihtiyaç duyacağımız dört adet NuGet paketi bulunmaktadır:

  1. Microsoft.EntityFrameworkCore

  2. Microsoft.EntityFrameworkCore.SqlServer

  3. Microsoft.EntityFrameworkCore.Tools

  4. Microsoft.EntityFrameworkCore.Design

Projenizin .NET sürümüne uygun olacak şekilde bu paketleri yükledikten sonra bir sonraki aşamaya geçebiliriz.


Veritabanı Bağlantı Ayarları

Bu proje kapsamında, veritabanı bağlantı adresini appsettings.json dosyası üzerinden yöneteceğim. 

"ConnectionStrings": {
  "DefaultConnection": "server = your-server-name; database = CQRSDesginPatternDb; integrated security = true; trustServerCertificate = true"
},

Entitylerin Oluşturulması

 public class Category
 {
     public int Id { get; set; }
     public string Name { get; set; }
 }

Context Sınıfı

 public class AppDbContext : DbContext
 {
     public AppDbContext(DbContextOptions options):base(options)
     {
         
     }
     public DbSet<Category> Categories { get; set; }
 }

Program.cs üzerinden dbcontext bağlantısı

builder.Services.AddDbContext<AppDbContext>(cfg =>
{
    cfg.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
});

Migration işlemi

Migration işlemlerini başlatmak için Package Manager Console üzerinden aşağıdaki komutu çalıştırın:

add-migration initial

Herhangi bir hata oluşmazsa, Entity Framework Core tarafından oluşturulan migration sınıfını görüntüleyebilirsiniz. Bu sınıf içerisinde veritabanına yansıtılacak tablolar ve alanların detaylarını incelemeniz mümkündür. Sonrasında migration’ı veritabanına uygulamak için yine Package Manager Console üzerinde şu komutu çalıştırın:

update-database

Bu işlemle birlikte migration veritabanına yansıtılacak ve gerekli tablolar oluşturulacaktır. Artık veritabanı temel düzeyde kullanıma hazır hale gelmiş olacaktır. Bir sonraki adım olarak Category (Kategori) yapısı üzerinde çalışmaya başlayacağız.

Kategori Listeleme

Kategori listeleme işlemi için Results klasörü altında kullanılacak olan sonucu temsil eden sınıfı oluşturalım. Genellikle isimlendirme standardı olarak; Get ön eki, ilgili entity adı ve QueryResult son eki birlikte kullanılır. Örneğin: GetCategoryQueryResult

  public class GetCategoryQueryResult
  {
      public int Id { get; set; }
      public string Name { get; set; }
  }

Bu sınıf içerisinde, istemciye döndürülecek property’leri tanımlamış olduk. Bu senaryoda tüm kategori listesini döndüreceğimiz için ek bir filtreleme kriteri bulunmamaktadır. Dolayısıyla ayrıca bir Query modeli yazmamıza gerek yok. Ancak belirli kriterlere göre sorgulama yapılması gerekseydi, o durumda ilgili Query sınıfını da tanımlamamız gerekecekti.

GetCategoryQueryHandler Sınıfının Oluşturulması

 public class GetCategoryQueryHandler(AppDbContext _context)
 {
     public async Task<List<GetCategoryQueryResult>> Handle()
     {
         return await  _context.Categories.Select(t => new GetCategoryQueryResult
         {
             Id = t.Id,
             Name = t.Name,
         }).ToListAsync();
     }
 }

Bu yapıda, AppDbContext örneğini primary constructor aracılığıyla sınıfa enjekte ediyoruz. Ardından Categories tablosu üzerinden veri çekip, her kayıt için GetCategoryQueryResult nesnesi oluşturuyor ve bu nesneleri liste halinde geriye döndürüyoruz.

Categories Controller

Controllers klasörü altında CategoriesController adında yeni bir API controller oluşturalım. Bu controller içerisinde, kategori listeleme işlemi için GetCategoryQueryHandler sınıfını kullanacağız.

private readonly GetCategoryQueryHandler _getCategoriesQueryHandler;

public CategoriesController(GetCategoryQueryHandler getCategoriesQueryHandler)
{
    _getCategoriesQueryHandler = getCategoriesQueryHandler;
}

[HttpGet]
public async Task<IActionResult> GetAll()
{
    var values = await _getCategoriesQueryHandler.Handle();
    return Ok(values);
}

Bu yapıda GetCategoryQueryHandler sınıfını dependency injection aracılığıyla controller’a enjekte ediyoruz. Listeleme işlemi gerçekleştirdiğimiz için, endpoint’i HttpGet attribute’u altında tanımlıyoruz. GetAll metodu içerisinde ise Handle metodunu çağırarak veritabanından kategori listesini çekiyor ve sonucu 200 OK statüsüyle birlikte API tüketicisine geri döndürüyoruz.

Dependency Injection ve Lifetime Tanımı

GetCategoryQueryHandler sınıfını controller içerisinde dependency injection ile kullandığımız için, Program.cs dosyasında ilgili lifetime tanımını yapmamız gerekmektedir:

builder.Services.AddScoped<GetCategoryQueryHandler>();

Burada AddScoped seçilmesinin nedeni, GetCategoryQueryHandler sınıfının her HTTP isteği için bir kez oluşturulmasını sağlamaktır. Böylece handler, DbContext ile aynı yaşam döngüsünü paylaşır ve bu durum hem performans hem de kaynak yönetimi açısından en doğru yaklaşımdır.

Postman Üzerinden Test

Bu isteği, projenin çalıştığı port numarasını da kullanarak şu adrese gönderiyoruz:  http://localhost:5087/api/categories

Postman üzerinden isteği gönderdiğimizde HTTP 200 OK statüsünün döndüğünü ancak sonuç kısmında herhangi bir veri olmadığını fark edeceğiz. Bunun temel sebebi, veritabanında henüz hiç kategori kaydının bulunmamasıdır.

Bir sonraki adımda kategori ekleme (Create) işlemini gerçekleştirerek veritabanına ilk kayıtlarımızı ekleyeceğiz.


Kategori Ekleme

Bu aşamada artık Command tarafına geçiyoruz. Veri ekleme işlemlerinde, eklenecek olan tüm alanların bir Command sınıfı içerisinde tanımlanması gerekir. Bu işlem için aşağıdaki gibi bir CreateCategoryCommand sınıfı oluşturalım:

 public class CreateCategoryCommand
 {
     public string Name { get; set; }
 }

Bir önceki bölümde tüm alanların command içerisinde yer alması gerektiğini belirtmiştik. Ancak burada Id alanının tanımlı olmadığını fark etmiş olabilirsiniz. Bunun nedeni, ekleme işlemlerinde Id alanının otomatik artan (Identity) bir birincil anahtar (Primary Key) olarak tanımlanmış olmasıdır. Bu nedenle, Id bilgisini istemciden göndermemize gerek yoktur; SQL veritabanı bu değeri otomatik olarak oluşturacaktır.

Göndereceğimiz propertylerimiz hazır şimdi Handler sınıfımızı yazalım.

Artık komut üzerinden gelen veriyi veritabanına ekleyecek olan Handler sınıfını yazabiliriz. Bu sınıf, AppDbContext aracılığıyla Categories tablosuna yeni bir kayıt ekleyecektir.

 public class CreateCategoryCommandHandler(AppDbContext _context)
 {
     public async Task Handle(CreateCategoryCommand command)
     {
         _context.Categories.Add(new Category
         {
             Name = command.Name
         });

         await _context.SaveChangesAsync();
     }
 }

Bu yapıda, CreateCategoryCommand üzerinden gelen değerleri kullanarak yeni bir Category nesnesi oluşturuyor ve bunu Categories tablosuna ekliyoruz. Ardından SaveChangesAsync() metodu ile işlemi veritabanına kalıcı hale getiriyoruz.

Categories Controller - Kategori Ekleme

Kategori ekleme işlemi için controller seviyesinde bir POST endpoint tanımlayalım. Bunun için CreateCategoryCommandHandler sınıfını dependency injection ile controller’a alıyor ve komutu işliyoruz.

private readonly CreateCategoryCommandHandler _createCategoryCommandHandler;
 [HttpPost]
 public async Task<IActionResult> Add(CreateCategoryCommand command)
 {
     await createCategoryCommandHandler.Handle(command);
     return Ok();    
 }

Bu yapıda:

  1. CreateCategoryCommandHandler dependency injection ile controller’a enjekte edilmektedir.

  2. Ekleme işlemi gerçekleştirdiğimiz için endpoint HttpPost attribute’u ile işaretlenmiştir.

  3. Add metodu, istemciden gelen CreateCategoryCommand nesnesini handler’a ileterek veritabanına kayıt eklenmesini sağlar.

  4. İşlem başarıyla tamamlandığında 200 OK döndürülür.

CreateCategoryCommandHandler sınıfını da controller içerisinde dependency injection ile kullanacağımız için, Program.cs dosyasında lifetime tanımını yapmamız gerekmektedir:

builder.Services.AddScoped<CreateCategoryCommandHandler>();

Postman üzerinden API’ye bir POST isteği göndererek yeni bir kategori ekleyebiliriz. Bunun için örnek bir JSON gövdesi aşağıdaki gibi olmalıdır:

{
   "name":"teknoloji"
}

Bu isteği, projenin çalıştığı port numarasını da kullanarak şu adrese gönderiyoruz:  http://localhost:5087/api/categories

İstek başarıyla işlendiğinde kategori veritabanına eklenecektir. Ardından GET endpoint’i üzerinden kategori listesini tekrar çağırdığınızda, eklenen kaydın listede yer aldığını görebilirsiniz.


Id’ye Göre Kategori Getirme

Bu aşamada belirli bir Id değerine göre kategori bilgisini döndürecek bir yapı oluşturacağız. Bu işlem hem bir şart içerdiği hem de geriye veri döndürdüğü için Query ve Result sınıflarını birlikte kullanıyoruz.

Öncelikle Id bilgisini taşıyacak olan GetCategoryByIdQuery sınıfını oluşturalım:

public class GetCategoryByIdQuery
{
    public int Id { get; set; }

    public GetCategoryByIdQuery(int id)
    {
        Id = id;
    }
}

Burada, Id parametresi constructor üzerinden alınarak property’e atanır. Böylece query nesnesi oluşturulurken Id değeri zorunlu hale gelmiş olur.

Result Sınıfı

Id’ye göre döndürülecek kategori bilgilerini tutacak olan GetCategoryByIdQueryResult sınıfını aşağıdaki gibi tanımlıyoruz:

 public class GetCategoryByIdQueryResult
 {
     public int Id { get; set; }
     public string Name { get; set; }
 }

Bu sınıf, istemciye geri dönecek olan veri modelini temsil eder.

Handler Sınıfı

Şimdi query’yi işleyerek veritabanından ilgili kaydı getirecek olan handler’ı yazalım:

 public class GetCategoryByIdQueryHandler(AppDbContext _context)
 {
     public async Task<GetCategoryByIdQueryResult> Handle(GetCategoryByIdQuery query)
     {
         var value = await _context.Categories.FindAsync(query.Id);
         return new GetCategoryByIdQueryResult
         {
             Id = value.Id,
             Name = value.Name
         };
     }
 }

Burada query üzerinden gelen Id değeri ile Categories tablosunda arama yapıyor, bulunan kaydı GetCategoryByIdQueryResult tipine map ederek geriye döndürüyoruz.

Categories Controller

private readonly GetCategoryByIdQueryHandler _getCategoryByIdQueryHandler;
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id)
{
    var values = await _getCategoryByIdQueryHandler.Handle(new GetCategoryByIdQuery(id));
    return Ok(values);
}

Aynı şekilde GetCategoryByIdQueryHandler dependency'e dahil ediyoruz ve id'ye göre veri getirme işlemi yapacağımız için endpointimiz httget olarak işaretlendi. 

Program.cs içerisinde handler için scoped lifetime tanımını yapmayı unutmayalım:

builder.Services.AddScoped<GetCategoryByIdQueryHandler>();

Aşağıdaki adrese GET isteği göndererek Id’ye göre kategori bilgisine erişebilirsiniz:

http://localhost:5087/api/categories/1 

Ben projeyi HTTP üzerinden çalıştırdığım için port numarası örnek olarak verilmiştir; sizde farklı olabilir.


Kategori Silme İşlemi

Silme işlemi, veri üzerinde değişiklik yapıldığı için Command tarafında ele alınmalıdır. Bu nedenle hem bir Command sınıfı hem de bu komutu işleyecek bir Handler sınıfı oluşturacağız.

Öncelikle silinecek kaydın Id bilgisini taşıyan RemoveCategoryCommand sınıfını yazalım:

 public class RemoveCategoryCommand
 {
     public int Id { get; set; }

     public RemoveCategoryCommand(int id)
     {
         Id = id;
     }
 }

Burada Id değeri constructor üzerinden alınarak property’e atanır.

Handler Sınıfımızı oluşturalım

Şimdi silme işlemini gerçekleştirecek handler’ı yazalım:

public class RemoveCategoryCommandHandler(AppDbContext _context)
{
    public async Task Handle(int id)
    {
        var value = await _context.Categories.FindAsync(id);
        if (value is not Category)
            return;

        _context.Categories.Remove(value);
        await _context.SaveChangesAsync();
    }
}

Bu yapıda:

  1. Command üzerinden gelen Id ile kayıt bulunur.

  2. Eğer kayıt yoksa null döneceği için işlem sonlandırılır.

  3. Kayıt mevcutsa silinir ve değişiklikler veritabanına kaydedilir.

Controller

private readonly RemoveCategoryCommandHandler _removeCategoryCommandHandler;
  [HttpDelete("{id}")]
  public async Task<IActionResult> Delete(int id)
  {
      await _removeCategoryCommandHandler.Handle(new RemoveCategoryCommand(id));
      return Ok();
  }

Aynı şekilde RemoveCategoryCommandHandler dependency'e dahil ediyoruz ve silme işlemi yapacağımız için endpointimiz httpdelete olarak işaretlendi. 

builder.Services.AddScoped<RemoveCategoryCommandHandler>();

http://localhost:5087/api/categories/1 adresine delete istek attığımızda id gönderdiğimiz veri silinecektir (ben projeyi httpden ayağa kaldırdım ve port numarası sizde farklı olabilir)


Kategori Güncelleme İşlemi

Güncelleme işlemi de veritabanında değişiklik yaptığı için Command tarafında ele alınmalıdır. Bu nedenle bir Command sınıfı ve bu komutu işleyecek bir Handler sınıfı tanımlıyoruz.

 public class UpdateCategoryCommand
 {
     public int Id { get; set; }
     public string Name { get; set; }
 }

Ekleme işleminde Id değeri gönderilmezken, güncelleme işleminde mutlaka gönderilmelidir. Çünkü güncellenecek kaydı veritabanında Id alanına göre bulacağız.

Handler Sınıfımız:

public class UpdateCategoryCommandHandler(AppDbContext _context)
{
    public async Task Handle(UpdateCategoryCommand command)
    {
        var value = await _context.Categories.FindAsync(command.Id);
        if (value is not Category)
            return;

        value.Name = command.Name;
        _context.Update(value);
        await _context.SaveChangesAsync();
    }
}

Bu yapıda:

  1. Command üzerinden gelen Id bilgisi ile kayıt bulunur.

  2. Kayıt yoksa işlem sonlandırılır.

  3. Kayıt varsa ilgili alan güncellenir ve veritabanına kaydedilir.

Controller

private readonly UpdateCategoryCommandHandler _updateCategoryCommandHandler;
 [HttpPut]
 public async Task<IActionResult> Update(UpdateCategoryCommand command)
 {
     await _updateCategoryCommandHandler.Handle(command);
     return Ok();
 }
builder.Services.AddScoped<UpdateCategoryCommandHandler>();

Aşağıdaki adrese PUT isteği gönderelim:

http://localhost:5087/api/categories

Gönderilecek JSON örneği:

{
   "id":1
   "name":"teknoloji"
}

İstek başarıyla işlendiğinde ilgili kayıt yeni değer ile güncellenecektir. Port numarası çalışma ortamınıza göre değişebilir.

Final Categories Controller

Aşağıdaki controller, fonksiyonel olarak doğru olsa da mimari açıdan önemli bir problem içeriyor. Tüm CRUD operasyonları için farklı handler sınıfları enjekte edildiği için controller katmanı giderek “şişkin” hale geliyor:

  1. GetCategoryQueryHandler

  2. CreateCategoryCommandHandler

  3. GetCategoryByIdQueryHandler

  4. RemoveCategoryCommandHandler

  5. UpdateCategoryCommandHandler

Burada yalnızca temel CRUD operasyonlarını görüyoruz. Ancak gerçek dünya uygulamalarında, entity’e özgü ek iş kuralları ve domain odaklı işlemler de kaçınılmaz olarak eklenecektir. Örneğin: Aktif kategorileri listele, kategoriye bağlı ürün sayısını getir kategori durumunu değiştir gibi spesifik fonksiyonlar için de benzer handler sınıfları oluşturulacaktır. Bu durumda controller seviyesi: Daha fazla bağımlılık alır Constructor giderek uzar Kod okunabilirliği düşer Bağımlılık yönetimi zorlaşır ve en önemlisi controller katmanı gereksiz derecede iş akışından haberdar hale gelecektir. Bu durum, Single Responsibility prensibine de aykırıdır.

Controller aslında yalnızca HTTP isteklerini karşılayan bir uç nokta olmalıdır ve iş kurallarıyla ilgili handler bağımlılıklarını tek tek bilmek zorunda değildir. 

Proje büyüdükçe Controller’lar gereksiz şekilde şişmeye başlar, test süreçleri giderek daha zor yönetilir, yeni bir handler eklendiğinde zincirleme düzenlemeler yapılması gerekir ve bunun sonucunda modülerlik ile sürdürülebilirlik büyük ölçüde zayıflar. Dolayısıyla bu yapı, küçük projelerde tolere edilebilir olsa bile orta ve büyük ölçekli projelerde sürdürülebilir değildir.

Tam bu noktada Mediator Pattern - MediatR, handler bağımlılıklarını merkezî bir iletişim mekanizması üzerinden yöneterek controller katmanını sadeleştirir ve bağımlılık karmaşasını ortadan kaldırır. Böylece controller yalnızca Mediator ile konuşur; handler detaylarından tamamen soyutlanmış olur.

  private readonly GetCategoryQueryHandler _getCategoriesQueryHandler;
  private readonly CreateCategoryCommandHandler _createCategoryCommandHandler;
  private readonly GetCategoryByIdQueryHandler _getCategoryByIdQueryHandler;
  private readonly RemoveCategoryCommandHandler _removeCategoryCommandHandler;
  private readonly UpdateCategoryCommandHandler _updateCategoryCommandHandler;

  public CategoriesController(GetCategoryQueryHandler getCategoriesQueryHandler, CreateCategoryCommandHandler createCategoryCommandHandler, GetCategoryByIdQueryHandler getCategoryByIdQueryHandler, RemoveCategoryCommandHandler removeCategoryCommandHandler, UpdateCategoryCommandHandler updateCategoryCommandHandler)
  {
      _getCategoriesQueryHandler = getCategoriesQueryHandler;
      _createCategoryCommandHandler = createCategoryCommandHandler;
      _getCategoryByIdQueryHandler = getCategoryByIdQueryHandler;
      _removeCategoryCommandHandler = removeCategoryCommandHandler;
      _updateCategoryCommandHandler = updateCategoryCommandHandler;
  }

  [HttpGet]
  public async Task<IActionResult> GetAll()
  {
      var values = await _getCategoriesQueryHandler.Handle();
      return Ok(values);
  }
  [HttpPut]
  public async Task<IActionResult> Update(UpdateCategoryCommand command)
  {
      await _updateCategoryCommandHandler.Handle(command);
      return Ok();
  }
  [HttpDelete("{id}")]
  public async Task<IActionResult> Delete(int id)
  {
      await _removeCategoryCommandHandler.Handle(new RemoveCategoryCommand(id));
      return Ok();
  }
  [HttpGet("{id}")]
  public async Task<IActionResult> GetById(int id)
  {
      var values = await _getCategoryByIdQueryHandler.Handle(new GetCategoryByIdQuery(id));
      return Ok(values);
  }
  [HttpPost]
  public async Task<IActionResult> Add(CreateCategoryCommand command)
  {
      await _createCategoryCommandHandler.Handle(command);
      return Ok();    
  }

Sonuç

Bu noktaya kadar Category entity’si için:

  1. Listeleme (GET)

  2. Id’ye göre getirme (GET)

  3. Ekleme (POST)

  4. Güncelleme (PUT)

  5. Silme (DELETE)

olmak üzere tüm CRUD operasyonlarını CQRS yaklaşımı ile tamamlamış olduk.

CQRSDesignPattern github: CQRSDesginPattern | Sinan Tosun

Diğer bloglarımda görüşmek üzere 👋