Claude
Skills
Sign in
Back

ddd-dotnet

Included with Lifetime
$97 forever

# Domain-Driven Design (DDD) in .NET - Basics & Patterns

Design

What this skill does

# Domain-Driven Design (DDD) in .NET - Basics & Patterns

## Overview

This skill provides a comprehensive guide to implementing Domain-Driven Design patterns in .NET, based on real-world production code from the LMP project. These patterns promote clean architecture, maintainability, and testability.

### Key Benefits
- **Clear Separation of Concerns**: Domain logic isolated from infrastructure
- **CQRS Pattern**: Optimized read and write operations
- **Hexagonal Architecture**: Ports (domain) and Adapters (infrastructure)
- **Testability**: Domain logic testable without dependencies
- **Time Control**: Deterministic time handling for testing

---

## Core Building Blocks

### 1. Domain Object Hierarchy

All domain objects implement a marker interface at the root:

```csharp
namespace Pikot.LMP.Domain.Common;

public interface IDomainObject;
```

### 2. Aggregates

Aggregates are consistency boundaries and entry points for domain operations.

```csharp
namespace Pikot.LMP.Domain.Common;

public interface IAggregate : IDomainObject
{
    int Id { get; }
    DateTimeOffset CreatedAt { get; }
    DateTimeOffset InsertedAt { get; }
}
```

**Versioned Aggregates** track changes over time:

```csharp
namespace Pikot.LMP.Domain.Common;

public interface IAggregateVersioned : IAggregate
{
    int Version { get; }
}
```

**Key Principles:**
- Aggregates are the only objects that can be retrieved from repositories
- They maintain invariants and enforce business rules
- They control access to their entities
- Use `CreatedAt` for business time, `InsertedAt` for audit/database time

### 3. Entities

Entities are objects with identity that belong to aggregates:

```csharp
namespace Pikot.LMP.Domain.Common;

public interface IEntity : IDomainObject
{
    int Id { get; }
    DateTimeOffset CreatedAt { get; }
    DateTimeOffset InsertedAt { get; }
}
```

**Example Entity:**

```csharp
using Pikot.LMP.Domain.Common;

namespace Pikot.LMP.Domain.Shipment.ShipperOrders;

public class Package : IEntity
{
    protected Package() { } // For EF Core

    public Package(decimal kg, DateTimeOffset createdAt, DateTimeOffset insertedAt, byte numeration)
    {
        Guid = Guid.NewGuid();
        Kg = kg;
        CreatedAt = createdAt;
        InsertedAt = insertedAt;
        Numeration = numeration;
        Status = PackageStatus.Pending;
    }

    public int Id { get; protected init; }
    public Guid Guid { get; protected init; }
    public DateTimeOffset CreatedAt { get; }
    public DateTimeOffset InsertedAt { get; }
    public decimal Kg { get; private set; }
    public byte Numeration { get; private set; }
    public PackageStatus Status { get; private set; }
    public int ShipperOrderId { get; protected init; }

    // Business logic methods
    public void UpdateWeight(decimal newKg)
    {
        Kg = newKg;
    }

    public void SetNumeration(byte numeration)
    {
        Numeration = numeration;
    }

    public void SetStatus(PackageStatus status)
    {
        Status = status;
    }
}
```

---

## Aggregate Root Pattern

**Example: ShipperOrder Aggregate**

```csharp
using Pikot.LMP.Domain.Common;
using Pikot.LMP.Domain.Common.Exceptions;

namespace Pikot.LMP.Domain.Shipment.ShipperOrders;

public class ShipperOrder : IAggregateVersioned
{
    protected ShipperOrder() { } // For EF Core

    public ShipperOrder(DateTimeOffset createdAt, DateTimeOffset insertedAt,
        int shipperId, string shipperCompanyName, /* ... other params */)
    {
        Guid = Guid.NewGuid();
        CreatedAt = createdAt;
        InsertedAt = insertedAt;
        Version = 1;

        // Initialize collections
        _changeEvents.Add(new ShipperOrderChangeEvent(/* ... */));
        _versions.Add(new ShipperOrderVersion(/* ... */));
    }

    // Properties
    public int Id { get; protected init; }
    public Guid Guid { get; protected init; }
    public int Version { get; private set; }
    public DateTimeOffset CreatedAt { get; }
    public DateTimeOffset InsertedAt { get; }
    public ShipperOrderStatusType Status { get; private set; }

    // Collections - private backing fields with public readonly access
    private readonly List<ShipperOrderChangeEvent> _changeEvents = [];
    public IReadOnlyCollection<ShipperOrderChangeEvent> Changes
    {
        get => _changeEvents;
        init => _changeEvents = value.ToList(); // For EF Core materialization
    }

    private readonly List<Package> _packages = [];
    public IReadOnlyCollection<Package> Packages
    {
        get => _packages.OrderBy(p => p.Numeration).ToList();
        init => _packages = value.ToList();
    }

    // Business methods with validation
    public void Pickup(int dispatcherUserId, DateTimeOffset timestamp, IClock clock,
        int dispatcherId, string dispatcherCompanyName)
    {
        if (Status is not (ShipperOrderStatusType.Created or ShipperOrderStatusType.Ready))
        {
            throw new InvalidTransitionException();
        }

        Status = ShipperOrderStatusType.PickedUp;
        DispatcherId = dispatcherId;
        DispatcherCompanyName = dispatcherCompanyName;

        foreach (var package in _packages)
        {
            package.SetStatus(PackageStatus.OutForDelivery);
        }

        var change = new ShipperOrderChangeEvent(timestamp, clock.Now, dispatcherUserId,
            ShipperOrderStatusType.PickedUp, ShipperOrderChangeEventType.StatusChanged,
            ShipperOrderChangeEventSourceType.Dispatcher);
        _changeEvents.Add(change);
    }

    public Package AddPackage(decimal kg, DateTimeOffset createdAt, DateTimeOffset insertedAt)
    {
        var numeration = (byte)(_packages.Count + 1);
        var package = new Package(kg, createdAt, insertedAt, numeration);
        _packages.Add(package);
        return package;
    }

    public void RemovePackage(int packageId)
    {
        var package = _packages.FirstOrDefault(p => p.Id == packageId);
        if (package == null)
            throw new NotFoundException("Package not found");

        _packages.Remove(package);
        RenumberPackages();
    }

    private void RenumberPackages()
    {
        byte numeration = 1;
        foreach (var package in _packages.OrderBy(p => p.Numeration))
        {
            package.SetNumeration(numeration);
            numeration++;
        }
    }
}
```

**Key Patterns:**
- Protected parameterless constructor for EF Core
- Public constructor with all required data
- Private setters on properties
- Private backing fields for collections with `IReadOnlyCollection<T>` exposure
- Business logic encapsulated in methods
- Validation within business methods
- Track domain events in collections

---

## Repository Pattern (CQRS)

### Write Repository

```csharp
namespace Pikot.LMP.Domain.Common;

public interface IRepository<TAggregate, in TUniqueKey> where TAggregate : class, IAggregate
{
    Task<TAggregate?> GetAsync(int id, CancellationToken cancellationToken);
    Task<bool> ExistsAsync(TUniqueKey key, CancellationToken cancellationToken);
    Task SaveChangesAsync(CancellationToken cancellationToken);
    void Add(TAggregate entity);
}
```

### Query Repository

```csharp
namespace Pikot.LMP.Domain.Common;

public interface IQueryRepository<TAggregate> where TAggregate : class, IAggregate
{
    Task<TAggregate?> GetAsync(int id, CancellationToken cancellationToken);
}
```

**Usage:**
- **Write operations**: Use `IRepository<TAggregate, TUniqueKey>` in command handlers
- **Read operations**: Use `IQueryRepository<TAggregate>` in query handlers
- Write repositories can check existence and add entities
- Query repositories are simpler and optimized for reads
- In command handlers where you need to load(read) then update the aggregate and save, use the write repository to load the aggregate. The query repository does not track the entity and the save on the write repository will do nothing if the aggregate is loaded with the query repository.

### Specific Repository In
Files: 3
Size: 27.8 KB
Complexity: 32/100
Category: Design

Related in Design