CQRS Pattern with MediatR

লেসন ৩৫ / ৫০

CQRS কী?

Command Query Responsibility Segregation — Read (Query) ও Write (Command) আলাদা model/handler-এ থাকে। Complex domain-এ scale ভালো।

MediatR Install

dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection

// Program.cs
builder.Services.AddMediatR(cfg =>
    cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));

Query Example

// Query
public record GetProductByIdQuery(int Id) : IRequest<ProductDto?>;

// Handler
public class GetProductByIdHandler : IRequestHandler<GetProductByIdQuery, ProductDto?> {
    private readonly AppDbContext _ctx;
    public GetProductByIdHandler(AppDbContext ctx) => _ctx = ctx;

    public async Task<ProductDto?> Handle(GetProductByIdQuery req, CancellationToken ct) {
        var p = await _ctx.Products.FindAsync(new object[]{ req.Id }, ct);
        return p == null ? null : new ProductDto(p.Id, p.Name, p.Price);
    }
}

Command Example

public record CreateProductCommand(string Name, decimal Price) : IRequest<int>;

public class CreateProductHandler : IRequestHandler<CreateProductCommand, int> {
    private readonly AppDbContext _ctx;
    public CreateProductHandler(AppDbContext ctx) => _ctx = ctx;

    public async Task<int> Handle(CreateProductCommand req, CancellationToken ct) {
        var p = new Product { Name = req.Name, Price = req.Price };
        _ctx.Products.Add(p);
        await _ctx.SaveChangesAsync(ct);
        return p.Id;
    }
}

Controller — Slim

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase {
    private readonly IMediator _mediator;
    public ProductsController(IMediator m) => _mediator = m;

    [HttpGet("{id}")]
    public async Task<IActionResult> Get(int id) {
        var dto = await _mediator.Send(new GetProductByIdQuery(id));
        return dto == null ? NotFound() : Ok(dto);
    }

    [HttpPost]
    public async Task<IActionResult> Create(CreateProductCommand cmd) {
        var id = await _mediator.Send(cmd);
        return CreatedAtAction(nameof(Get), new { id }, null);
    }
}

Pipeline Behavior (Validation/Logging)

public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> {
    private readonly IEnumerable<IValidator<TRequest>> _validators;
    public ValidationBehavior(IEnumerable<IValidator<TRequest>> v) => _validators = v;

    public async Task<TResponse> Handle(TRequest req, RequestHandlerDelegate<TResponse> next, CancellationToken ct) {
        var ctx = new ValidationContext<TRequest>(req);
        var failures = (await Task.WhenAll(_validators.Select(v => v.ValidateAsync(ctx, ct))))
            .SelectMany(r => r.Errors).Where(f => f != null).ToList();
        if (failures.Any()) throw new ValidationException(failures);
        return await next();
    }
}