Claude
Skills
Sign in
Back

ddd-cqrs-patterns

Included with Lifetime
$97 forever

Domain-Driven Design and CQRS patterns for .NET microservices including aggregates, value objects, domain events, repositories, and application layer design

Design

What this skill does


# Domain-Driven Design & CQRS Patterns

## DDD Layered Architecture

```
┌──────────────────────────────────────────┐
│           Presentation Layer              │
│  (Blazor Web App / API Controllers)       │
├──────────────────────────────────────────┤
│           Application Layer               │
│  (Commands, Queries, Handlers, DTOs)      │
│  (MediatR, Validation, Mapping)           │
├──────────────────────────────────────────┤
│            Domain Layer                   │
│  (Entities, Value Objects, Aggregates)    │
│  (Domain Events, Domain Services)         │
│  (Repository Interfaces, Specifications)  │
├──────────────────────────────────────────┤
│         Infrastructure Layer              │
│  (EF Core, Repositories, Event Bus)       │
│  (External Services, Messaging)           │
└──────────────────────────────────────────┘
```

**Key rule**: Dependencies point inward. Domain has zero external dependencies.

## Entity Base Class

```csharp
public abstract class Entity
{
    private int? _requestedHashCode;
    private int _id;

    public virtual int Id
    {
        get => _id;
        protected set => _id = value;
    }

    private readonly List<INotification> _domainEvents = [];
    public IReadOnlyCollection<INotification> DomainEvents => _domainEvents.AsReadOnly();

    public void AddDomainEvent(INotification eventItem) => _domainEvents.Add(eventItem);
    public void RemoveDomainEvent(INotification eventItem) => _domainEvents.Remove(eventItem);
    public void ClearDomainEvents() => _domainEvents.Clear();

    public bool IsTransient() => Id == default;

    public override bool Equals(object? obj)
    {
        if (obj is not Entity other) return false;
        if (ReferenceEquals(this, other)) return true;
        if (GetType() != other.GetType()) return false;
        if (other.IsTransient() || IsTransient()) return false;
        return Id == other.Id;
    }

    public override int GetHashCode()
    {
        if (!IsTransient())
        {
            _requestedHashCode ??= Id.GetHashCode() ^ 31;
            return _requestedHashCode.Value;
        }
        return base.GetHashCode();
    }

    public static bool operator ==(Entity? left, Entity? right) => Equals(left, right);
    public static bool operator !=(Entity? left, Entity? right) => !Equals(left, right);
}
```

## Value Objects

```csharp
public abstract class ValueObject
{
    protected abstract IEnumerable<object?> GetEqualityComponents();

    public override bool Equals(object? obj)
    {
        if (obj is null || obj.GetType() != GetType()) return false;
        var other = (ValueObject)obj;
        return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
    }

    public override int GetHashCode() =>
        GetEqualityComponents()
            .Select(x => x?.GetHashCode() ?? 0)
            .Aggregate((x, y) => x ^ y);

    public static bool operator ==(ValueObject? left, ValueObject? right) =>
        Equals(left, right);
    public static bool operator !=(ValueObject? left, ValueObject? right) =>
        !Equals(left, right);
}

// Example: Address value object
public sealed class Address : ValueObject
{
    public string Street { get; }
    public string City { get; }
    public string State { get; }
    public string Country { get; }
    public string ZipCode { get; }

    public Address(string street, string city, string state, string country, string zipCode)
    {
        Street = street;
        City = city;
        State = state;
        Country = country;
        ZipCode = zipCode;
    }

    protected override IEnumerable<object?> GetEqualityComponents()
    {
        yield return Street;
        yield return City;
        yield return State;
        yield return Country;
        yield return ZipCode;
    }
}

// Example: Money value object
public sealed class Money : ValueObject
{
    public decimal Amount { get; }
    public string Currency { get; }

    public Money(decimal amount, string currency)
    {
        if (amount < 0) throw new ArgumentException("Amount cannot be negative");
        Amount = amount;
        Currency = currency ?? throw new ArgumentNullException(nameof(currency));
    }

    public Money Add(Money other)
    {
        if (Currency != other.Currency) throw new InvalidOperationException("Cannot add different currencies");
        return new Money(Amount + other.Amount, Currency);
    }

    protected override IEnumerable<object?> GetEqualityComponents()
    {
        yield return Amount;
        yield return Currency;
    }
}
```

## Aggregate Root

```csharp
public abstract class AggregateRoot : Entity, IAggregateRoot { }

// Example: Order aggregate
public sealed class Order : AggregateRoot
{
    private readonly List<OrderItem> _orderItems = [];
    public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly();

    public DateTime OrderDate { get; private set; }
    public Address ShippingAddress { get; private set; } = null!;
    public OrderStatus Status { get; private set; }
    public int? BuyerId { get; private set; }

    // Private constructor for EF Core
    private Order() { }

    // Factory method enforces invariants
    public Order(int buyerId, Address shippingAddress)
    {
        BuyerId = buyerId;
        ShippingAddress = shippingAddress ?? throw new ArgumentNullException(nameof(shippingAddress));
        Status = OrderStatus.Submitted;
        OrderDate = DateTime.UtcNow;

        // Raise domain event
        AddDomainEvent(new OrderStartedDomainEvent(this, buyerId));
    }

    public void AddOrderItem(int productId, string productName, decimal unitPrice, int units)
    {
        var existingItem = _orderItems.SingleOrDefault(o => o.ProductId == productId);

        if (existingItem is not null)
        {
            existingItem.AddUnits(units);
        }
        else
        {
            var orderItem = new OrderItem(productId, productName, unitPrice, units);
            _orderItems.Add(orderItem);
        }
    }

    public void SetShippedStatus()
    {
        if (Status != OrderStatus.Paid)
            throw new OrderingDomainException("Cannot ship an order that is not paid.");

        Status = OrderStatus.Shipped;
        AddDomainEvent(new OrderShippedDomainEvent(this));
    }

    public void SetPaidStatus()
    {
        if (Status != OrderStatus.Submitted)
            throw new OrderingDomainException("Cannot pay for an order that is not submitted.");

        Status = OrderStatus.Paid;
        AddDomainEvent(new OrderPaidDomainEvent(Id));
    }

    public void SetCancelledStatus()
    {
        if (Status == OrderStatus.Shipped)
            throw new OrderingDomainException("Cannot cancel a shipped order.");

        Status = OrderStatus.Cancelled;
        AddDomainEvent(new OrderCancelledDomainEvent(this));
    }

    public decimal GetTotal() => _orderItems.Sum(i => i.UnitPrice * i.Units);
}
```

## Enumeration Class (Smart Enum)

```csharp
public abstract class Enumeration : IComparable
{
    public string Name { get; }
    public int Id { get; }

    protected Enumeration(int id, string name) => (Id, Name) = (id, name);

    public override string ToString() => Name;

    public static IEnumerable<T> GetAll<T>() where T : Enumeration =>
        typeof(T).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)
            .Select(f => f.GetValue(null))
            .Cast<T>();

    public int CompareTo(object? other) => Id.CompareTo(((Enumeration)other!).Id);
}

public sealed class OrderStatus : Enumeration
{
    public static readonly OrderStatus Submitted = new(1, nameof(Submitted));
    public static readonly OrderStatus AwaitingValidation = new(2, nameof(AwaitingValidation));
    public static readonly OrderStatus StockConfirmed = new(3, nameof(StockConfirmed));
    public static readonly OrderStatus Paid = new(4, nameof(Paid));
    public static readonly OrderStatus Shipped = new(5, nameof(Shipped));
    public

Related in Design