.NET Core

.NET'te CQRS ve MediatR: Command ve Query Ayrımı

14 dk okuma9 Şubat 2026Güncellendi 9 Mar 2026
CQRS .NETMediatRCommand Query Separation.NET mediator patternCQRS tutorialMediatR handlersEvent sourcing .NET.NET architecture patterns

CQRS (Command Query Responsibility Segregation), yazma islemlerini (command) okuma islemlerinden (query) ayiran bir mimari desendir. .NET ekosisteminde MediatR, bu ayirimi acik handler'lar, pipeline davranislari ve temiz bir in-process mesajlasma yaklasimi ile uygulamak icin standart haline gelmis bir kutuphanedir. Bu yazida deseni bastan sona, gercek kod ornekleriyle, test stratejileriyle ve production sistemlerden edindigim deneyimlerle ele aliyorum.

Neden CQRS?

Cogu uygulama, hem okuma hem yazma islemlerini tek bir model uzerinden yuruterek baslar. Basit CRUD icin bu gayet yeterlidir, ancak domain buyudukce gerilimler ortaya cikar:

  1. Farkli optimizasyon ihtiyaclari -- Okuma tarafi denormalize projeksiyonlar ve cache'lemeden fayda gorurken, yazma tarafi siki validasyon ve transactional tutarlilik gerektirir. Tek model, her iki tarafta da odun vermeye zorlar.
  2. Belirgin niyet -- Bir gelistirici CreateOrderCommand dosyasini actiginda niyet hemen anlasilir. Bunu, 30 metodu olan ve okuma, yazma, yan etkileri karistiran genel bir OrderService ile karsilastirin.
  3. Bagimsiz olceklenebilirlik -- Bircok sistemde okuma islemleri yazma islemlerinin 10 katindan fazladir. CQRS, her iki tarafi bagimsiz olceklemenizi saglar.
  4. Event-driven uyum -- CQRS, event-driven mimariler ve event sourcing ile dogal olarak eslenir. Yazma tarafi event'ler uretir, okuma tarafi bu event'lerden projeksiyonlar olusturur.

Domain agirlikli calistigim projelerde en buyuk kazanim performans degil, gelistirici netligiydi. Yeni takim uyeleri, hangi servis metodunun ne yaptigini tahmin etmek yerine, niyete gore kod tabaninda yollarini bulabiliyorlardi.

Temel Kavramlar

Command'lar

Command'lar, sistem durumunu degistirme niyetini temsil eder. Emir kipinde adlandirilmalidir: CreateOrder, CancelSubscription, UpdateShippingAddress. Bir command ya basarili olur ya basarisiz -- zengin veri dondurmemelidir.

csharp
public record CreateOrderCommand(
    Guid CustomerId,
    List<OrderItemDto> Items,
    string ShippingAddress
) : IRequest<CreateOrderResult>;

public record CreateOrderResult(
    Guid OrderId,
    string Status
);

Query'ler

Query'ler, yan etki olmadan veri getirir. Tanimlayici sekilde adlandirilir: GetOrderById, ListActiveSubscriptions. Query'ler, tuketiciye optimize edilmis DTO'lar veya read model'ler dondurur.

csharp
public record GetOrderByIdQuery(Guid OrderId) : IRequest<OrderDetailDto>;

public record OrderDetailDto(
    Guid OrderId,
    string CustomerName,
    List<OrderItemDto> Items,
    decimal TotalAmount,
    string Status,
    DateTime CreatedAt
);

Handler'lar

Her command veya query'nin tam olarak bir handler'i vardir. Bu, her handler'in tek bir sorumluluga odaklanmasini saglar.

csharp
public class CreateOrderCommandHandler
    : IRequestHandler<CreateOrderCommand, CreateOrderResult>
{
    private readonly IOrderRepository _orderRepository;
    private readonly IUnitOfWork _unitOfWork;

    public CreateOrderCommandHandler(
        IOrderRepository orderRepository,
        IUnitOfWork unitOfWork)
    {
        _orderRepository = orderRepository;
        _unitOfWork = unitOfWork;
    }

    public async Task<CreateOrderResult> Handle(
        CreateOrderCommand request,
        CancellationToken cancellationToken)
    {
        var order = Order.Create(
            request.CustomerId,
            request.Items.Select(i => new OrderItem(i.ProductId, i.Quantity, i.UnitPrice)),
            request.ShippingAddress
        );

        await _orderRepository.AddAsync(order, cancellationToken);
        await _unitOfWork.CommitAsync(cancellationToken);

        return new CreateOrderResult(order.Id, order.Status.ToString());
    }
}
csharp
public class GetOrderByIdQueryHandler
    : IRequestHandler<GetOrderByIdQuery, OrderDetailDto>
{
    private readonly IReadOnlyOrderRepository _readRepository;

    public GetOrderByIdQueryHandler(IReadOnlyOrderRepository readRepository)
    {
        _readRepository = readRepository;
    }

    public async Task<OrderDetailDto> Handle(
        GetOrderByIdQuery request,
        CancellationToken cancellationToken)
    {
        var dto = await _readRepository.GetOrderDetailAsync(
            request.OrderId, cancellationToken);

        return dto ?? throw new OrderNotFoundException(request.OrderId);
    }
}

Pipeline Davranislari

MediatR'in en guclu ozelliklerinden biri pipeline davranislaridir. Middleware gibi calisarak mediator uzerinden gecen her istegi sararlar. Cross-cutting concern'ler handler'larda degil, burada olmalidir.

Validasyon Davranisi

csharp
public class ValidationBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    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 cancellationToken)
    {
        if (!_validators.Any())
            return await next();

        var context = new ValidationContext<TRequest>(request);

        var failures = (await Task.WhenAll(
                _validators.Select(v => v.ValidateAsync(context, cancellationToken))))
            .SelectMany(result => result.Errors)
            .Where(f => f is not null)
            .ToList();

        if (failures.Count > 0)
            throw new ValidationException(failures);

        return await next();
    }
}

Loglama Davranisi

csharp
public class LoggingBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;

    public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
    {
        _logger = logger;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        var requestName = typeof(TRequest).Name;
        _logger.LogInformation("Isleniyor {RequestName}: {@Request}", requestName, request);

        var stopwatch = Stopwatch.StartNew();
        var response = await next();
        stopwatch.Stop();

        _logger.LogInformation(
            "{RequestName} islendi, sure: {ElapsedMs}ms",
            requestName, stopwatch.ElapsedMilliseconds);

        return response;
    }
}

Performans Izleme Davranisi

csharp
public class PerformanceBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly ILogger<PerformanceBehavior<TRequest, TResponse>> _logger;
    private const int WarningThresholdMs = 500;

    public PerformanceBehavior(
        ILogger<PerformanceBehavior<TRequest, TResponse>> logger)
    {
        _logger = logger;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        var stopwatch = Stopwatch.StartNew();
        var response = await next();
        stopwatch.Stop();

        if (stopwatch.ElapsedMilliseconds > WarningThresholdMs)
        {
            _logger.LogWarning(
                "Uzun suren istek: {RequestName} ({ElapsedMs}ms) - {@Request}",
                typeof(TRequest).Name,
                stopwatch.ElapsedMilliseconds,
                request);
        }

        return response;
    }
}

Kayit ve Kurulum

Her seyi DI container'da bir araya getirmek:

csharp
// Program.cs veya Startup.cs
builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(typeof(CreateOrderCommand).Assembly);

    // Pipeline davranislari kayit sirasina gore calisir
    cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
    cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
    cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(PerformanceBehavior<,>));
});

// FluentValidation validator'larini kaydet
builder.Services.AddValidatorsFromAssembly(typeof(CreateOrderCommand).Assembly);

Minimal bir controller su sekilde gorunur:

csharp
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly IMediator _mediator;

    public OrdersController(IMediator mediator) => _mediator = mediator;

    [HttpPost]
    public async Task<IActionResult> Create(
        [FromBody] CreateOrderCommand command,
        CancellationToken ct)
    {
        var result = await _mediator.Send(command, ct);
        return CreatedAtAction(nameof(GetById), new { id = result.OrderId }, result);
    }

    [HttpGet("{id:guid}")]
    public async Task<IActionResult> GetById(Guid id, CancellationToken ct)
    {
        var result = await _mediator.Send(new GetOrderByIdQuery(id), ct);
        return Ok(result);
    }
}

Event Sourcing'e Giris

CQRS ve event sourcing sik sik birlikte anilir ve bunun iyi bir nedeni var -- birbirlerini dogal olarak tamamlarlar, ancak ayni sey degillerdir.

Geleneksel bir CQRS kurulumunda yazma modeli mevcut durumu veritabanina kaydeder. Event sourcing'de ise mevcut durumu saklamak yerine, o duruma ulastiran olaylarin sirasini saklarsiniz. Her OrderCreated, ItemAdded, OrderShipped olayi bir event store'a eklenir ve mevcut durum bu olaylar yeniden oynatilarak yeniden olusturulur.

csharp
// Order aggregate'inin urettigi domain event'leri
public record OrderCreatedEvent(
    Guid OrderId,
    Guid CustomerId,
    DateTime CreatedAt
) : INotification;

public record OrderItemAddedEvent(
    Guid OrderId,
    Guid ProductId,
    int Quantity,
    decimal UnitPrice
) : INotification;

public record OrderShippedEvent(
    Guid OrderId,
    string TrackingNumber,
    DateTime ShippedAt
) : INotification;

Okuma tarafi bu event'lere abone olur ve optimize edilmis projeksiyonlar olusturur:

csharp
public class OrderSummaryProjection
    : INotificationHandler<OrderCreatedEvent>,
      INotificationHandler<OrderShippedEvent>
{
    private readonly IOrderSummaryStore _store;

    public OrderSummaryProjection(IOrderSummaryStore store) => _store = store;

    public async Task Handle(
        OrderCreatedEvent notification,
        CancellationToken cancellationToken)
    {
        await _store.CreateSummaryAsync(new OrderSummary
        {
            OrderId = notification.OrderId,
            CustomerId = notification.CustomerId,
            Status = "Created",
            CreatedAt = notification.CreatedAt
        }, cancellationToken);
    }

    public async Task Handle(
        OrderShippedEvent notification,
        CancellationToken cancellationToken)
    {
        await _store.UpdateStatusAsync(
            notification.OrderId, "Shipped", cancellationToken);
    }
}

Temel avantaj: ucretsiz olarak eksiksiz bir denetim izi elde edersiniz ve event'leri yeniden oynatarak geriye donuk yeni read model'ler olusturabilirsiniz. Dezavantaji ise event versiyonlama, performans icin snapshot alma ve yazma ile okuma taraflari arasindaki eventual consistency konularinda artan karmasikliktir.

Domain agirlikli calistigim projelerde event sourcing, denetim ve hata ayiklama icin cok degerli oldu. Bir hataya yol acan olaylarin tam sirasini yeniden oynatabilmek, saatlerce surecek arastirma surecini kisaltti.

CQRS Handler'larini Test Etmek

CQRS'in MediatR ile birlikte en guclu argumanlarinan biri test edilebilirliktir. Her handler, acik bagimliliklari olan bagimsiz bir siniftir ve bu da birim testlerini kolaylastirir.

Command Handler Testi

csharp
public class CreateOrderCommandHandlerTests
{
    private readonly Mock<IOrderRepository> _repoMock;
    private readonly Mock<IUnitOfWork> _uowMock;
    private readonly CreateOrderCommandHandler _handler;

    public CreateOrderCommandHandlerTests()
    {
        _repoMock = new Mock<IOrderRepository>();
        _uowMock = new Mock<IUnitOfWork>();
        _handler = new CreateOrderCommandHandler(
            _repoMock.Object, _uowMock.Object);
    }

    [Fact]
    public async Task Handle_GecerliCommand_SiparisOlusturulurVeKaydedilir()
    {
        // Arrange
        var command = new CreateOrderCommand(
            CustomerId: Guid.NewGuid(),
            Items: new List<OrderItemDto>
            {
                new(ProductId: Guid.NewGuid(), Quantity: 2, UnitPrice: 29.99m)
            },
            ShippingAddress: "Istiklal Cad. No:42"
        );

        // Act
        var result = await _handler.Handle(command, CancellationToken.None);

        // Assert
        result.OrderId.Should().NotBeEmpty();
        result.Status.Should().Be("Pending");

        _repoMock.Verify(
            r => r.AddAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>()),
            Times.Once);
        _uowMock.Verify(
            u => u.CommitAsync(It.IsAny<CancellationToken>()),
            Times.Once);
    }
}

Pipeline Davranisi Testi

csharp
public class ValidationBehaviorTests
{
    [Fact]
    public async Task Handle_GecersizIstek_ValidationExceptionFirlatiir()
    {
        // Arrange
        var validator = new InlineValidator<CreateOrderCommand>();
        validator.RuleFor(x => x.Items).NotEmpty();

        var behavior = new ValidationBehavior<CreateOrderCommand, CreateOrderResult>(
            new[] { validator });

        var invalidCommand = new CreateOrderCommand(
            Guid.NewGuid(), new List<OrderItemDto>(), "Istiklal Cad. No:42");

        // Act & Assert
        await Assert.ThrowsAsync<ValidationException>(
            () => behavior.Handle(
                invalidCommand,
                () => Task.FromResult(new CreateOrderResult(Guid.NewGuid(), "ok")),
                CancellationToken.None));
    }

    [Fact]
    public async Task Handle_GecerliIstek_SonrakiAdimCagrilir()
    {
        // Arrange
        var behavior = new ValidationBehavior<CreateOrderCommand, CreateOrderResult>(
            Enumerable.Empty<IValidator<CreateOrderCommand>>());

        var expected = new CreateOrderResult(Guid.NewGuid(), "Pending");
        var nextCalled = false;

        // Act
        var result = await behavior.Handle(
            new CreateOrderCommand(Guid.NewGuid(), new List<OrderItemDto>
            {
                new(Guid.NewGuid(), 1, 10m)
            }, "adres"),
            () =>
            {
                nextCalled = true;
                return Task.FromResult(expected);
            },
            CancellationToken.None);

        // Assert
        nextCalled.Should().BeTrue();
        result.Should().Be(expected);
    }
}

MediatR ile Entegrasyon Testi

csharp
public class OrderIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public OrderIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Fact]
    public async Task SiparisOlusturma_UctenUca_OlusturulanSiparisiDondurur()
    {
        // Arrange
        using var scope = _factory.Services.CreateScope();
        var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();

        var command = new CreateOrderCommand(
            Guid.NewGuid(),
            new List<OrderItemDto> { new(Guid.NewGuid(), 3, 15.00m) },
            "Bagdat Cad. No:78"
        );

        // Act
        var result = await mediator.Send(command);

        // Assert -- query tarafi uzerinden dogrula
        var order = await mediator.Send(new GetOrderByIdQuery(result.OrderId));
        order.Should().NotBeNull();
        order.TotalAmount.Should().Be(45.00m);
    }
}

CQRS Ne Zaman Gereksiz Kalinir?

CQRS gercek yapisal karmasiklik ekler. Iste durust bir karar cercevesi:

CQRS'i atlayin:

  • Uygulamaniz minimal is kurallari olan basit bir CRUD ise
  • Takim kucukse ve duzeni korumak icin mimari sinirlara ihtiyac duymuyorsa
  • Okuma ve yazma karmasikligi asagi yukari esitse -- istismar edecek bir asimetri yoksa
  • Prototip veya MVP gelistiriyorsaniz ve teslimat hizi uzun vadeli yapidan daha onemliyse
  • Domain'de 5-10'dan az aggregate varsa

CQRS'i dusunun:

  • Okuma ve yazma modelleri temelden farkli sekillere veya performans ihtiyaclarina sahipse
  • Cok sayida validasyon kurali ve durum gecisi iceren karmasik is akislariniz varsa
  • Birden fazla takim ayni domain uzerinde calisiyor ve net sinirlara ihtiyac duyuyorsa
  • Denetim izine ihtiyaciniz varsa veya event sourcing'i kullanmayi planliyorsaniz
  • Okuma tarafiniz denormalize gorunumler, arama indeksleri veya materialize projeksiyonlar gerektiriyorsa

Domain agirlikli calistigim projelerde kirilma noktasi genellikle 15-20 command/query cifti civarindaydi. Bunun altinda ayri modellerin, handler'larin ve pipeline altyapisinin getirdigi ek yuk, netlik kazanimlarini karsilamiyordu.

Yaygin CQRS Hatalari

1. Command'lardan Zengin Veri Dondurmek

Command'lar minimal bilgi dondurmeli -- bir ID, bir durum veya hic bir sey. CreateOrderCommand'iniz tam siparis detay DTO'sunu donduruyorsa, okuma/yazma sinirini bulaniklastiriyorsunuz demektir.

csharp
// Bundan kacinin
public record CreateOrderCommand(...) : IRequest<OrderDetailDto>;

// Bunu tercih edin
public record CreateOrderCommand(...) : IRequest<CreateOrderResult>;
public record CreateOrderResult(Guid OrderId, string Status);

2. Sisman Handler'lar

Handler'iniz 200 satir uzunlugundaysa, handler cok fazla is yapiyor demektir. Domain mantigi domain entity'lerine veya domain servislerine tasinmalidir. Handler bir orkestrator, is mantigi kapsayicisi degil.

csharp
// Kacinilmasi gereken: handler'da is mantigi
public async Task<Result> Handle(CreateOrderCommand cmd, CancellationToken ct)
{
    // 150 satir validasyon, hesaplama ve durum yonetimi...
}

// Tercih edilen: handler orkestre eder, domain modeli mantigi icerir
public async Task<Result> Handle(CreateOrderCommand cmd, CancellationToken ct)
{
    var order = Order.Create(cmd.CustomerId, cmd.Items, cmd.ShippingAddress);
    await _repository.AddAsync(order, ct);
    await _unitOfWork.CommitAsync(ct);
    return new Result(order.Id);
}

3. Her Sey Icin MediatR Kullanmak

MediatR, in-process mesajlasma icin bir aractir. Ayni bounded context icindeki siniflar arasindaki basit metod cagrilarinin yerine kullanmayin. A sinifi her zaman B sinifini cagiriyorsa, dogrudan bir bagimlilik mediator uzerinden yonlendirmekten daha aciktir.

4. Pipeline'i Atlamak

Validasyonu, loglamayi ve hata yonetimini dogrudan her handler'a eklemek amaci bozar. Cross-cutting concern'leri merkezilestirmek icin pipeline davranislarini kullanin. Aksi takdirde, onlarca handler'da tekrarlanan sablonlarla basa cikmak zorunda kalirsiniz.

5. Eventual Consistency'yi Goz Ardi Etmek

Read model'iniz event'lerden veya projeksiyonlardan olusturuluyorsa, write model'in gerisinde kalacaktir. UI ve API tuketicileriniz buna gore tasarlanmalidir. Bir command'dan 200 dondurulmesi, okuma tarafinin henuz guncellendigi anlamina gelmez.

Sonuc

CQRS ve MediatR, karmasik .NET uygulamalarinda netlik, test edilebilirlik ve olceklenebilirlik acisindan buyuk iyilestirmeler saglayabilir. Desen, okuma ve yazma arasinda gercek bir asimetri oldugunda, domain ayri modelleri hakli kilacak kadar zengin oldugunda ve takim dolayli konvansiyon yerine acik niyet ifadesine deger verdiginde parlak sonuclar verir. Ancak varsayilan bir secim degildir -- gercek karmasikliktan kaynaklanan mimari bir yatirimdir.

Kucuk baslayin: domain'inizin en karmasik bolumunde birkac command icin MediatR'i tanitarak baslayin. Netlik kazanimlari ek yuku hakliyorsa, oradan genisletin. Haklamiyorsa, sisteminiz hakkinda degerli bir sey ogrenmissiniz demektir.

CQRS'in domain'inize uygun olup olmadigini degerlendirmek ve pragmatik bir gecis stratejisi planlamak icin yardimci olabilirim.

İlgili Makaleler

Flutter Projeniz mi Var?

iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.

İletişime Geç