Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Um comando direto ignora o prompt de revisão. Verifique a origem antes de executá-lo.
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.
Scope
Aggregate roots, entities, and value objects
Domain events and integration events
Domain services and rich vs anemic model design
Repository contract design (persistence-agnostic)
Out of scope
EF Core configuration and aggregate persistence mapping -- see [skill:dotnet-efcore-architecture]
Tactical EF Core usage (DbContext lifecycle, migrations) -- see [skill:dotnet-efcore-patterns]
Input validation at API boundaries -- see [skill:dotnet-validation-patterns]
Data access technology selection -- see [skill:dotnet-data-access-strategy]
Vertical slice architecture and request pipelines -- see [skill:dotnet-architecture-patterns]
Messaging infrastructure and saga orchestration -- see [skill:dotnet-messaging-patterns]
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.
Aggregate Roots and Entities
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.
Entity Base Class
Entities have identity that persists across state changes. Use a base class to standardize identity and equality:
publicabstractclassEntity<TId> : IEquatable<Entity<TId>>
whereTId : notnull
{
// default! required for ORM hydration; Id is set immediately after constructionpublic 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:
- [
publicvoidAddLine(ProductId productId, int quantity, Money unitPrice)
if
throw
new
"Cannot modify a non-draft order."
if
0
throw
new
"Quantity must be positive."
var
new
publicvoidSubmit()
if
throw
new
"Only draft orders can be submitted."
if
0
throw
new
"Cannot submit an empty order."
new
privatevoidRecalculateTotal()
### Aggregate Design Rules
in
by
not
lock
and
and
as
from
For the EF Core persistence implications of these rules (navigation properties, owned types, cascade behavior), see
[skill:dotnet-efcore-architecture].
---
## Value Objects
Value objects have no identity -- they are defined by their attribute values. Two value objects with the same attributes
are equal. In C#, `record` and `recordstruct` provide natural value semantics.
### Record-Based Value Objects
```csharp
// Simple value object -- wraps a primitive to enforce constraintspublicsealedrecord CustomerId
public
string
get
publicCustomerId(stringvalue)
if
string
value
throw
new
"Customer ID cannot be empty."
value
publicoverridestringToString()
// Composite value object -- multiple properties with validation
public
sealed
record
Address
public
string
get
public
string
get
public
string
get
public
string
get
public
string
get
publicAddress(string street, string city, string state,
string postalCode, string country)
if
string
throw
new
"Street is required."
if
string
throw
new
"City is required."
if
string
throw
new
"Postal code is required."
### Money Value Object
is
value
object
with
public
sealed
record
Money
public
decimal
get
public
string
get
publicMoney(decimal amount, string currency)
if
string
throw
new
"Currency is required."
publicstatic Money Zero(string currency)
new
0
public Money Add(Money other)
return
new
public Money Subtract(Money other)
return
new
public Money Multiply(int quantity)
new
public Money Multiply(decimal factor)
new
privatevoidEnsureSameCurrency(Money other)
if
throw
new
$"Cannot operate on {Currency} and {other.Currency}."
publicoverridestringToString()
$"{Amount:F2}{Currency}"
### Value Object EF Core Mapping
Map value objects using owned types orvalueconversions (implementation in [skill:dotnet-efcore-architecture]):
```csharp
// Owned type -- maps to columns in the parent table
builder.OwnsOne(o => o.Total, money =>
{
money.Property(m => m.Amount).HasColumnName("TotalAmount")
"TotalCurrency"
3
// Value conversion -- single-property value objects
value
new
value
50
### When to Use Value Objects
value
object
Domain concept withconstraints (email, money, quantity) | Infrastructure IDs with no domain rules (correlation IDs, trace IDs) |
| Multiple properties that form a unit (address, date range) | Single valuewith no validation needed |
| Need to prevent primitive obsession in domain methods | Simple DTO fields at API boundary |
---
## Domain Events
Domain events represent something meaningful that happened in the domain. They enable loose coupling between aggregates
and trigger side effects (sending emails, updating read models, publishing integration events).
### Event Contracts
```csharp
// Marker interface for all domain eventspublicinterface IDomainEvent
// Map domain event to integration event (no domain types)
await
new
## Rich vs Anemic Domain Models
### Rich Domain Model
and
return
public
sealed
class
ShoppingCart
AggregateRoot
Guid
private
readonly
public
publicvoidAddItem(ProductId productId, int quantity, Money unitPrice)
var
if
is
not
null
else
new
publicvoidRemoveItem(ProductId productId)
var
throw
new
$"Product {productId} not in cart."
public Money GetTotal(string currency)
### Anemic Domain Model (Anti-Pattern)
with
public
in
// ANTI-PATTERN: Entity is just a data container
public
class
ShoppingCart
public
get
set
public
get
set
// All logic lives here -- the entity has no behavior
public
class
ShoppingCartService
publicvoidAddItem(ShoppingCart cart, string productId,
int quantity, decimal unitPrice)
var
if
null
else
new
### Decision Guide
in
on
class
Persistence
coupling
Requires
ORM
friendly
private
setters
Simple
property
mapping
Team
familiarity
DDD
experience
required
Familiar
to
most
developers
Recommendation
Start
with
a
rich
model
for
aggregates
with
complex
business
rules
Anemic
models
are
acceptable
for
simple
CRUD
entities
where
the
domain
logic
is
minimal
e.g
reference
data
configuration
records
Domain
Services
Domain
services
encapsulate
business
logic
that
does
not
naturally
belong
to
a
single
entity
or
value
object
They
operate
on
domain
types
and
enforce
cross
aggregate
rules
csharp
public
sealed
class
PricingService
public Money CalculateDiscount(
Order order,
CustomerTier tier,
IReadOnlyList<PromotionRule> activePromotions)
var
// Tier-based discount
switch
0.10
0.15
// Promotion-based discounts
foreach
var
in
if
return
### When to Use Domain Services
from
not
A business rule does not belong to any single entity (e.g., pricing across products and customer tiers)
- External policy or configuration drives the logic (e.g., tax calculation rules)
Domain services should remain **pure** -- no infrastructure dependencies. If the logic needs a database or external API,
place it in an application service that calls the domain service with pre-loaded data.
---
## Repository Contracts
Repository interfaces belong in the **domain layer** and express aggregate loading and saving semantics. Implementation
details (EF Core, Dapper) live in the infrastructure layer.
```csharp
// Domain layer -- defines the contractpublicinterface IOrderRepository
Task AddAsync(Order order, CancellationToken ct)
Task SaveChangesAsync(CancellationToken ct)
// Domain layer -- unit of work abstraction (optional)
// Specific domain exceptions for different invariant violations
publicsealedclassInsufficientStockException(
ProductId productId, int requested, int available)
: DomainException($"Insufficient stock for {productId}: " +
$"requested {requested}, available {available}")
public
public
int
public
int
text
Map domain exceptions to HTTP responses at the API boundary (e.g., `DomainException` to 422 Unprocessable Entity). Do
notlet infrastructure concerns like HTTP status codes leak into the domain layer.
---
## Agent Gotchas
1. **Do not expose public setters on aggregate properties** -- all state changes must go through methods on the
aggregate root that enforce invariants. Use `privateset` or `init` for properties.
2. **Do not create navigation properties between aggregate roots** -- reference other aggregates by ID valueobjects
(e.g., `CustomerId`) notby entity navigation. Cross-aggregate navigation breaks bounded context isolation.
3. **Do not dispatch domain events inside the transaction** -- dispatch after `SaveChangesAsync` succeeds. Dispatching
before save means side effects fire even if the save fails.
4. **Do not use domain types in integration events** -- integration events cross bounded context boundaries and must use
primitives or DTOs. Domain type changes would break other services.
5. **Do not put validation logic only in the API layer** -- domain invariants belong in the domain model. API validation
([skill:dotnet-validation-patterns]) catches malformed input