Skip to content

⚑ 3. Setup CQRS Pattern with MediatR #124

@hootanht

Description

@hootanht

🎯 Sprint 1 - Task 3: Setup CQRS Pattern with MediatR

Epic: #121
Sprint: 1 (2 weeks)
Estimated Effort: 5 days
Depends On: #122 (Project Structure), #123 (Domain Entities)

πŸ“‹ Description

Implement the Command Query Responsibility Segregation (CQRS) pattern using MediatR to separate read and write operations. This will replace the current service layer with a more scalable and maintainable approach.

🎯 Current State vs Target

Current Service Layer (Problems)

// Current: Monolithic service with mixed responsibilities
public class BookService : IBookService
{
    // 17 methods mixing commands and queries
    Task<bool> CreateBookAsync(CreateBookVM book, CancellationToken ct);
    Task<BookDetailsVM> GetBookDetailsBySlugAsync(string slug, CancellationToken ct);
    Task<ListBooksVM> GetListAsync(string searchText, ...);
    // ... more mixed operations
}

Target CQRS Structure

// Commands (Write Operations)
public record CreateBookCommand(string Title, string Description, long CategoryId) : IRequest<long>;
public record UpdateBookCommand(long Id, string Title, string Description) : IRequest;
public record DeleteBookCommand(long Id) : IRequest;

// Queries (Read Operations)  
public record GetBookByIdQuery(long Id) : IRequest<BookDto>;
public record GetBookBySlugQuery(string Slug) : IRequest<BookDetailsDto>;
public record GetBookListQuery(string? SearchText, int Page, int PageSize) : IRequest<PagedList<BookDto>>;

βœ… Acceptance Criteria

1. MediatR Infrastructure Setup

  • Install and configure MediatR in Application and Web projects
  • Setup dependency injection for MediatR
  • Create pipeline behaviors for cross-cutting concerns
  • Implement request/response logging

2. Command Infrastructure

  • Create base ICommand and ICommand<TResponse> interfaces
  • Implement command handlers base class
  • Setup command validation pipeline
  • Implement domain event dispatch after commands

3. Query Infrastructure

  • Create base IQuery<TResponse> interface
  • Implement query handlers base class
  • Setup query caching pipeline (future)
  • Implement pagination support

4. Book Domain Commands

  • CreateBookCommand and handler
  • UpdateBookCommand and handler
  • DeleteBookCommand and handler
  • AddAuthorToBookCommand and handler
  • RemoveAuthorFromBookCommand and handler
  • UploadBookFileCommand and handler

5. Book Domain Queries

  • GetBookByIdQuery and handler
  • GetBookBySlugQuery and handler
  • GetBookListQuery and handler with filtering
  • GetBooksByAuthorQuery and handler
  • GetBooksByCategoryQuery and handler

6. Author Domain Operations

  • CreateAuthorCommand, UpdateAuthorCommand, DeleteAuthorCommand
  • GetAuthorByIdQuery, GetAuthorBySlugQuery, GetAuthorsListQuery

7. Category Domain Operations

  • CreateCategoryCommand, UpdateCategoryCommand, DeleteCategoryCommand
  • GetCategoryByIdQuery, GetCategoriesListQuery, GetCategoryHierarchyQuery

πŸ—οΈ Implementation Details

MediatR Configuration

// In Refhub.Application/DependencyInjection.cs
public static class DependencyInjection
{
    public static IServiceCollection AddApplication(this IServiceCollection services)
    {
        services.AddMediatR(cfg =>
        {
            cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
        });

        services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
        services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
        services.AddTransient(typeof(IPipelineBehavior<,>), typeof(PerformanceBehavior<,>));

        services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());

        return services;
    }
}

Base Interfaces

// Refhub.Application/Common/ICommand.cs
public interface ICommand : IRequest { }
public interface ICommand<out TResponse> : IRequest<TResponse> { }

// Refhub.Application/Common/IQuery.cs  
public interface IQuery<out TResponse> : IRequest<TResponse> { }

Command Example: CreateBookCommand

// Refhub.Application/Commands/Books/CreateBookCommand.cs
public record CreateBookCommand(
    string Title,
    string Description,
    long CategoryId,
    int PageCount,
    List<long> AuthorIds,
    IFormFile? PdfFile,
    IFormFile? ImageFile,
    string? UserId) : ICommand<long>;

// Refhub.Application/Commands/Books/CreateBookCommandHandler.cs
public class CreateBookCommandHandler : IRequestHandler<CreateBookCommand, long>
{
    private readonly IBookRepository _bookRepository;
    private readonly IFileUploadService _fileUploadService;
    private readonly IUnitOfWork _unitOfWork;

    public CreateBookCommandHandler(
        IBookRepository bookRepository,
        IFileUploadService fileUploadService,
        IUnitOfWork unitOfWork)
    {
        _bookRepository = bookRepository;
        _fileUploadService = fileUploadService;
        _unitOfWork = unitOfWork;
    }

    public async Task<long> Handle(CreateBookCommand request, CancellationToken cancellationToken)
    {
        // 1. Create domain entity with business logic
        var book = Book.Create(
            request.Title,
            request.Description,
            request.CategoryId,
            request.PageCount,
            request.UserId);

        // 2. Add authors
        foreach (var authorId in request.AuthorIds)
        {
            book.AddAuthor(authorId);
        }

        // 3. Handle file uploads
        if (request.PdfFile != null)
        {
            var pdfPath = await _fileUploadService.UploadBookPdf(
                request.PdfFile, 
                book.Slug, 
                cancellationToken);
            book.SetPdfFile(pdfPath);
        }

        if (request.ImageFile != null)
        {
            var imagePath = await _fileUploadService.UploadBookImage(
                request.ImageFile, 
                book.Slug, 
                cancellationToken);
            book.SetImage(imagePath);
        }

        // 4. Save to repository
        await _bookRepository.AddAsync(book, cancellationToken);
        await _unitOfWork.SaveChangesAsync(cancellationToken);

        return book.Id;
    }
}

// Refhub.Application/Commands/Books/CreateBookCommandValidator.cs
public class CreateBookCommandValidator : AbstractValidator<CreateBookCommand>
{
    public CreateBookCommandValidator()
    {
        RuleFor(x => x.Title)
            .NotEmpty().WithMessage("Title is required")
            .MaximumLength(200).WithMessage("Title must not exceed 200 characters");

        RuleFor(x => x.Description)
            .MaximumLength(2000).WithMessage("Description must not exceed 2000 characters");

        RuleFor(x => x.CategoryId)
            .GreaterThan(0).WithMessage("Valid category is required");

        RuleFor(x => x.PageCount)
            .GreaterThan(0).WithMessage("Page count must be greater than 0")
            .LessThanOrEqualTo(10000).WithMessage("Page count seems unrealistic");

        RuleFor(x => x.AuthorIds)
            .NotEmpty().WithMessage("At least one author is required")
            .Must(ids => ids.All(id => id > 0)).WithMessage("All author IDs must be valid");
    }
}

Query Example: GetBookBySlugQuery

// Refhub.Application/Queries/Books/GetBookBySlugQuery.cs
public record GetBookBySlugQuery(string Slug) : IQuery<BookDetailsDto?>;

// Refhub.Application/Queries/Books/GetBookBySlugQueryHandler.cs
public class GetBookBySlugQueryHandler : IRequestHandler<GetBookBySlugQuery, BookDetailsDto?>
{
    private readonly IBookRepository _bookRepository;
    private readonly IMapper _mapper;

    public GetBookBySlugQueryHandler(IBookRepository bookRepository, IMapper mapper)
    {
        _bookRepository = bookRepository;
        _mapper = mapper;
    }

    public async Task<BookDetailsDto?> Handle(GetBookBySlugQuery request, CancellationToken cancellationToken)
    {
        var book = await _bookRepository.GetBySlugWithDetailsAsync(request.Slug, cancellationToken);
        
        return book == null ? null : _mapper.Map<BookDetailsDto>(book);
    }
}

DTOs for Clean Separation

// Refhub.Application/DTOs/Books/BookDetailsDto.cs
public class BookDetailsDto
{
    public long Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public string Slug { get; set; } = string.Empty;
    public string? Description { get; set; }
    public int PageCount { get; set; }
    public string? PdfFilePath { get; set; }
    public string? ImagePath { get; set; }
    public DateTime CreatedAt { get; set; }
    public CategoryDto Category { get; set; } = new();
    public List<AuthorDto> Authors { get; set; } = new();
    public List<KeywordDto> Keywords { get; set; } = new();
    public List<BookDto> RelatedBooks { get; set; } = new();
}

πŸ”§ Implementation Steps

Day 1: Infrastructure Setup

  1. Install NuGet Packages

    <!-- Refhub.Application -->
    <PackageReference Include="MediatR" Version="12.4.1" />
    <PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.8.0" />
    <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
  2. Create Base Interfaces and Classes

    • ICommand, IQuery interfaces
    • Base command/query handler classes
    • Pipeline behaviors for validation, logging

Day 2: Commands Infrastructure

  1. Command Pattern Implementation

    • Create command base classes
    • Implement validation pipeline
    • Setup command handlers registration
  2. Book Commands

    • CreateBookCommand and handler
    • UpdateBookCommand and handler
    • Command validators

Day 3: Queries Infrastructure

  1. Query Pattern Implementation

    • Create query base classes
    • Implement query handlers
    • Setup AutoMapper profiles
  2. Book Queries

    • GetBookBySlugQuery and handler
    • GetBookListQuery with pagination
    • Query DTOs

Day 4: Author & Category Operations

  1. Author CQRS

    • Author commands and queries
    • Author DTOs and validators
  2. Category CQRS

    • Category commands and queries
    • Hierarchical category support

Day 5: Controller Integration & Testing

  1. Update Controllers

    • Inject IMediator instead of services
    • Update controller actions to use commands/queries
    • Remove old service dependencies
  2. Testing

    • Unit tests for commands and queries
    • Integration tests for handlers
    • Validation tests

πŸ“ Pipeline Behaviors

Validation Behavior

public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators;
    }

    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
    {
        if (_validators.Any())
        {
            var context = new ValidationContext<TRequest>(request);
            var validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
            var failures = validationResults.SelectMany(r => r.Errors).Where(f => f != null).ToList();

            if (failures.Any())
                throw new ValidationException(failures);
        }

        return await next();
    }
}

🎯 Testing Criteria

  • All commands have unit tests
  • All queries have unit tests
  • Validation behaviors work correctly
  • Controllers use MediatR instead of services
  • Pipeline behaviors execute in correct order
  • Domain events are dispatched correctly

πŸ”— Related Tasks

πŸ“š Resources


Sprint: 1 | Assignee: @hootanht | Priority: High | Size: Extra Large

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions