К основному содержанию
скилл CX Codex

ddd — скилл DDD для Claude Code: агрегаты и value objects в .NET

Скилл для тактических паттернов Domain-Driven Design в .NET: агрегаты, value objects, доменные события, строго типизированные ID и репозитории для агрегатов.

Установить в Claude Code · Через Skills CLI (Vercel) Все способы и платформы ↓
npx skills add codewithmukesh/dotnet-claude-kit --skill ddd -a claude-code
Открыть на GitHub

Что делает ddd

Скилл задаёт агенту правила DDD для .NET-проектов. Внутри — пять принципов: агрегат как граница согласованности, value objects вместо примитивов, доменные события для побочных эффектов, агрегатный корень как единственная точка входа, один репозиторий на корень. Дальше идут примеры кода на C#: класс Order с методами Place, AddLine, Confirm и Result вместо исключений, запись Money с проверкой в конструкторе, readonly record struct для CustomerId и OrderNumber.

Отдельный блок — конфигурация EF Core: конвертеры для строго типизированных ID, ComplexProperty для Money с точностью 18, 2, AutoInclude для строк заказа. Доменные события поднимаются в агрегате, а рассылаются в SaveChangesAsync. Скилл стоит подключать, когда архитектура уже строится вокруг агрегатов и ограниченных контекстов. Если проект на простом CRUD, эти правила только добавят кода.

Автор советует использовать скилл вместе с clean-architecture. Текст обрезан на описании AggregateRoot, так что про остальные паттерны судить по нему нельзя.

Какие задачи решает

  • разложить доменную логику по агрегатам вместо анемичных моделей
  • заменить строки и числа на value objects с валидацией
  • развести GUID разных сущностей через строго типизированные ID
  • настроить EF Core конвертеры и complex properties для value objects
  • отвязать побочные эффекты от агрегата через доменные события

Примеры запросов агенту

  • Создай агрегат Order с методами Place, AddLine и Confirm по паттернам скилла ddd
  • Сделай value object Money как sealed record с проверкой в конструкторе
  • Настрой EF Core конфигурацию для строго типизированных ID и Money

Как установить ddd

1Через Skills CLI (Vercel)

npx skills add codewithmukesh/dotnet-claude-kit --skill ddd -a claude-code

2Вручную: положить файлы

~/.claude/skills/ddd/SKILL.md

Папка скилла целиком; для одного проекта — .claude/skills/ddd/

По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов

Вопросы о ddd

Как установить ddd в Claude?

Выполните npx skills add codewithmukesh/dotnet-claude-kit --skill ddd -a claude-code. После этого файл скилла лежит в ~/.claude/skills/ddd/SKILL.md.

Что делает скилл ddd?

Описывает тактические паттерны Domain-Driven Design для .NET: агрегаты и их корни, value objects, доменные события, доменные сервисы, строго типизированные ID и репозитории для сохранения агрегатов.

С чем сочетается ddd?

Автор советует ставить его вместе со скиллом clean-architecture и подключать, когда architecture-advisor рекомендует связку DDD и Clean Architecture.

Исходный текст

SKILL.md · MIT
---
name: ddd
description: >
  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.
---

# 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.cs
public 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.cs
public 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.cs
public 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.cs
public 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.cs
public 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.cs
public sealed record OrderPlaced(Guid OrderId, CustomerId CustomerId, DateTimeOffset PlacedAt) : IDomainEvent
{
    public DateTimeOffset OccurredAt => PlacedAt;
}

// Infrastructure/Persistence/AppDbContext.cs — publisher injected via primary constructor
public 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 objects
public 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 touches
public 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 ID
public 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 aggregate
order.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 Id
public record Address(string Street, string City, string PostalCode, string Country);
```

### Anemic Aggregates

```csharp
// BAD — aggregate is just a data bag, service does all the work
public class Order : AggregateRoot
{
    public OrderStatus Status { get; set; }  // public setter!
    public List<OrderLine> Lines { get; set; } = [];
}

// Service directly manipulates order state
order.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 event
order.AddLine(productId, quantity, unitPrice);  // validates, recalculates
```

## Decision Guide

| Scenario | Recommendation |
|----------|---------------|
| When to use DDD | Complex domain with business rules that go beyond CRUD |
| When to use value objects | Any concept with validation rules or equality based on attributes, not identity |
| Aggregate size | Keep small — typically 1 root entity + 0-3 child entities. Load the whole aggregate every time |
| Domain events vs integration events | Domain events: within bounded context, same transaction. Integration events: cross-context, via message bus |
| Strongly-typed IDs | Always for aggregate root IDs that cross boundaries. Optional for child entity IDs |
| When NOT to use DDD | Simple CRUD, settings, audit logs, read models — use plain entities |
| Repository vs DbContext | Repository per aggregate root for complex aggregates; IAppDbContext for simpler queries |
| Domain services | Only when logic requires multiple aggregates or external data the aggregate should not know about |

© codewithmukesh · MIT · codewithmukesh/dotnet-claude-kit

Похожие скиллы

Всё по теме «Бэкенд и API» →
V3 DDD Architecture ruvnet Скилл раскладывает монолитный orchestrator из claude-flow v3 по пяти bounded contexts и собирает домены в микрокернель с шиной событий. CX Codex +1 1 100установок ddd-context ruvnet Разворачивает структуру ограниченного контекста DDD с разделением на домен, приложение и инфраструктуру. Пригодится при распиле монолита. CX Codex +1 738установок dotnet-patterns affaan-m Набор архитектурных правил C# и ASP.NET Core для ИИ-ассистентов. Задаёт стандарты написания чистого кода, работы с DI, асинхронностью и обработкой ошибок. CX Codex +1 4 600установок migrate-dotnet10-to-dotnet11 dotnet Скилл переводит проекты и решения с .NET 10 на .NET 11. Он обновляет целевой фреймворк, пакеты и исправляет несовместимости C# 15 и EF Core 11. CX Codex +1 3 400установок migrate-dotnet9-to-dotnet10 dotnet Ведёт проект с net9.0 на net10.0: правит TargetFramework, чинит ошибки сборки и разбирает ломающие изменения .NET 10, C# 14, ASP.NET Core 10 и EF Core 10. CX Codex +1 3 400установок csharp официальный github Правило задаёт Copilot стиль C#-кода: именование, nullable-типы, форматирование и работу с ASP.NET Core. Пригодится, если агент пишет по-разному в каждом файле. ★ 40 418звёзд репо

Из того же набора46

clean-architecture Скилл для .NET-проектов: раскладывает код по четырём слоям, следит за направлением зависимостей и подсказывает, где живут use case-хендлеры. CX Codex +1 3 000установок verify Запускает проверку .NET-проекта перед ревью: сборку, тесты, безопасность, форматирование и анализ изменений. CX Codex +1 2 800установок code-review Многоуровневое ревью .NET-кода: Roslyn MCP ищет антипаттерны и диагностику, а ручная проверка идёт по зонам риска. CX Codex +1 2 800установок project-setup Подбирает стек технологий для проектов на .NET: базу данных, авторизацию, кеш и очереди. Пригодится на старте архитектурного выбора или ревизии зависимостей. CX Codex +1 2 800установок instinct-system Фиксирует паттерны, правки пользователя и находки в файлах проекта. Нужен, когда агенту надо учиться на повторяющихся решениях. CX Codex +1 2 800установок container-publish Собирает OCI-образы прямо из dotnet publish /t:PublishContainer, без Dockerfile. Пригодится, когда проект на .NET 10 надо упаковать в контейнер и запушить в реестр. CX Codex +1 2 800установок workflow-mastery Набор практик и готовых конфигураций для ускорения разработки на .NET в Claude Code. Помогает настроить параллельные сессии через git worktree, циклы верификации и лимиты контекста. CX Codex +1 2 800установок aspire Помогает настроить .NET Aspire для локального запуска сервисов, ресурсов и обнаружения сервисов. Нужен при работе с AppHost и общими настройками сервисов. CX Codex +1 2 800установок

Спонсоры проекта

Компании, которые помогают держать аналитику открытой