TaskHub.Shared

Domain Modeling & Aggregates

The TaskHub.Shared.Domain module provides the building blocks for implementing Domain-Driven Design (DDD) patterns.

IAggregate & AggregateBase

An Aggregate is a cluster of domain objects that can be treated as a single unit. Every aggregate has a Root, which is the only member of the aggregate that outside objects are allowed to hold a reference to.

Aggregate Lifecycle

  1. Creation: Aggregates are typically created via a constructor or a static factory method. During creation, the aggregate’s initial state is set, and a “Created” domain event is often recorded.
  2. State Mutation: All state changes must happen through methods on the Aggregate Root. These methods ensure that domain invariants are maintained.
  3. Event Collection: As state changes occur, the aggregate records Domain Events. These events represent something significant that happened in the domain.
  4. Persistence: The repository saves the aggregate’s state. After a successful save, the recorded events are typically dispatched to other parts of the system (or outbox).
  5. Clearing Events: Once events are dispatched, they must be cleared from the aggregate to prevent duplicate processing.

AggregateBase Implementation

public abstract class AggregateBase<T>(T id) : IAggregate
{
    public T Id { get; protected set; } = id;
    
    // Internal collection of events
    protected readonly List<IDomainEvent> MyEvents = [];
    
    // Public read-only access
    public IReadOnlyCollection<IDomainEvent> Events => MyEvents.AsReadOnly();
    
    // Method to clear events after dispatch
    public void ClearEvents() => MyEvents.Clear();
}

Complex Aggregate Example

This example demonstrates an aggregate with child entities and invariant checks.

public class Project : AggregateBase<Guid>
{
    public string Name { get; private set; }
    private readonly List<TaskItem> _tasks = new();
    public IReadOnlyCollection<TaskItem> Tasks => _tasks.AsReadOnly();

    public Project(Guid id, string name) : base(id)
    {
        if (string.IsNullOrWhiteSpace(name))
            throw new DomainException(ProjectErrors.InvalidName);

        Name = name;
        MyEvents.Add(new ProjectCreated(id, name));
    }

    public void AddTask(string title, string description)
    {
        if (_tasks.Count >= 100)
            throw new DomainException(ProjectErrors.ProjectFull);

        var task = new TaskItem(Guid.NewGuid(), title, description);
        _tasks.Add(task);
        
        MyEvents.Add(new TaskAddedToProject(Id, task.Id));
    }
}

public class TaskItem(Guid id, string title, string description)
{
    public Guid Id { get; } = id;
    public string Title { get; } = title;
    public string Description { get; } = description;
}

Domain Events

Domain events are simple POCOs that implement IDomainEvent.

public record ProjectCreated(Guid ProjectId, string Name) : DomainEventBase, IDomainEvent;

Events in TaskHub.Shared include metadata like CreatedAt and can be versioned using the [EventVersion] attribute.