π― 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
2. Command Infrastructure
3. Query Infrastructure
4. Book Domain Commands
5. Book Domain Queries
6. Author Domain Operations
7. Category Domain Operations
ποΈ 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
-
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" />
-
Create Base Interfaces and Classes
ICommand, IQuery interfaces
- Base command/query handler classes
- Pipeline behaviors for validation, logging
Day 2: Commands Infrastructure
-
Command Pattern Implementation
- Create command base classes
- Implement validation pipeline
- Setup command handlers registration
-
Book Commands
CreateBookCommand and handler
UpdateBookCommand and handler
- Command validators
Day 3: Queries Infrastructure
-
Query Pattern Implementation
- Create query base classes
- Implement query handlers
- Setup AutoMapper profiles
-
Book Queries
GetBookBySlugQuery and handler
GetBookListQuery with pagination
- Query DTOs
Day 4: Author & Category Operations
-
Author CQRS
- Author commands and queries
- Author DTOs and validators
-
Category CQRS
- Category commands and queries
- Hierarchical category support
Day 5: Controller Integration & Testing
-
Update Controllers
- Inject
IMediator instead of services
- Update controller actions to use commands/queries
- Remove old service dependencies
-
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
π Related Tasks
π Resources
Sprint: 1 | Assignee: @hootanht | Priority: High | Size: Extra Large
π― 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)
Target CQRS Structure
β Acceptance Criteria
1. MediatR Infrastructure Setup
2. Command Infrastructure
ICommandandICommand<TResponse>interfaces3. Query Infrastructure
IQuery<TResponse>interface4. Book Domain Commands
CreateBookCommandand handlerUpdateBookCommandand handlerDeleteBookCommandand handlerAddAuthorToBookCommandand handlerRemoveAuthorFromBookCommandand handlerUploadBookFileCommandand handler5. Book Domain Queries
GetBookByIdQueryand handlerGetBookBySlugQueryand handlerGetBookListQueryand handler with filteringGetBooksByAuthorQueryand handlerGetBooksByCategoryQueryand handler6. Author Domain Operations
CreateAuthorCommand,UpdateAuthorCommand,DeleteAuthorCommandGetAuthorByIdQuery,GetAuthorBySlugQuery,GetAuthorsListQuery7. Category Domain Operations
CreateCategoryCommand,UpdateCategoryCommand,DeleteCategoryCommandGetCategoryByIdQuery,GetCategoriesListQuery,GetCategoryHierarchyQueryποΈ Implementation Details
MediatR Configuration
Base Interfaces
Command Example: CreateBookCommand
Query Example: GetBookBySlugQuery
DTOs for Clean Separation
π§ Implementation Steps
Day 1: Infrastructure Setup
Install NuGet Packages
Create Base Interfaces and Classes
ICommand,IQueryinterfacesDay 2: Commands Infrastructure
Command Pattern Implementation
Book Commands
CreateBookCommandand handlerUpdateBookCommandand handlerDay 3: Queries Infrastructure
Query Pattern Implementation
Book Queries
GetBookBySlugQueryand handlerGetBookListQuerywith paginationDay 4: Author & Category Operations
Author CQRS
Category CQRS
Day 5: Controller Integration & Testing
Update Controllers
IMediatorinstead of servicesTesting
π Pipeline Behaviors
Validation Behavior
π― Testing Criteria
π Related Tasks
π Resources
Sprint: 1 | Assignee: @hootanht | Priority: High | Size: Extra Large