1using MediatR;
2
3// Command — intent to change state, returns result
4public record CreateProductCommand(string Name, decimal Price, int Stock)
5 : IRequest<Guid>;
6
7// Query — read data, no side effects
8public record GetProductQuery(Guid Id) : IRequest<ProductDto>;
9
10// Command Handler — validates, creates, persists
11public class CreateProductHandler : IRequestHandler<CreateProductCommand, Guid>
12{
13 private readonly IProductRepository _repo;
14 private readonly IValidator<CreateProductCommand> _validator;
15
16 public async Task<Guid> Handle(CreateProductCommand cmd, CancellationToken ct)
17 {
18 await _validator.ValidateAndThrowAsync(cmd, ct);
19 var product = Product.Create(cmd.Name, cmd.Price, cmd.Stock);
20 await _repo.AddAsync(product, ct);
21 return product.Id;
22 }
23}
24
25// Query Handler — reads from optimised read model
26public class GetProductHandler : IRequestHandler<GetProductQuery, ProductDto>
27{
28 private readonly IReadDbContext _readDb;
29
30 public async Task<ProductDto> Handle(GetProductQuery q, CancellationToken ct)
31 => await _readDb.Products
32 .Where(p => p.Id == q.Id)
33 .Select(p => new ProductDto(p.Id, p.Name, p.Price))
34 .FirstOrDefaultAsync(ct)
35 ?? throw new NotFoundException(nameof(Product), q.Id);
36}
37
38// Controller uses MediatR — no direct dependency on handlers
39[ApiController, Route("products")]
40public class ProductsController(IMediator mediator) : ControllerBase
41{
42 [HttpPost]
43 public async Task<IActionResult> Create(CreateProductCommand cmd)
44 => Ok(await mediator.Send(cmd));
45
46 [HttpGet("{id}")]
47 public async Task<ProductDto> Get(Guid id)
48 => await mediator.Send(new GetProductQuery(id));
49}