用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/rudironsoni/Synaxis --skill dotnet-domain-modeling命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
正在显示 SKILL.md
| name | dotnet-domain-modeling |
| category | developer-experience |
| subcategory | cli |
| description | Models business domains. Aggregates, value objects, domain events, rich models, repositories. |
| license | MIT |
| targets | ["*"] |
| tags | ["architecture","dotnet","skill"] |
| version | 0.0.1 |
| author | dotnet-agent-harness |
| invocable | true |
| claudecode | {"allowed-tools":["Read","Grep","Glob","Bash","Write","Edit"]} |
| codexcli | {"short-description":".NET skill guidance for architecture tasks"} |
| opencode | {"allowed-tools":["Read","Grep","Glob","Bash","Write","Edit"]} |
| copilot | {} |
| geminicli | {} |
| antigravity | {} |
Domain-Driven Design tactical patterns in C#. Covers aggregate roots, entities, value objects, domain events, integration events, domain services, repository contract design, and the distinction between rich and anemic domain models. These patterns apply to the domain layer itself -- the pure C# model that encapsulates business rules -- independent of any persistence technology.
Cross-references: [skill:dotnet-efcore-architecture] for aggregate persistence and repository implementation with EF Core, [skill:dotnet-efcore-patterns] for DbContext configuration and migrations, [skill:dotnet-architecture-patterns] for vertical slices and request pipeline design, [skill:dotnet-validation-patterns] for input validation patterns, [skill:dotnet-messaging-patterns] for integration event infrastructure.
An aggregate is a cluster of domain objects treated as a single unit for data changes. The aggregate root is the entry point -- all modifications to the aggregate pass through it.
Entities have identity that persists across state changes. Use a base class to standardize identity and equality:
public abstract class Entity<TId> : IEquatable<Entity<TId>>
where TId : notnull
{
// default! required for ORM hydration; Id is set immediately after construction
public TId Id { ; ; } = !;
{ }
=> Id = id;
=>
obj Entity<TId> other && Equals(other);
=>
other
&& GetType() == other.GetType()
&& EqualityComparer<TId>.Default.Equals(Id, other.Id);
=>
EqualityComparer<TId>.Default.GetHashCode(Id);
==(Entity<TId>? left, Entity<TId>? right) =>
Equals(left, right);
!=(Entity<TId>? left, Entity<TId>? right) =>
!Equals(left, right);
}
```text
The aggregate root extends `Entity` collects domain events:
```csharp
<> : <>
:
{
List<IDomainEvent> _domainEvents = [];
IReadOnlyList<IDomainEvent> DomainEvents =>
_domainEvents.AsReadOnly();
{ }
{ }
=>
_domainEvents.Add(domainEvent);
=> _domainEvents.Clear();
}
```text
```csharp
: <>
{
CustomerId CustomerId { ; ; } = !;
OrderStatus Status { ; ; }
Money Total { ; ; } = Money.Zero();
List<OrderLine> _lines = [];
IReadOnlyList<OrderLine> Lines => _lines.AsReadOnly();
{ }
{
order = Order(Guid.NewGuid())
{
CustomerId = customerId,
Status = OrderStatus.Draft
};
order.RaiseDomainEvent( OrderCreated(order.Id, customerId));
order;
}
{
(Status != OrderStatus.Draft)
DomainException();
(quantity <= )
DomainException();
line = OrderLine(productId, quantity, unitPrice);
_lines.Add(line);
RecalculateTotal();
}
{
(Status != OrderStatus.Draft)
DomainException();
(_lines.Count == )
DomainException();
Status = OrderStatus.Submitted;
RaiseDomainEvent( OrderSubmitted(Id, Total));
}
=>
Total = _lines.Aggregate(
Money.Zero(Total.Currency),
(sum, line) => sum.Add(line.LineTotal));
}
```text
| Rule | Rationale |
| ------------------------------------------- | --------------------------------------------------------------------- |
| All mutations go through the aggregate root | Enforces invariants one place |
| Reference other aggregates ID only | Prevents cross-aggregate coupling; use `CustomerId` `Customer` |
| Keep aggregates small | Large aggregates cause contention slow loads |
| One aggregate per transaction | Cross-aggregate changes use domain events eventual consistency |
| Expose collections `IReadOnlyList<T>` | Prevents external code bypassing root methods to mutate children |
{
Value { ; }
{
(.IsNullOrWhiteSpace())
DomainException();
Value = ;
}
=> Value;
}
{
Street { ; }
City { ; }
State { ; }
PostalCode { ; }
Country { ; }
{
(.IsNullOrWhiteSpace(street))
DomainException();
(.IsNullOrWhiteSpace(city))
DomainException();
(.IsNullOrWhiteSpace(postalCode))
DomainException();
Street = street;
City = city;
State = state;
PostalCode = postalCode;
Country = country;
}
}
```text
Money the canonical example of a multi-field behavior:
```csharp
{
Amount { ; }
Currency { ; }
{
(.IsNullOrWhiteSpace(currency))
DomainException();
Amount = amount;
Currency = currency.ToUpperInvariant();
}
=> (m, currency);
{
EnsureSameCurrency(other);
Money(Amount + other.Amount, Currency);
}
{
EnsureSameCurrency(other);
Money(Amount - other.Amount, Currency);
}
=>
(Amount * quantity, Currency);
=>
(Amount * factor, Currency);
{
(Currency != other.Currency)
DomainException(
);
}
=> ;
}
```text
;
money.Property(m => m.Currency).HasColumnName()
.HasMaxLength();
});
builder.Property(o => o.CustomerId)
.HasConversion(
id => id.Value,
=> CustomerId())
.HasMaxLength();
```text
| Use | Use primitive |
| ---------------------------------------------------------- | -------------------------------------------------------------------- |
|
{
Guid EventId { ; }
DateTimeOffset OccurredAt { ; }
}
:
{
Guid EventId { ; } = Guid.NewGuid();
DateTimeOffset OccurredAt { ; } = DateTimeOffset.UtcNow;
}
;
;
;
```text
Dispatch events after `SaveChangesAsync` succeeds to ensure the aggregate state persisted before side effects
execute:
```
{
{
( domainEvent events)
{
handlerType = (IDomainEventHandler<>)
.MakeGenericType(domainEvent.GetType());
handlers = serviceProvider.GetServices(handlerType);
( handler handlers)
{
(()handler).HandleAsync(
()domainEvent, ct);
}
}
}
}
< >
:
{
;
}
```text
Use an EF Core `SaveChangesInterceptor` a wrapper to dispatch events after save:
```
{
{
(eventData.Context )
{
aggregates = eventData.Context.ChangeTracker
.Entries<AggregateRoot<Guid>>()
.Where(e => e.Entity.DomainEvents.Count > )
.Select(e => e.Entity)
.ToList();
events = aggregates
.SelectMany(a => a.DomainEvents)
.ToList();
( aggregate aggregates)
{
aggregate.ClearDomainEvents();
}
dispatcher.DispatchAsync(events, ct);
}
result;
}
}
```text
| Aspect | Domain Event | Integration Event |
| ----------- | ----------------------------------------- | ------------------------------------------------------- |
| Scope | Within a bounded context | Across bounded contexts / services |
| Transport | In-process (dispatcher) |
{
{
publishEndpoint.Publish(
OrderSubmittedIntegration(
domainEvent.OrderId,
domainEvent.Total.Amount,
domainEvent.Total.Currency),
ct);
}
}
```text
---
Business logic lives inside the domain entities. Methods enforce invariants meaningful results:
```csharp
: <>
{
List<CartItem> _items = [];
IReadOnlyList<CartItem> Items => _items.AsReadOnly();
{
existing = _items.Find(i => i.ProductId == productId);
(existing )
{
existing.IncreaseQuantity(quantity);
}
{
_items.Add( CartItem(productId, quantity, unitPrice));
}
}
{
item = _items.Find(i => i.ProductId == productId)
?? DomainException(
);
_items.Remove(item);
}
=>
_items.Aggregate(
Money.Zero(currency),
(sum, item) => sum.Add(item.LineTotal));
}
```text
Entities are data bags setters. Business logic lives external services:
```csharp
{
Guid Id { ; ; }
List<CartItem> Items { ; ; } = [];
}
{
{
existing = cart.Items.Find(i => i.ProductId == productId);
(existing != )
existing.Quantity += quantity;
cart.Items.Add( CartItem { ... });
}
}
```text
| Factor | Rich model | Anemic model |
| -------------------- | ------------------------------------- | --------------------------------- |
| Complex invariants | Enforced entity | Scattered across services |
| Testability | Test entity behavior directly | Test service + entity together |
| Discoverability | Methods entity show capabilities | Must find the right service |
| | - | |
| | | |
**:** .
(., , ).
---
##
.
- .
```
{
{
discount = Money.Zero(order.Total.Currency);
discount = tier
{
CustomerTier.Gold => discount.Add(
order.Total.Multiply(m)),
CustomerTier.Platinum => discount.Add(
order.Total.Multiply(m)),
_ => discount
};
( promo activePromotions)
{
(promo.AppliesTo(order))
{
discount = discount.Add(promo.Calculate(order));
}
}
discount;
}
}
```text
- Logic requires data **multiple aggregates** that should reference each other
-
{
Task<Order?> FindByIdAsync(Guid id, CancellationToken ct);
;
;
}
{
;
}
```text
For EF Core repository implementations, see [skill:dotnet-efcore-architecture].
| Rule | Rationale |
| ------------------------------------------------ | ---------------------------------------------------------- |
| One repository per aggregate root | Child entities are accessed through the root |
| No `IQueryable<T>` types | Prevents persistence concerns leaking domain |
| No generic `IRepository<T>` | Cannot express aggregate-specific loading rules |
| Return domain types, DTOs | Repositories serve the domain; read models use projections |
| Include `CancellationToken` all methods | Required proper cancellation propagation |
---
Use domain-specific exceptions to signal invariant violations. This separates domain errors infrastructure errors:
```csharp
:
{
{ }
{ }
}
{
ProductId ProductId => productId;
Requested => requested;
Available => available;
}
```; domain validation enforces business rules.
**Do create anemic entities `List<T>` properties** -- expose collections `IReadOnlyList<T>`
provide mutation methods the aggregate root that enforce business rules.
**Do inject infrastructure services domain entities** -- entities should be pure C
services logic that needs external data, application services infrastructure orchestration.
---
- [Domain-driven design EF Core](https:
- [Implementing domain events](https:
- [Value objects DDD](https:
- [
Primary approach: Use Serena symbol operations for efficient code navigation:
serena_find_symbol instead of text searchserena_get_symbols_overview for file organizationserena_find_referencing_symbols for impact analysisserena_replace_symbol_body for clean modificationsWhen to use Serena vs traditional tools:
Example workflow:
# Instead of:
Read: src/Services/OrderService.cs
Grep: "public void ProcessOrder"
# Use:
serena_find_symbol: "OrderService/ProcessOrder"
serena_get_symbols_overview: "src/Services/OrderService.cs"
Routes .NET/C# work to domain skills. Loads coding-standards for code paths.
基于 SOC 职业分类