Ddd

codewithmukesh/dotnet-claude-kit/skills/ddd

作者 codewithmukesh23300897f4d1無授權條款754 個星標收錄於 2026年10月8日更新於 2026年10月8日儲存庫2 個月前更新

Domain-Driven Design tactical patterns for .NET applications. Covers aggregates, aggregate roots, value objects, domain events, domain services, strongly-typed IDs, and repository patterns for aggregate persistence. Load this skill when implementing DDD, working with aggregates, value objects, domain events, bounded contexts, or when the architecture-advisor recommends DDD + Clean Architecture. Pair with the clean-architecture skill.

AI 產生的概覽

針對 .NET 的領域驅動設計戰術模式指南,涵蓋聚合、值物件、領域事件與儲存庫。

功能
此技能為 .NET 應用程式提供領域驅動設計戰術模式的參考指引。內容說明聚合與聚合根、值物件、強型別 ID、領域事件、領域服務與聚合儲存庫,並附上 C# 程式碼範例與 EF Core 設定片段。此外也列出常見反模式,並提供各模式適用時機的決策指南。
適用情境
適用於在 .NET 程式碼庫中實作 DDD,或處理聚合、值物件、領域事件、限界情境時。也適合架構顧問建議採用 DDD 搭配整潔架構的情況。
執行需求
未包含指令碼或工具,僅為說明性內容。範例假設使用 .NET/C# 專案,並依賴 EF Core 及提供 INotification 的中介者套件,文中亦說明可改用一般標記介面。

Domain-Driven Design (DDD)

Core Principles

  1. Aggregates define consistency boundaries — An aggregate is a cluster of entities and value objects treated as a single unit for data changes. All invariants within an aggregate are enforced in a single transaction. Cross-aggregate consistency is eventual.
  2. Value objects over primitives — Replace primitive obsession with value objects. Money, EmailAddress, OrderNumber are not strings — they carry validation, equality, and behavior. Use C# records for immutable value objects.
  3. Domain events decouple side effects — When something meaningful happens in the domain (OrderPlaced, PaymentReceived), raise a domain event. Side effects (send email, update read model, notify another aggregate) subscribe to these events. The aggregate stays focused on its own rules.
  4. Aggregate root is the sole entry point — External code accesses an aggregate only through its root entity. Child entities are never loaded or modified independently. The root enforces all invariants for the entire aggregate.
  5. Repositories persist aggregates, not entities — One repository per aggregate root. The repository loads and saves the entire aggregate as a unit. No repository for child entities. The Infrastructure implementation uses DbContext internally — this is a DDD tactical pattern for aggregate boundaries, not a generic CRUD wrapper.

Patterns

Aggregate Root

The aggregate root owns all access to its children and enforces invariants:

csharp
// Domain/Orders/Order.cspublic sealed class Order : AggregateRoot{    private readonly List<OrderLine> _lines = [];
    private Order() { } // EF Core
    public OrderNumber Number { get; private set; } = null!;    public CustomerId CustomerId { get; private set; }    public Money Total { get; private set; } = Money.Zero("USD");    public OrderStatus Status { get; private set; }    public DateTimeOffset PlacedAt { get; private set; }    public IReadOnlyList<OrderLine> Lines => _lines.AsReadOnly();
    public static Order Place(CustomerId customerId, OrderNumber number, DateTimeOffset now)    {        var order = new Order        {            Id = Guid.CreateVersion7(),            CustomerId = customerId,            Number = number,            Status = OrderStatus.Placed,            PlacedAt = now        };
        order.RaiseDomainEvent(new OrderPlaced(order.Id, customerId, now));        return order;    }
    public Result AddLine(ProductId productId, int quantity, Money unitPrice)    {        if (Status is not OrderStatus.Placed)            return Result.Failure("Cannot modify a confirmed or cancelled order");
        if (quantity <= 0)            return Result.Failure("Quantity must be positive");
        var existing = _lines.FirstOrDefault(l => l.ProductId == productId);        if (existing is not null)        {            existing.IncreaseQuantity(quantity);        }        else        {            _lines.Add(new OrderLine(productId, quantity, unitPrice));        }
        RecalculateTotal();        return Result.Success();    }
    public Result Confirm()    {        if (Status is not OrderStatus.Placed)            return Result.Failure("Only placed orders can be confirmed");
        if (_lines.Count == 0)            return Result.Failure("Cannot confirm an order with no lines");
        Status = OrderStatus.Confirmed;        RaiseDomainEvent(new OrderConfirmed(Id));        return Result.Success();    }
    private void RecalculateTotal()    {        Total = _lines.Aggregate(Money.Zero(Total.Currency), (sum, line) => sum + line.Subtotal);    }}

Value Objects as Records

Use C# records for immutable value objects with structural equality:

csharp
// Domain/Common/Money.cspublic sealed record Money{    public decimal Amount { get; }    public string Currency { get; }
    public Money(decimal amount, string currency)    {        ArgumentOutOfRangeException.ThrowIfNegative(amount);        ArgumentException.ThrowIfNullOrWhiteSpace(currency);
        Amount = amount;        Currency = currency.ToUpperInvariant();    }
    public static Money Zero(string currency) => new(0, currency);
    public static Money operator +(Money left, Money right)    {        if (left.Currency != right.Currency)            throw new InvalidOperationException($"Cannot add {left.Currency} and {right.Currency}");        return new Money(left.Amount + right.Amount, left.Currency);    }}
// Other value objects (EmailAddress, OrderNumber, etc.) follow the same pattern:// sealed record, constructor validation, no public setters

Strongly-Typed IDs with EF Core Converters

Prevent mixing up GUIDs from different entities:

csharp
// Domain/Common/StronglyTypedId.cspublic readonly record struct CustomerId(Guid Value){    public static CustomerId New() => new(Guid.CreateVersion7());    public override string ToString() => Value.ToString();}
public readonly record struct ProductId(Guid Value){    public static ProductId New() => new(Guid.CreateVersion7());}
public readonly record struct OrderNumber(string Value){    public override string ToString() => Value;}
// Infrastructure/Persistence/Configurations/OrderConfiguration.cspublic class OrderConfiguration : IEntityTypeConfiguration<Order>{    public void Configure(EntityTypeBuilder<Order> builder)    {        builder.HasKey(o => o.Id);
        builder.Property(o => o.CustomerId)            .HasConversion(id => id.Value, value => new CustomerId(value));
        builder.Property(o => o.Number)            .HasConversion(n => n.Value, value => new OrderNumber(value))            .HasMaxLength(50);
        builder.ComplexProperty(o => o.Total, money =>        {            money.Property(m => m.Amount).HasColumnName("Total").HasPrecision(18, 2);            money.Property(m => m.Currency).HasColumnName("Currency").HasMaxLength(3);        });
        builder.HasMany(o => o.Lines).WithOne().HasForeignKey("OrderId");        builder.Navigation(o => o.Lines).AutoInclude();    }}

Domain Event Dispatching

Raise events in the aggregate, dispatch in SaveChangesAsync:

csharp
// Domain/Common/AggregateRoot.cspublic abstract class AggregateRoot : Entity{    private readonly List<IDomainEvent> _domainEvents = [];
    public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
    protected void RaiseDomainEvent(IDomainEvent domainEvent) => _domainEvents.Add(domainEvent);
    public void ClearDomainEvents() => _domainEvents.Clear();}
// INotification comes from the MIT-licensed Mediator package (not MediatR —// see the packages rule). Use a plain marker interface if you don't use a mediator.public interface IDomainEvent : INotification{    DateTimeOffset OccurredAt { get; }}
// Domain/Orders/Events/OrderPlaced.cspublic sealed record OrderPlaced(Guid OrderId, CustomerId CustomerId, DateTimeOffset PlacedAt) : IDomainEvent{    public DateTimeOffset OccurredAt => PlacedAt;}
// Infrastructure/Persistence/AppDbContext.cs — publisher injected via primary constructorpublic class AppDbContext(DbContextOptions<AppDbContext> options, IPublisher publisher)    : DbContext(options){    public override async Task<int> SaveChangesAsync(CancellationToken ct = default)    {        var aggregates = ChangeTracker.Entries<AggregateRoot>()            .Where(e => e.Entity.DomainEvents.Count > 0)            .Select(e => e.Entity)            .ToList();
        var events = aggregates.SelectMany(a => a.DomainEvents).ToList();
        var result = await base.SaveChangesAsync(ct);
        foreach (var @event in events)            await publisher.Publish(@event, ct);
        foreach (var aggregate in aggregates)            aggregate.ClearDomainEvents();
        return result;    }}

Domain Services

For logic that does not belong to a single aggregate:

csharp
// Domain/Orders/Services/PricingService.cs// Coordinates logic across aggregates — takes domain interfaces, returns value objectspublic sealed class PricingService(IDiscountPolicy discountPolicy){    public Money CalculatePrice(ProductId productId, int quantity, Money unitPrice, CustomerId customerId)    {        var subtotal = new Money(unitPrice.Amount * quantity, unitPrice.Currency);        var discount = discountPolicy.GetDiscount(customerId, productId, quantity);        return new Money(subtotal.Amount * (1 - discount), subtotal.Currency);    }}

Anti-patterns

Oversized Aggregates

csharp
// BAD — Customer aggregate owns everything the customer touchespublic class Customer : AggregateRoot{    public List<Order> Orders { get; } = [];        // should be separate aggregate    public List<Payment> Payments { get; } = [];     // should be separate aggregate    public List<Address> Addresses { get; } = [];    // might be OK as child    public ShoppingCart Cart { get; set; }            // should be separate aggregate}
// GOOD — small, focused aggregates linked by IDpublic class Customer : AggregateRoot{    public CustomerName Name { get; private set; }    public EmailAddress Email { get; private set; }    // Orders, Payments, Cart are separate aggregates referencing CustomerId}

Domain Events for Intra-Aggregate Logic

csharp
// BAD — using events for logic within the same aggregateorder.RaiseDomainEvent(new OrderLineAdded(line));// Then a handler recalculates the total... but you're in the same aggregate!
// GOOD — just call the method directly within the aggregate_lines.Add(line);RecalculateTotal();  // private method, no event needed

Value Objects with Identity

csharp
// BAD — value object with an Id (it's an entity then!)public record Address{    public Guid Id { get; init; }  // value objects don't have identity    public string Street { get; init; }}
// GOOD — value objects are defined by their attributes, not an Idpublic record Address(string Street, string City, string PostalCode, string Country);

Anemic Aggregates

csharp
// BAD — aggregate is just a data bag, service does all the workpublic class Order : AggregateRoot{    public OrderStatus Status { get; set; }  // public setter!    public List<OrderLine> Lines { get; set; } = [];}
// Service directly manipulates order stateorder.Status = OrderStatus.Confirmed;  // no invariant check!order.Lines.Add(newLine);              // no validation!
// GOOD — aggregate encapsulates rules (see Aggregate Root pattern above)order.Confirm();  // validates status, raises eventorder.AddLine(productId, quantity, unitPrice);  // validates, recalculates

Decision Guide

ScenarioRecommendation
When to use DDDComplex domain with business rules that go beyond CRUD
When to use value objectsAny concept with validation rules or equality based on attributes, not identity
Aggregate sizeKeep small — typically 1 root entity + 0-3 child entities. Load the whole aggregate every time
Domain events vs integration eventsDomain events: within bounded context, same transaction. Integration events: cross-context, via message bus
Strongly-typed IDsAlways for aggregate root IDs that cross boundaries. Optional for child entity IDs
When NOT to use DDDSimple CRUD, settings, audit logs, read models — use plain entities
Repository vs DbContextRepository per aggregate root for complex aggregates; IAppDbContext for simpler queries
Domain servicesOnly when logic requires multiple aggregates or external data the aggregate should not know about

來源與署名

來源:codewithmukesh/dotnet-claude-kit位於skills/ddd提交2330089

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架

更多來自 codewithmukesh/dotnet-claude-kit 的技能

Wrap Up

codewithmukesh

在 session 結束時把已完成工作、待辦事項與經驗寫入交接檔案,並在 session 開始時重新載入。

Productivity & Workflow7542 個月前更新

Workflow Mastery

codewithmukesh

Claude Code workflow mastery for .NET developers. Covers parallel execution with git worktrees, plan mode strategy, verification loops, auto-formatting hooks, permission setup for dotnet CLI, prompting techniques, subagent patterns, and context discipline — token budget management, MCP-first navigation, lazy loading, and subagent isolation — all adapted for the .NET ecosystem. Load this skill when setting up Claude Code for a .NET project, optimizing workflows, running parallel sessions, when context is running low or sessions feel sluggish, when exploring a large codebase efficiently, or when the user mentions "productivity", "workflow", "parallel", "worktree", "plan mode", "permissions", "hooks", "10x", "setup Claude Code", "speed up development", "context", "tokens", "budget", "running out of context", "too many files", or "large codebase". Inspired by tips from Boris Cherny (creator of Claude Code) and the Anthropic team.

待分類7542 個月前更新

Vertical Slice

codewithmukesh

指導 .NET 開發者以垂直切片架構組織應用程式,涵蓋功能資料夾、端點分組與處理常式模式。

Software Development7542 個月前更新

Testing

codewithmukesh

Testing strategy for .NET 10 applications. Covers xUnit v3, WebApplicationFactory for integration tests, Testcontainers for real database testing, Verify for snapshot testing, and the AAA pattern. Load this skill when writing tests, setting up test infrastructure, reviewing test coverage, or when the user mentions "test", "xUnit", "WebApplicationFactory", "Testcontainers", "integration test", "unit test", "bUnit", "snapshot test", "Verify", "test coverage", "AAA pattern", "WireMock", or "FakeTimeProvider".

待分類7542 個月前更新

Tdd

codewithmukesh

Guided test-driven development workflow for .NET 10 using xUnit v3, WebApplicationFactory, Testcontainers, and Verify snapshots. Follows the strict red-green-refactor cycle. Use when: "TDD", "test-driven", "let's TDD this", "red green refactor", "write the test first", or when building a feature with clear acceptance criteria.

待分類7542 個月前更新

Spec

codewithmukesh

透過結構化提問,把模糊的功能想法轉化為雙方確認並持久化的規格文件。

Productivity & Workflow7542 個月前更新