🎯 Sprint 1 - Task 2: Implement Domain Entities with Rich Business Logic
Epic: #121
Sprint: 1 (2 weeks)
Estimated Effort: 4 days
Depends On: #122 (Project Structure)
📋 Description
Transform the current anemic data models into rich domain entities with encapsulated business logic. This includes implementing the BaseEntity pattern, value objects, and moving business rules from services into the domain layer.
🎯 Current State Analysis
Existing Anemic Models (Problems to Fix)
// Current: Anemic models with no business logic
public class Book
{
public int Id { get; set; } // Should be long Id
public string Title { get; set; } // No validation
public string Slug { get; set; } // No slug generation logic
public bool IsDeleted { get; set; } // No audit trail
// ... other properties with no encapsulation
}
Target Rich Domain Entities
// Target: Rich domain entities with business logic
public class Book : BaseEntity
{
private Book() { } // Private constructor for EF
public static Book Create(string title, string description, Category category)
{
// Business logic for creation
// Validation rules
// Domain events
}
public void UpdateDetails(string title, string description)
{
// Business rules for updates
// Domain events
}
public void AddAuthor(Author author)
{
// Business rules for author addition
// Prevent duplicates, validate constraints
}
}
✅ Acceptance Criteria
1. BaseEntity Implementation
2. Value Objects
3. Rich Domain Entities
Book Entity
Author Entity
Category Entity
4. Domain Interfaces
🏗️ Implementation Details
BaseEntity Structure
namespace Refhub.Domain.Common;
public abstract class BaseEntity : IEquatable<BaseEntity>
{
public long Id { get; protected set; }
public DateTime CreatedAt { get; protected set; }
public DateTime? UpdatedAt { get; protected set; }
public string? CreatedBy { get; protected set; }
public string? UpdatedBy { get; protected set; }
private readonly List<IDomainEvent> _domainEvents = new();
public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
protected void AddDomainEvent(IDomainEvent domainEvent)
{
_domainEvents.Add(domainEvent);
}
public void ClearDomainEvents()
{
_domainEvents.Clear();
}
public bool Equals(BaseEntity? other)
{
return other is not null && Id == other.Id && GetType() == other.GetType();
}
public override bool Equals(object? obj)
{
return obj is BaseEntity entity && Equals(entity);
}
public override int GetHashCode()
{
return HashCode.Combine(Id, GetType());
}
public static bool operator ==(BaseEntity? left, BaseEntity? right)
{
return Equals(left, right);
}
public static bool operator !=(BaseEntity? left, BaseEntity? right)
{
return !Equals(left, right);
}
}
Value Object Example: Slug
namespace Refhub.Domain.ValueObjects;
public sealed class Slug : ValueObject
{
public string Value { get; }
private Slug(string value)
{
Value = value;
}
public static Slug Create(string input)
{
if (string.IsNullOrWhiteSpace(input))
throw new ArgumentException("Slug cannot be empty", nameof(input));
var slug = GenerateSlug(input);
return new Slug(slug);
}
private static string GenerateSlug(string input)
{
// Persian/English slug generation logic
return input.ToLowerInvariant()
.Replace(" ", "-")
.RemoveSpecialCharacters()
.Trim('-');
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Value;
}
public static implicit operator string(Slug slug) => slug.Value;
}
Rich Book Entity Example
namespace Refhub.Domain.Entities;
public class Book : BaseEntity, ISoftDeletable
{
private readonly List<BookAuthor> _bookAuthors = new();
private readonly List<BookKeyword> _bookKeywords = new();
public Title Title { get; private set; }
public Slug Slug { get; private set; }
public PageCount PageCount { get; private set; }
public FilePath? PdfFilePath { get; private set; }
public FilePath? ImagePath { get; private set; }
public string? Description { get; private set; }
public long CategoryId { get; private set; }
public string? UserId { get; private set; }
public bool IsDeleted { get; private set; }
// Navigation properties
public Category Category { get; private set; } = null!;
public ApplicationUser? User { get; private set; }
public IReadOnlyList<BookAuthor> BookAuthors => _bookAuthors.AsReadOnly();
public IReadOnlyList<BookKeyword> BookKeywords => _bookKeywords.AsReadOnly();
private Book() { } // EF Core constructor
public static Book Create(
string title,
string description,
long categoryId,
int pageCount,
string? userId = null)
{
var book = new Book
{
Title = Title.Create(title),
Slug = Slug.Create(title),
Description = description,
CategoryId = categoryId,
PageCount = PageCount.Create(pageCount),
UserId = userId,
CreatedAt = DateTime.UtcNow
};
book.AddDomainEvent(new BookCreatedEvent(book));
return book;
}
public void UpdateDetails(string title, string description, long categoryId, int pageCount)
{
Guard.Against.NullOrEmpty(title, nameof(title));
Guard.Against.Negative(pageCount, nameof(pageCount));
Title = Title.Create(title);
Slug = Slug.Create(title); // Regenerate slug
Description = description;
CategoryId = categoryId;
PageCount = PageCount.Create(pageCount);
UpdatedAt = DateTime.UtcNow;
AddDomainEvent(new BookUpdatedEvent(this));
}
public void AddAuthor(long authorId)
{
if (_bookAuthors.Any(ba => ba.AuthorId == authorId))
return; // Already exists
var bookAuthor = BookAuthor.Create(Id, authorId);
_bookAuthors.Add(bookAuthor);
AddDomainEvent(new AuthorAddedToBookEvent(Id, authorId));
}
public void RemoveAuthor(long authorId)
{
var bookAuthor = _bookAuthors.FirstOrDefault(ba => ba.AuthorId == authorId);
if (bookAuthor != null)
{
_bookAuthors.Remove(bookAuthor);
AddDomainEvent(new AuthorRemovedFromBookEvent(Id, authorId));
}
}
public void SetPdfFile(string filePath)
{
PdfFilePath = FilePath.Create(filePath);
UpdatedAt = DateTime.UtcNow;
}
public void SetImage(string imagePath)
{
ImagePath = FilePath.Create(imagePath);
UpdatedAt = DateTime.UtcNow;
}
public void SoftDelete()
{
IsDeleted = true;
UpdatedAt = DateTime.UtcNow;
AddDomainEvent(new BookDeletedEvent(this));
}
}
🔧 Implementation Steps
Day 1: Base Infrastructure
- Create Common Classes
BaseEntity abstract class
ValueObject abstract class
IDomainEvent interface
ISoftDeletable interface
Guard static class for validations
Day 2: Value Objects
- Implement Value Objects
Slug with Persian/English support
Title with validation rules
PageCount with range validation
FilePath with path validation
Day 3: Domain Entities
-
Book Entity
- Rich Book entity with business logic
- Factory methods and business methods
- Domain events integration
-
Author Entity
- Rich Author entity
- Slug generation and validation
- Business methods
Day 4: Remaining Entities & Events
-
Category Entity
- Hierarchical category support
- Business logic implementation
-
Domain Events
BookCreatedEvent, BookUpdatedEvent
AuthorCreatedEvent, AuthorUpdatedEvent
CategoryCreatedEvent
-
Domain Interfaces
- Repository interfaces with domain methods
- Domain service interfaces
📝 Domain Events to Implement
public record BookCreatedEvent(Book Book) : IDomainEvent;
public record BookUpdatedEvent(Book Book) : IDomainEvent;
public record BookDeletedEvent(Book Book) : IDomainEvent;
public record AuthorAddedToBookEvent(long BookId, long AuthorId) : IDomainEvent;
public record AuthorRemovedFromBookEvent(long BookId, long AuthorId) : IDomainEvent;
🎯 Testing Criteria
🔗 Related Tasks
📚 Resources
Sprint: 1 | Assignee: @hootanht | Priority: High | Size: Large
🎯 Sprint 1 - Task 2: Implement Domain Entities with Rich Business Logic
Epic: #121
Sprint: 1 (2 weeks)
Estimated Effort: 4 days
Depends On: #122 (Project Structure)
📋 Description
Transform the current anemic data models into rich domain entities with encapsulated business logic. This includes implementing the BaseEntity pattern, value objects, and moving business rules from services into the domain layer.
🎯 Current State Analysis
Existing Anemic Models (Problems to Fix)
Target Rich Domain Entities
✅ Acceptance Criteria
1. BaseEntity Implementation
BaseEntityabstract class with audit fieldsISoftDeletableinterface for soft delete patternIDomainEventsupport for domain eventslonginstead ofintfor primary keys (better scalability)2. Value Objects
Slugvalue object with validation and generation logicFilePathvalue object for file path validationPageCountvalue object with range validationTitlevalue object with length and content validation3. Rich Domain Entities
Book Entity
Book.Create()with validationUpdateDetails(),AddAuthor(),RemoveAuthor()BookCreated,BookUpdated,AuthorAddedAuthor Entity
Author.Create()with validationUpdateProfile(),AddExpertise()AuthorCreated,AuthorUpdatedCategory Entity
Category.Create()with validationUpdateDetails(),AddSubCategory()CategoryCreated,CategoryUpdated4. Domain Interfaces
IBookRepositorywith domain-specific methodsIAuthorRepositorywith domain-specific methodsICategoryRepositorywith domain-specific methodsISlugGeneratorfor slug generation logic🏗️ Implementation Details
BaseEntity Structure
Value Object Example: Slug
Rich Book Entity Example
🔧 Implementation Steps
Day 1: Base Infrastructure
BaseEntityabstract classValueObjectabstract classIDomainEventinterfaceISoftDeletableinterfaceGuardstatic class for validationsDay 2: Value Objects
Slugwith Persian/English supportTitlewith validation rulesPageCountwith range validationFilePathwith path validationDay 3: Domain Entities
Book Entity
Author Entity
Day 4: Remaining Entities & Events
Category Entity
Domain Events
BookCreatedEvent,BookUpdatedEventAuthorCreatedEvent,AuthorUpdatedEventCategoryCreatedEventDomain Interfaces
📝 Domain Events to Implement
🎯 Testing Criteria
🔗 Related Tasks
📚 Resources
Sprint: 1 | Assignee: @hootanht | Priority: High | Size: Large