CQRS & Clean Architecture
Professional .NET codebases use CQRS (Command Query Responsibility Segregation) with MediatR to separate reads from writes, Clean Architecture layers to decouple business logic from infrastructure, and the Result pattern instead of exceptions for flow control.
45 min•By Priygop Team•Last updated: Feb 2026
CQRS with MediatR
Example
// Install: dotnet add package MediatR
// Query (read) — returns data
public record GetProductQuery(int Id) : IRequest<ProductDto?>;
public class GetProductHandler : IRequestHandler<GetProductQuery, ProductDto?> {
private readonly AppDbContext _db;
public GetProductHandler(AppDbContext db) => _db = db;
public async Task<ProductDto?> Handle(GetProductQuery request, CancellationToken ct) =>
await _db.Products
.AsNoTracking()
.Where(p => p.Id == request.Id)
.Select(p => new ProductDto(p.Id, p.Name, p.Price))
.FirstOrDefaultAsync(ct);
}
// Command (write) — changes state
public record CreateProductCommand(string Name, decimal Price) : IRequest<Result<int>>;
public class CreateProductHandler : IRequestHandler<CreateProductCommand, Result<int>> {
private readonly AppDbContext _db;
public CreateProductHandler(AppDbContext db) => _db = db;
public async Task<Result<int>> Handle(CreateProductCommand cmd, CancellationToken ct) {
if (string.IsNullOrWhiteSpace(cmd.Name))
return Result.Failure<int>("Name is required");
if (cmd.Price <= 0)
return Result.Failure<int>("Price must be positive");
var product = new Product { Name = cmd.Name, Price = cmd.Price };
_db.Products.Add(product);
await _db.SaveChangesAsync(ct);
return Result.Success(product.Id);
}
}
// Controller dispatches — thin, no business logic
[ApiController, Route("api/[controller]")]
public class ProductsController : ControllerBase {
private readonly IMediator _mediator;
public ProductsController(IMediator mediator) => _mediator = mediator;
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id) {
var result = await _mediator.Send(new GetProductQuery(id));
return result is null ? NotFound() : Ok(result);
}
[HttpPost]
public async Task<IActionResult> Create(CreateProductCommand cmd) {
var result = await _mediator.Send(cmd);
return result.IsSuccess
? CreatedAtAction(nameof(Get), new { id = result.Value }, null)
: BadRequest(result.Error);
}
}
// Pipeline behaviors (cross-cutting concerns via MediatR)
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse> {
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct) {
// Log, validate, time, authorize — before calling handler
return await next();
}
}