Skip to content

Implementing CQRS and Event Sourcing in .NET 9

CoreConceptAugust 3, 20269 min read

In enterprise .NET applications, traditional Create-Read-Update-Delete (CRUD) architectures suffer when handling complex domain business rules or scaling high-volume read traffic independently of writes. Blending validation write logic with reporting query logic in a single data model creates bloated ORM entities and database lock contention.

Command Query Responsibility Segregation (CQRS) separates write operations (Commands) from read operations (Queries). When combined with Event Sourcing — storing domain state changes as an append-only stream of immutable events — developers build audit-compliant, high-performance .NET 9 systems. This guide explores MediatR command routing, aggregate event hydration, and Marten read projections.

CQRS and Event Sourcing architecture pillars in .NET 9
CQRS and Event Sourcing architecture pillars in .NET 9

Mental Model: Segregating Command Mutators from Query Read Models

CQRS splits the traditional single data model into two distinct pipelines: the Command Pipeline (write side) and the Query Pipeline (read side).

Commands represent state-modifying business actions (CreateOrderCommand, CancelSubscriptionCommand). Commands execute business rules, enforce domain invariants, and write state changes without returning data payloads. Queries represent side-effect-free data fetches (GetOrderSummaryQuery), returning optimized DTO read models directly to API consumers.

Segregating read and write models enables independent data store scaling. Write models prioritize transactional integrity, while read models use denormalized, pre-aggregated database views. For core C# language primitives, read csharp async await and dotnet tools cheat sheet.

CQRS command execution flow from MediatR handler to Marten event stream append and async read projection
CQRS command execution flow from MediatR handler to Marten event stream append and async read projection

Quick reference

  • Commands modify domain state and enforce business rule invariants without returning payloads.
  • Queries fetch side-effect-free DTO data models optimized for UI consumption.
  • Eliminates ORM mapping bloat by bypassing domain entities for read queries.
  • Enables scaling read database replicas independently of write master databases.
  • Provides clear separation of concerns across enterprise C# application layers.

Remember this

Separate write-side command handlers from read-side DTO queries to eliminate ORM lock contention.

Implementing In-Memory Command Buses with MediatR in C#

In .NET 9, the MediatR library provides a clean in-memory mediator implementation for dispatching commands and queries to their respective C# handler classes.

Define commands implementing IRequest<Result> and queries implementing IRequest<OrderDto>. Handlers implement IRequestHandler<TRequest, TResponse>, isolating execution logic inside single-purpose handler classes (CreateOrderCommandHandler).

Use MediatR Pipeline Behaviors (IPipelineBehavior<TRequest, TResponse>) to inject cross-cutting concerns — such as FluentValidation input checking, logging, transaction boundaries, and metrics collection — without cluttering domain handler source code.

Quick reference

  • MediatR decouples API controllers from domain logic by dispatching requests in memory.
  • Commands implement IRequest<Result> and handlers implement IRequestHandler.
  • Pipeline Behaviors execute FluentValidation checks before reaching command handlers.
  • Centralizes exception handling and performance logging across all API endpoints.
  • Supports C# 12 primary constructors and record types for concise immutable command definitions.

Remember this

Use MediatR in-memory dispatching and Pipeline Behaviors to decouple API controllers from handlers.

Event Sourcing & Aggregate State Reconstruction with Marten

Event Sourcing shifts state storage from current-state table rows (Status = 'Completed') to an append-only sequence of historical domain events (OrderCreated, PaymentReceived, OrderShipped).

Marten is an open-source .NET library turning PostgreSQL into a document database and event store. In Marten, an Aggregate (e.g., OrderAggregate) reconstructs its current state by replaying past events from an event stream (session.Events.FetchStreamAsync(orderId)).

Replaying immutable event streams provides complete auditability, time-travel debugging capabilities, and zero data loss during schema migrations, since historical domain events remain permanently intact.

CQRS command execution flow from MediatR handler to Marten event stream append and async read projection
CQRS command execution flow from MediatR handler to Marten event stream append and async read projection

Quick reference

  • Event Sourcing records state changes as an append-only stream of immutable domain events.
  • Marten uses PostgreSQL JSONB storage to store and query .NET event streams efficiently.
  • Aggregate state is reconstructed dynamically by calling Apply(Event) mutation methods.
  • Enables time-travel auditing to inspect exact domain aggregate state at any past timestamp.
  • Eliminates complex SQL update migration scripts by preserving historical event payloads.

Remember this

Store aggregate domain events in Marten and reconstruct state by replaying event streams.

Building Asynchronous Read Model Projections for Query Performance

Replaying hundreds of historical events to compute current aggregate state for every read query slows down API response times. To solve this, Event Sourcing uses Projections.

A Projection listens to incoming event streams and projects them into flat, denormalized read-model tables (such as OrderDetailsReadModel).

Marten supports Inline Projections (updated synchronously inside the write transaction) and Async Projections (processed asynchronously in a background worker thread using Marten Async Daemon). Async Projections ensure write commands return instantly while background read-model tables update in real-time.

Quick reference

  • Projections transform raw event streams into denormalized read-side database views.
  • Inline projections update read tables synchronously inside write transaction blocks.
  • Async projections run background daemons for high-throughput write performance.
  • Query handlers fetch pre-aggregated read models using fast primary-key database lookups.
  • Rebuild projections from scratch anytime by re-running the projection daemon over historical events.

Remember this

Project event streams asynchronously into flat read-side database tables for sub-5ms query performance.

Key takeaway

To test CQRS in .NET 9, send a CreateOrderCommand via MediatR. Verify that the event stream appends to PostgreSQL and the Async Projection updates the read model table within 10 milliseconds.

Share:

Related Articles

Shipping faster in .NET is less about memorizing NuGet packages and more about knowing which job needs a tool: identity,

Read

SOLID is five design heuristics for object-oriented code that must change safely. Robert Martin popularized the acronym;

Read

Most .NET projects start clean and become entangled within six months. Controllers call repositories that call other ser

Read

Keep learning

Follow a structured path or browse all courses to go deeper.