Skip to content

🎯 2. Implement Domain Entities with Rich Business Logic #123

@hootanht

Description

@hootanht

🎯 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

  • Create BaseEntity abstract class with audit fields
  • Implement ISoftDeletable interface for soft delete pattern
  • Add IDomainEvent support for domain events
  • Use long instead of int for primary keys (better scalability)

2. Value Objects

  • Slug value object with validation and generation logic
  • FilePath value object for file path validation
  • PageCount value object with range validation
  • Title value object with length and content validation

3. Rich Domain Entities

Book Entity

  • Factory method Book.Create() with validation
  • Business methods: UpdateDetails(), AddAuthor(), RemoveAuthor()
  • Domain events: BookCreated, BookUpdated, AuthorAdded
  • Business rules: Title uniqueness per category, slug generation
  • Encapsulated collections for authors, keywords

Author Entity

  • Factory method Author.Create() with validation
  • Business methods: UpdateProfile(), AddExpertise()
  • Domain events: AuthorCreated, AuthorUpdated
  • Business rules: Slug uniqueness, full name validation

Category Entity

  • Factory method Category.Create() with validation
  • Business methods: UpdateDetails(), AddSubCategory()
  • Hierarchical category support
  • Domain events: CategoryCreated, CategoryUpdated

4. Domain Interfaces

  • IBookRepository with domain-specific methods
  • IAuthorRepository with domain-specific methods
  • ICategoryRepository with domain-specific methods
  • ISlugGenerator for slug generation logic

🏗️ 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

  1. Create Common Classes
    • BaseEntity abstract class
    • ValueObject abstract class
    • IDomainEvent interface
    • ISoftDeletable interface
    • Guard static class for validations

Day 2: Value Objects

  1. Implement Value Objects
    • Slug with Persian/English support
    • Title with validation rules
    • PageCount with range validation
    • FilePath with path validation

Day 3: Domain Entities

  1. Book Entity

    • Rich Book entity with business logic
    • Factory methods and business methods
    • Domain events integration
  2. Author Entity

    • Rich Author entity
    • Slug generation and validation
    • Business methods

Day 4: Remaining Entities & Events

  1. Category Entity

    • Hierarchical category support
    • Business logic implementation
  2. Domain Events

    • BookCreatedEvent, BookUpdatedEvent
    • AuthorCreatedEvent, AuthorUpdatedEvent
    • CategoryCreatedEvent
  3. 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

  • All domain entities have unit tests
  • Business rules are tested
  • Domain events are triggered correctly
  • Value objects validate input correctly
  • Factory methods create valid entities
  • Business methods maintain invariants

🔗 Related Tasks

📚 Resources


Sprint: 1 | Assignee: @hootanht | Priority: High | Size: 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