dotnet-ddd
9
总安装量
9
周安装量
#31858
全站排名
安装命令
npx skills add https://github.com/baotoq/micro-commerce --skill dotnet-ddd
Agent 安装分布
opencode
9
gemini-cli
9
github-copilot
9
codex
9
kimi-cli
9
amp
9
Skill 文档
Domain-Driven Design in .NET
Tactical DDD implementation patterns in modern C# â building rich domain models with Entities, Value Objects, Aggregates, Domain Events, and Repositories.
Scope: This skill covers tactical DDD (the building blocks). For strategic DDD (Bounded Contexts, Context Mapping, subdomain analysis), use the domain-analysis skill.
When to Use
- Modeling a domain with complex business rules
- Implementing Entities, Value Objects, or Aggregates
- Raising and handling Domain Events
- Designing Repository interfaces
- Structuring a .NET solution with DDD layers
- Applying the Result pattern, Strongly-typed IDs, or Specification pattern
Not for: Simple CRUD apps, anemic domain models, or when business logic lives entirely in services.
Key Concepts
| Concept | What It Is | C# Implementation |
|---|---|---|
| Entity | Object with identity that persists across state changes | Class with Id, equality by identity |
| Value Object | Immutable object defined by its attributes, no identity | record or sealed class with structural equality |
| Aggregate | Cluster of Entities/VOs with a single root, consistency boundary | Root entity that guards all invariants |
| Aggregate Root | Entry point to an Aggregate â the only externally-referenced entity | Public API, owns child entities |
| Domain Event | Something that happened in the domain that other parts care about | record implementing IDomainEvent |
| Repository | Abstraction for persisting/retrieving Aggregates | Interface in Domain, implementation in Infrastructure |
| Domain Service | Stateless operation that doesn’t belong to a single Entity/VO | Static method or injected service |
| Specification | Encapsulated query/business rule | Class with IsSatisfiedBy(T) |
Entity Base Class
public abstract class Entity<TId> : IEquatable<Entity<TId>>
where TId : notnull
{
public TId Id { get; protected init; }
private readonly List<IDomainEvent> _domainEvents = [];
public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents;
protected Entity(TId id) => Id = id;
public void RaiseDomainEvent(IDomainEvent domainEvent) =>
_domainEvents.Add(domainEvent);
public void ClearDomainEvents() => _domainEvents.Clear();
public bool Equals(Entity<TId>? other) =>
other is not null && Id.Equals(other.Id);
public override bool Equals(object? obj) =>
obj is Entity<TId> other && Equals(other);
public override int GetHashCode() => Id.GetHashCode();
public static bool operator ==(Entity<TId>? left, Entity<TId>? right) =>
Equals(left, right);
public static bool operator !=(Entity<TId>? left, Entity<TId>? right) =>
!Equals(left, right);
// Protected parameterless constructor for ORM
protected Entity() => Id = default!;
}
Value Object with record
public record Money(decimal Amount, string Currency)
{
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException($"Cannot add {Currency} to {other.Currency}");
return this with { Amount = Amount + other.Amount };
}
public static Money Zero(string currency) => new(0, currency);
}
public record Address(string Street, string City, string State, string ZipCode, string Country);
public record DateRange
{
public DateOnly Start { get; init; }
public DateOnly End { get; init; }
public DateRange(DateOnly start, DateOnly end)
{
if (end < start)
throw new ArgumentException("End date must be after start date");
Start = start;
End = end;
}
public bool Overlaps(DateRange other) =>
Start <= other.End && other.Start <= End;
}
Aggregate Example
public sealed class Order : Entity<OrderId>
{
private readonly List<OrderLine> _lines = [];
public IReadOnlyList<OrderLine> Lines => _lines;
public CustomerId CustomerId { get; private init; }
public OrderStatus Status { get; private set; }
public Money Total => _lines.Aggregate(Money.Zero("USD"), (sum, line) => sum.Add(line.SubTotal));
private Order() { } // ORM
public static Order Create(CustomerId customerId)
{
var order = new Order(OrderId.New())
{
CustomerId = customerId,
Status = OrderStatus.Draft
};
order.RaiseDomainEvent(new OrderCreatedEvent(order.Id));
return order;
}
public void AddLine(ProductId productId, int quantity, Money unitPrice)
{
if (Status != OrderStatus.Draft)
throw new DomainException("Can only add lines to draft orders");
if (quantity <= 0)
throw new DomainException("Quantity must be positive");
var line = new OrderLine(OrderLineId.New(), productId, quantity, unitPrice);
_lines.Add(line);
}
public void Submit()
{
if (_lines.Count == 0)
throw new DomainException("Cannot submit an empty order");
Status = OrderStatus.Submitted;
RaiseDomainEvent(new OrderSubmittedEvent(Id, Total));
}
}
Aggregate rules:
- Reference other Aggregates by ID only, never by direct object reference
- All state changes go through the Aggregate Root
- One transaction = one Aggregate (eventual consistency between Aggregates)
- Keep Aggregates small â only include what must be immediately consistent
Strongly-Typed IDs
public readonly record struct OrderId(Guid Value)
{
public static OrderId New() => new(Guid.NewGuid());
public override string ToString() => Value.ToString();
}
public readonly record struct CustomerId(Guid Value)
{
public static CustomerId New() => new(Guid.NewGuid());
}
Domain Events
public interface IDomainEvent
{
DateTime OccurredOn { get; }
}
public abstract record DomainEvent : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record OrderCreatedEvent(OrderId OrderId) : DomainEvent;
public record OrderSubmittedEvent(OrderId OrderId, Money Total) : DomainEvent;
Repository Interface
// Define in Domain layer â implement in Infrastructure
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(OrderId id, CancellationToken ct = default);
Task AddAsync(Order order, CancellationToken ct = default);
Task UpdateAsync(Order order, CancellationToken ct = default);
}
// Optional: generic base interface
public interface IRepository<T, TId>
where T : Entity<TId>
where TId : notnull
{
Task<T?> GetByIdAsync(TId id, CancellationToken ct = default);
Task AddAsync(T entity, CancellationToken ct = default);
}
Result Pattern (No Exceptions for Expected Failures)
public sealed class Result<T>
{
public T? Value { get; }
public Error? Error { get; }
public bool IsSuccess => Error is null;
private Result(T value) => Value = value;
private Result(Error error) => Error = error;
public static Result<T> Success(T value) => new(value);
public static Result<T> Failure(Error error) => new(error);
public TOut Match<TOut>(Func<T, TOut> onSuccess, Func<Error, TOut> onFailure) =>
IsSuccess ? onSuccess(Value!) : onFailure(Error!);
}
public record Error(string Code, string Message);
Usage in aggregate:
public Result<Order> Submit()
{
if (_lines.Count == 0)
return Result<Order>.Failure(OrderErrors.EmptyOrder);
Status = OrderStatus.Submitted;
RaiseDomainEvent(new OrderSubmittedEvent(Id, Total));
return Result<Order>.Success(this);
}
Constraints
MUST DO
- Keep domain layer free of infrastructure dependencies (no EF, no HTTP, no logging)
- Use Value Objects for concepts with no identity (Money, Address, Email)
- Enforce invariants inside the Aggregate â never outside
- Reference other Aggregates by ID only
- Use factory methods (
Create,From) instead of public constructors for Aggregates - Raise Domain Events for side effects that cross Aggregate boundaries
- Use
CancellationTokenon all async Repository methods
MUST NOT DO
- Expose setters on Aggregate state (use behavior methods instead)
- Let Aggregates depend on repositories or services
- Create “God Aggregates” that contain everything
- Use Domain Events for intra-Aggregate communication
- Put business logic in Application Services â it belongs in the domain
- Use anemic domain models (entities as data bags with logic in services)
Additional References
Load based on your task â do not load all at once:
- references/building-blocks.md â deep dive into Entity, Value Object, Aggregate patterns with edge cases
- references/domain-events.md â event dispatching strategies, outbox pattern, integration events
- references/patterns.md â Result pattern, Specification, Strongly-typed IDs, Guard clauses
- references/project-structure.md â solution layout, layer dependencies, project references