What is CQRS?
CQRS (Command Query Responsibility Segregation) is a pattern that separates read and write operations for a data store. Commands change the state of the system, while Queries return data without side effects.
CQRS is implemented across all services in AspNetRun using MediatR as the mediator and BuildingBlocks.CQRS for abstractions.
Core Principle
Commands
Write operations
Change system state
Void or return simple results
Can fail with validation errors
Examples: CreateOrder, UpdateProduct, DeleteBasket
Queries
Read operations
Never modify state
Return data
Idempotent (can be called multiple times)
Examples: GetOrders, GetProductById, GetBasket
CQRS Building Blocks
The project defines CQRS abstractions in the BuildingBlocks shared library:
Command Interfaces
src/BuildingBlocks/BuildingBlocks/CQRS/ICommand.cs
using MediatR ;
namespace BuildingBlocks . CQRS ;
// Command without return value (void)
public interface ICommand : ICommand < Unit >
{
}
// Command with return value
public interface ICommand < out TResponse > : IRequest < TResponse >
{
}
src/BuildingBlocks/BuildingBlocks/CQRS/ICommandHandler.cs
using MediatR ;
namespace BuildingBlocks . CQRS ;
// Handler for void commands
public interface ICommandHandler < in TCommand >
: ICommandHandler < TCommand , Unit >
where TCommand : ICommand < Unit >
{
}
// Handler for commands with return values
public interface ICommandHandler < in TCommand , TResponse >
: IRequestHandler < TCommand , TResponse >
where TCommand : ICommand < TResponse >
where TResponse : notnull
{
}
Query Interfaces
src/BuildingBlocks/BuildingBlocks/CQRS/IQuery.cs
using MediatR ;
namespace BuildingBlocks . CQRS ;
public interface IQuery < out TResponse > : IRequest < TResponse >
where TResponse : notnull
{
}
src/BuildingBlocks/BuildingBlocks/CQRS/IQueryHandler.cs
using MediatR ;
namespace BuildingBlocks . CQRS ;
public interface IQueryHandler < in TQuery , TResponse >
: IRequestHandler < TQuery , TResponse >
where TQuery : IQuery < TResponse >
where TResponse : notnull
{
}
Why use abstractions?
Consistency : All services use the same pattern
Clarity : Explicit distinction between commands and queries
MediatR Integration : Abstractions wrap MediatR interfaces
Type Safety : Generic constraints ensure correct usage
CQRS in the Ordering Service
The Ordering service demonstrates CQRS with Clean Architecture and DDD.
Command Example: CreateOrder
1. Command Definition
src/Services/Ordering/Ordering.Application/Orders/Commands/CreateOrder/CreateOrderCommand.cs
using BuildingBlocks . CQRS ;
using FluentValidation ;
using Ordering . Application . Dtos ;
namespace Ordering . Application . Orders . Commands . CreateOrder ;
// Command with input data and result type
public record CreateOrderCommand ( OrderDto Order )
: ICommand < CreateOrderResult >;
// Result returned after successful command execution
public record CreateOrderResult ( Guid Id );
// Validation rules for the command
public class CreateOrderCommandValidator : AbstractValidator < CreateOrderCommand >
{
public CreateOrderCommandValidator ()
{
RuleFor ( x => x . Order . OrderName )
. NotEmpty (). WithMessage ( "Name is required" );
RuleFor ( x => x . Order . CustomerId )
. NotNull (). WithMessage ( "CustomerId is required" );
RuleFor ( x => x . Order . OrderItems )
. NotEmpty (). WithMessage ( "OrderItems should not be empty" );
}
}
2. Command Handler
src/Services/Ordering/Ordering.Application/Orders/Commands/CreateOrder/CreateOrderHandler.cs
namespace Ordering . Application . Orders . Commands . CreateOrder ;
public class CreateOrderHandler ( IApplicationDbContext dbContext )
: ICommandHandler < CreateOrderCommand , CreateOrderResult >
{
public async Task < CreateOrderResult > Handle (
CreateOrderCommand command ,
CancellationToken cancellationToken )
{
// 1. Create Order entity from command object
var order = CreateNewOrder ( command . Order );
// 2. Save to database
dbContext . Orders . Add ( order );
await dbContext . SaveChangesAsync ( cancellationToken );
// 3. Return result
return new CreateOrderResult ( order . Id . Value );
}
private Order CreateNewOrder ( OrderDto orderDto )
{
// Create value objects
var shippingAddress = Address . Of (
orderDto . ShippingAddress . FirstName ,
orderDto . ShippingAddress . LastName ,
orderDto . ShippingAddress . EmailAddress ,
orderDto . ShippingAddress . AddressLine ,
orderDto . ShippingAddress . Country ,
orderDto . ShippingAddress . State ,
orderDto . ShippingAddress . ZipCode );
var billingAddress = Address . Of (
orderDto . BillingAddress . FirstName ,
orderDto . BillingAddress . LastName ,
orderDto . BillingAddress . EmailAddress ,
orderDto . BillingAddress . AddressLine ,
orderDto . BillingAddress . Country ,
orderDto . BillingAddress . State ,
orderDto . BillingAddress . ZipCode );
// Create aggregate using factory method
var newOrder = Order . Create (
id : OrderId . Of ( Guid . NewGuid ()),
customerId : CustomerId . Of ( orderDto . CustomerId ),
orderName : OrderName . Of ( orderDto . OrderName ),
shippingAddress : shippingAddress ,
billingAddress : billingAddress ,
payment : Payment . Of (
orderDto . Payment . CardName ,
orderDto . Payment . CardNumber ,
orderDto . Payment . Expiration ,
orderDto . Payment . Cvv ,
orderDto . Payment . PaymentMethod )
);
// Add order items through aggregate method
foreach ( var orderItemDto in orderDto . OrderItems )
{
newOrder . Add (
ProductId . Of ( orderItemDto . ProductId ),
orderItemDto . Quantity ,
orderItemDto . Price );
}
return newOrder ;
}
}
3. API Endpoint
src/Services/Ordering/Ordering.API/Endpoints/CreateOrder.cs
namespace Ordering . API . Endpoints ;
public record CreateOrderRequest ( OrderDto Order );
public record CreateOrderResponse ( Guid Id );
public class CreateOrder : ICarterModule
{
public void AddRoutes ( IEndpointRouteBuilder app )
{
app . MapPost ( "/orders" , async ( CreateOrderRequest request , ISender sender ) =>
{
// 1. Map request to command
var command = request . Adapt < CreateOrderCommand >();
// 2. Send command via MediatR
var result = await sender . Send ( command );
// 3. Map result to response
var response = result . Adapt < CreateOrderResponse >();
return Results . Created ( $"/orders/ { response . Id } " , response );
})
. WithName ( "CreateOrder" )
. Produces < CreateOrderResponse >( StatusCodes . Status201Created )
. ProducesProblem ( StatusCodes . Status400BadRequest )
. WithSummary ( "Create Order" )
. WithDescription ( "Create Order" );
}
}
Query Example: GetOrders
1. Query Definition
src/Services/Ordering/Ordering.Application/Orders/Queries/GetOrders/GetOrdersQuery.cs
using BuildingBlocks . Pagination ;
namespace Ordering . Application . Orders . Queries . GetOrders ;
// Query with parameters and result type
public record GetOrdersQuery ( PaginationRequest PaginationRequest )
: IQuery < GetOrdersResult >;
// Result containing the data
public record GetOrdersResult ( PaginatedResult < OrderDto > Orders );
2. Query Handler
src/Services/Ordering/Ordering.Application/Orders/Queries/GetOrders/GetOrdersHandler.cs
using BuildingBlocks . Pagination ;
namespace Ordering . Application . Orders . Queries . GetOrders ;
public class GetOrdersHandler ( IApplicationDbContext dbContext )
: IQueryHandler < GetOrdersQuery , GetOrdersResult >
{
public async Task < GetOrdersResult > Handle (
GetOrdersQuery query ,
CancellationToken cancellationToken )
{
// 1. Get pagination parameters
var pageIndex = query . PaginationRequest . PageIndex ;
var pageSize = query . PaginationRequest . PageSize ;
// 2. Get total count
var totalCount = await dbContext . Orders . LongCountAsync ( cancellationToken );
// 3. Query orders with pagination
var orders = await dbContext . Orders
. Include ( o => o . OrderItems )
. OrderBy ( o => o . OrderName . Value )
. Skip ( pageSize * pageIndex )
. Take ( pageSize )
. ToListAsync ( cancellationToken );
// 4. Return paginated result
return new GetOrdersResult (
new PaginatedResult < OrderDto >(
pageIndex ,
pageSize ,
totalCount ,
orders . ToOrderDtoList ()));
}
}
3. API Endpoint
namespace Ordering . API . Endpoints ;
public record GetOrdersRequest ( PaginationRequest PaginationRequest );
public record GetOrdersResponse ( PaginatedResult < OrderDto > Orders );
public class GetOrders : ICarterModule
{
public void AddRoutes ( IEndpointRouteBuilder app )
{
app . MapGet ( "/orders" , async ([ AsParameters ] GetOrdersRequest request , ISender sender ) =>
{
var query = request . Adapt < GetOrdersQuery >();
var result = await sender . Send ( query );
var response = result . Adapt < GetOrdersResponse >();
return Results . Ok ( response );
})
. WithName ( "GetOrders" )
. Produces < GetOrdersResponse >( StatusCodes . Status200OK )
. WithSummary ( "Get Orders" )
. WithDescription ( "Get Orders with Pagination" );
}
}
CQRS in the Catalog Service
The Catalog service demonstrates CQRS with Vertical Slice Architecture.
Command: CreateProduct
src/Services/Catalog/Catalog.API/Products/CreateProduct/CreateProductHandler.cs
namespace Catalog . API . Products . CreateProduct ;
public record CreateProductCommand ( string Name , List < string > Category ,
string Description , string ImageFile , decimal Price )
: ICommand < CreateProductResult >;
public record CreateProductResult ( Guid Id );
public class CreateProductCommandValidator : AbstractValidator < CreateProductCommand >
{
public CreateProductCommandValidator ()
{
RuleFor ( x => x . Name ). NotEmpty (). WithMessage ( "Name is required" );
RuleFor ( x => x . Category ). NotEmpty (). WithMessage ( "Category is required" );
RuleFor ( x => x . ImageFile ). NotEmpty (). WithMessage ( "ImageFile is required" );
RuleFor ( x => x . Price ). GreaterThan ( 0 ). WithMessage ( "Price must be greater than 0" );
}
}
internal class CreateProductCommandHandler
( IDocumentSession session )
: ICommandHandler < CreateProductCommand , CreateProductResult >
{
public async Task < CreateProductResult > Handle (
CreateProductCommand command ,
CancellationToken cancellationToken )
{
// Create Product entity from command object
var product = new Product
{
Name = command . Name ,
Category = command . Category ,
Description = command . Description ,
ImageFile = command . ImageFile ,
Price = command . Price
};
// Save to database (Marten document store)
session . Store ( product );
await session . SaveChangesAsync ( cancellationToken );
// Return result
return new CreateProductResult ( product . Id );
}
}
Query: GetProducts
src/Services/Catalog/Catalog.API/Products/GetProducts/GetProductsHandler.cs
namespace Catalog . API . Products . GetProducts ;
public record GetProductsQuery ( int ? PageNumber = 1 , int ? PageSize = 10 )
: IQuery < GetProductsResult >;
public record GetProductsResult ( IEnumerable < Product > Products );
internal class GetProductsQueryHandler
( IDocumentSession session )
: IQueryHandler < GetProductsQuery , GetProductsResult >
{
public async Task < GetProductsResult > Handle (
GetProductsQuery query ,
CancellationToken cancellationToken )
{
var products = await session . Query < Product >()
. ToPagedListAsync ( query . PageNumber ?? 1 , query . PageSize ?? 10 , cancellationToken );
return new GetProductsResult ( products );
}
}
Vertical Slice Difference:
Commands, handlers, and endpoints are in the same feature folder
Simpler model without domain complexity
Uses Marten document database instead of EF Core
No separate domain layer
The project uses MediatR pipeline behaviors for cross-cutting concerns:
Validation Behavior
Automatically validates commands before execution:
src/BuildingBlocks/BuildingBlocks/Behaviors/ValidationBehavior.cs
using BuildingBlocks . CQRS ;
using FluentValidation ;
using MediatR ;
namespace BuildingBlocks . Behaviors ;
public class ValidationBehavior < TRequest , TResponse >
( IEnumerable < IValidator < TRequest >> validators )
: IPipelineBehavior < TRequest , TResponse >
where TRequest : ICommand < TResponse >
{
public async Task < TResponse > Handle (
TRequest request ,
RequestHandlerDelegate < TResponse > next ,
CancellationToken cancellationToken )
{
// Create validation context
var context = new ValidationContext < TRequest >( request );
// Run all validators
var validationResults =
await Task . WhenAll ( validators . Select ( v =>
v . ValidateAsync ( context , cancellationToken )));
// Collect failures
var failures =
validationResults
. Where ( r => r . Errors . Any ())
. SelectMany ( r => r . Errors )
. ToList ();
// Throw if validation failed
if ( failures . Any ())
throw new ValidationException ( failures );
// Continue pipeline
return await next ();
}
}
Logging Behavior
Logs all requests with performance tracking:
src/BuildingBlocks/BuildingBlocks/Behaviors/LoggingBehavior.cs
using MediatR ;
using Microsoft . Extensions . Logging ;
using System . Diagnostics ;
namespace BuildingBlocks . Behaviors ;
public class LoggingBehavior < TRequest , TResponse >
( ILogger < LoggingBehavior < TRequest , TResponse >> logger )
: IPipelineBehavior < TRequest , TResponse >
where TRequest : notnull , IRequest < TResponse >
where TResponse : notnull
{
public async Task < TResponse > Handle (
TRequest request ,
RequestHandlerDelegate < TResponse > next ,
CancellationToken cancellationToken )
{
logger . LogInformation (
"[START] Handle request={Request} - Response={Response} - RequestData={RequestData}" ,
typeof ( TRequest ). Name , typeof ( TResponse ). Name , request );
var timer = new Stopwatch ();
timer . Start ();
var response = await next ();
timer . Stop ();
var timeTaken = timer . Elapsed ;
// Warn if request took too long
if ( timeTaken . Seconds > 3 )
logger . LogWarning (
"[PERFORMANCE] The request {Request} took {TimeTaken} seconds." ,
typeof ( TRequest ). Name , timeTaken . Seconds );
logger . LogInformation (
"[END] Handled {Request} with {Response}" ,
typeof ( TRequest ). Name , typeof ( TResponse ). Name );
return response ;
}
}
Registering Behaviors
src/Services/Catalog/Catalog.API/Program.cs:8-13
var assembly = typeof ( Program ). Assembly ;
builder . Services . AddMediatR ( config =>
{
config . RegisterServicesFromAssembly ( assembly );
config . AddOpenBehavior ( typeof ( ValidationBehavior <,>));
config . AddOpenBehavior ( typeof ( LoggingBehavior <,>));
});
Request Flow with Behaviors
Benefits of CQRS
Scalability Read and write sides can be scaled independently
Optimization Optimize reads separately from writes (e.g., denormalized read models)
Simplicity Each handler has a single responsibility
Testability Handlers are easy to unit test in isolation
Security Different permissions for commands vs queries
Performance Can use different data stores for reads and writes
CQRS Variations
Simple CQRS (Used in AspNetRun)
Characteristics:
Same database for reads and writes
Synchronous updates
Simple to implement
Good for most applications
Advanced CQRS with Separate Models
Characteristics:
Separate databases for reads and writes
Eventual consistency
Maximum scalability
More complex to implement
Best Practices
Commands should be task-based
// Good: Task-based command name
public record CreateOrderCommand ( OrderDto Order ) : ICommand < CreateOrderResult >;
// Bad: CRUD-based name
public record InsertOrderCommand ( OrderDto Order ) : ICommand < CreateOrderResult >;
Queries should never modify state
// Good: Read-only query
public async Task < GetOrdersResult > Handle ( GetOrdersQuery query , CancellationToken ct )
{
return await dbContext . Orders . AsNoTracking (). ToListAsync ( ct );
}
// Bad: Query that modifies state
public async Task < GetOrdersResult > Handle ( GetOrdersQuery query , CancellationToken ct )
{
var orders = await dbContext . Orders . ToListAsync ( ct );
orders [ 0 ]. Status = OrderStatus . Processing ; // DON'T DO THIS!
await dbContext . SaveChangesAsync ();
return orders ;
}
// Define validator for command
public class CreateOrderCommandValidator : AbstractValidator < CreateOrderCommand >
{
public CreateOrderCommandValidator ()
{
RuleFor ( x => x . Order . OrderName ). NotEmpty ();
RuleFor ( x => x . Order . CustomerId ). NotNull ();
}
}
// Validation runs automatically via pipeline behavior
Each handler should do one thing well. Don’t put multiple responsibilities in a single handler.
Clean Architecture See how CQRS fits into the Application layer
DDD Principles Learn how CQRS complements Domain-Driven Design
Vertical Slice Compare CQRS implementation in different architectures
Microservices Understand CQRS in distributed systems