TaskHub.Shared

TaskHub.Shared.Domain - Technical Manual

The TaskHub.Shared.Domain module provides the foundational building blocks for implementing a rich, observable domain model following the principles of Domain-Driven Design (DDD). It enforces strong encapsulation, invariant protection, and a reliable event-driven architecture.

🏛 Deep Architecture

1. Aggregate Lifecycle & Internal Event Bus

The AggregateBase<TId> class is the heart of the domain. It manages more than just an identifier:

2. Domain Exception Strategy

TaskHub uses a specialized exception hierarchy to distinguish between Expected Business Rule Violations and Unexpected Technical Failures:


🛠 API Reference

AggregateBase<TId>

| Member | Type | Description | | :— | :— | :— | | Id | TId | The unique, strongly-typed identity of the aggregate. | | Events | IReadOnlyCollection<IDomainEvent> | Public access to captured events (for the persistence layer). | | AddEvent(IDomainEvent event) | void | Captures a new domain event. | | ClearEvents() | void | Resets the event list (called by UnitOfWork after save). |

IDomainEvent

| Property | Type | Description | | :— | :— | :— | | CreatedAt | DateTime | Timestamp of when the event occurred in UTC. |


🚀 Complex Implementation Example

Scenario: A Project Management Aggregate

using TaskHub.Shared.Domain.Aggregates;
using TaskHub.Shared.Domain.Events;
using TaskHub.Shared.Domain.Exceptions;

public class Project : AggregateBase<Guid>
{
    private readonly List<ProjectTask> _tasks = new();

    public Project(Guid id, string name, Guid ownerId) : base(id)
    {
        if (string.IsNullOrWhiteSpace(name)) throw new DomainException("Project name cannot be empty.");
        
        Name = name;
        OwnerId = ownerId;
        Status = ProjectStatus.Active;

        AddEvent(new ProjectCreatedDomainEvent(id, name, ownerId));
    }

    public string Name { get; private set; }
    public Guid OwnerId { get; private set; }
    public ProjectStatus Status { get; private set; }
    public IReadOnlyCollection<ProjectTask> Tasks => _tasks.AsReadOnly();

    public void AddTask(string title, string description)
    {
        if (Status != ProjectStatus.Active)
            throw new DomainException("Cannot add tasks to an inactive project.");

        if (_tasks.Any(t => t.Title == title))
            throw new DomainException($"Task with title '{title}' already exists.");

        var task = new ProjectTask(Guid.NewGuid(), title, description);
        _tasks.Add(task);

        AddEvent(new TaskAddedToProjectDomainEvent(Id, task.Id, title));
    }

    public void Complete()
    {
        if (_tasks.Any(t => !t.IsCompleted))
            throw new DomainException("Cannot complete project while tasks are pending.");

        Status = ProjectStatus.Completed;
        AddEvent(new ProjectCompletedDomainEvent(Id));
    }
}

⚙️ Persistence Integration (EF Core)

The domain model is designed to be mapped seamlessly to EF Core. Use IEntityTypeConfiguration to map the private fields and collections.

public class ProjectConfiguration : IEntityTypeConfiguration<Project>
{
    public void Configure(EntityTypeBuilder<Project> builder)
    {
        builder.HasKey(x => x.Id);
        
        // Map the private list to a backing field
        builder.HasMany(x => x.Tasks)
               .WithOne()
               .HasForeignKey("ProjectId")
               .OnDelete(DeleteBehavior.Cascade);
               
        var navigation = builder.Metadata.FindNavigation(nameof(Project.Tasks));
        navigation?.SetPropertyAccessMode(PropertyAccessMode.Field);
    }
}

👁 Telemetry & Diagnostics

Captured Data

When events are dispatched via the IEventsBus (integrated with the Outbox), the system automatically adds:


✅ Best Practices & Anti-Patterns

🟢 Best Practices

🔴 Anti-Patterns