-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathddd-patterns.mdc
More file actions
244 lines (200 loc) · 7.68 KB
/
ddd-patterns.mdc
File metadata and controls
244 lines (200 loc) · 7.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
---
description: "ABP DDD patterns - Entities, Aggregate Roots, Repositories, Domain Services"
globs:
- "**/*.Domain/**/*.cs"
- "**/Domain/**/*.cs"
- "**/Entities/**/*.cs"
alwaysApply: false
---
# ABP DDD Patterns
> **Docs**: https://abp.io/docs/latest/framework/architecture/domain-driven-design
## Rich Domain Model vs Anemic Domain Model
ABP promotes **Rich Domain Model** pattern where entities contain both data AND behavior:
| Anemic (Anti-pattern) | Rich (Recommended) |
|----------------------|-------------------|
| Entity = data only | Entity = data + behavior |
| Logic in services | Logic in entity methods |
| Public setters | Private setters with methods |
| No validation in entity | Entity enforces invariants |
**Encapsulation is key**: Protect entity state by using private setters and exposing behavior through methods.
## Entities
### Entity Example (Rich Model)
```csharp
public class OrderLine : Entity<Guid>
{
public Guid ProductId { get; private set; }
public int Count { get; private set; }
public decimal Price { get; private set; }
protected OrderLine() { } // For ORM
internal OrderLine(Guid id, Guid productId, int count, decimal price) : base(id)
{
ProductId = productId;
SetCount(count); // Validates through method
Price = price;
}
public void SetCount(int count)
{
if (count <= 0)
throw new BusinessException("Orders:InvalidCount");
Count = count;
}
}
```
## Aggregate Roots
Aggregate roots are consistency boundaries that:
- Own their child entities
- Enforce business rules
- Publish domain events
```csharp
public class Order : AggregateRoot<Guid>
{
public string OrderNumber { get; private set; }
public Guid CustomerId { get; private set; }
public OrderStatus Status { get; private set; }
public ICollection<OrderLine> Lines { get; private set; }
protected Order() { } // For ORM
public Order(Guid id, string orderNumber, Guid customerId) : base(id)
{
OrderNumber = Check.NotNullOrWhiteSpace(orderNumber, nameof(orderNumber));
CustomerId = customerId;
Status = OrderStatus.Created;
Lines = new List<OrderLine>();
}
public void AddLine(Guid lineId, Guid productId, int count, decimal price)
{
// Business rule: Can only add lines to created orders
if (Status != OrderStatus.Created)
throw new BusinessException("Orders:CannotModifyOrder");
Lines.Add(new OrderLine(lineId, productId, count, price));
}
public void Complete()
{
if (Status != OrderStatus.Created)
throw new BusinessException("Orders:CannotCompleteOrder");
Status = OrderStatus.Completed;
// Publish events for side effects
AddLocalEvent(new OrderCompletedEvent(Id)); // Same transaction
AddDistributedEvent(new OrderCompletedEto { OrderId = Id }); // Cross-service
}
}
```
### Domain Events
- `AddLocalEvent()` - Handled within same transaction, can access full entity
- `AddDistributedEvent()` - Handled asynchronously, use ETOs (Event Transfer Objects)
### Entity Best Practices
- **Encapsulation**: Private setters, public methods that enforce rules
- **Primary constructor**: Enforce invariants, accept `id` parameter
- **Protected parameterless constructor**: Required for ORM
- **Initialize collections**: In primary constructor
- **Virtual members**: For ORM proxy compatibility
- **Reference by Id**: Don't add navigation properties to other aggregates
- **Don't generate GUID in constructor**: Use `IGuidGenerator` externally
## Repository Pattern
### When to Use Custom Repository
- **Generic repository** (`IRepository<T, TKey>`): Sufficient for simple CRUD operations
- **Custom repository**: Only when you need custom query methods
### Interface (Domain Layer)
```csharp
// Define custom interface only when custom queries are needed
public interface IOrderRepository : IRepository<Order, Guid>
{
Task<Order> FindByOrderNumberAsync(string orderNumber, bool includeDetails = false);
Task<List<Order>> GetListByCustomerAsync(Guid customerId, bool includeDetails = false);
}
```
### Repository Best Practices
- **One repository per aggregate root only** - Never create repositories for child entities
- Child entities must be accessed/modified only through their aggregate root
- Creating repositories for child entities breaks data consistency (bypasses aggregate root's business rules)
- In ABP, use `AddDefaultRepositories()` without `includeAllEntities: true` to enforce this
- Define custom repository only when custom queries are needed
- ABP handles `CancellationToken` automatically; add parameter only for explicit cancellation control
- Single entity methods: `includeDetails = true` by default
- List methods: `includeDetails = false` by default
- Don't return projection classes
- Interface in Domain, implementation in data layer
```csharp
// ✅ Correct: Repository for aggregate root (Order)
public interface IOrderRepository : IRepository<Order, Guid> { }
// ❌ Wrong: Repository for child entity (OrderLine)
// OrderLine should only be accessed through Order aggregate
public interface IOrderLineRepository : IRepository<OrderLine, Guid> { } // Don't do this!
```
## Domain Services
Use domain services for business logic that:
- Spans multiple aggregates
- Requires repository queries to enforce rules
```csharp
public class OrderManager : DomainService
{
private readonly IOrderRepository _orderRepository;
private readonly IProductRepository _productRepository;
public OrderManager(
IOrderRepository orderRepository,
IProductRepository productRepository)
{
_orderRepository = orderRepository;
_productRepository = productRepository;
}
public async Task<Order> CreateAsync(string orderNumber, Guid customerId)
{
// Business rule: Order number must be unique
var existing = await _orderRepository.FindByOrderNumberAsync(orderNumber);
if (existing != null)
{
throw new BusinessException("Orders:OrderNumberAlreadyExists")
.WithData("OrderNumber", orderNumber);
}
return new Order(GuidGenerator.Create(), orderNumber, customerId);
}
public async Task AddProductAsync(Order order, Guid productId, int count)
{
var product = await _productRepository.GetAsync(productId);
order.AddLine(productId, count, product.Price);
}
}
```
### Domain Service Best Practices
- Use `*Manager` suffix naming
- No interface by default (create only if needed)
- Accept/return domain objects, not DTOs
- Don't depend on authenticated user - pass values from application layer
- Use base class properties (`GuidGenerator`, `Clock`) instead of injecting these services
## Domain Events
### Local Events
```csharp
// In aggregate
AddLocalEvent(new OrderCompletedEvent(Id));
// Handler
public class OrderCompletedEventHandler : ILocalEventHandler<OrderCompletedEvent>, ITransientDependency
{
public async Task HandleEventAsync(OrderCompletedEvent eventData)
{
// Handle within same transaction
}
}
```
### Distributed Events (ETO)
For inter-module/microservice communication:
```csharp
// In Domain.Shared
[EventName("Orders.OrderCompleted")]
public class OrderCompletedEto
{
public Guid OrderId { get; set; }
public string OrderNumber { get; set; }
}
```
## Specifications
Reusable query conditions:
```csharp
public class CompletedOrdersSpec : Specification<Order>
{
public override Expression<Func<Order, bool>> ToExpression()
{
return o => o.Status == OrderStatus.Completed;
}
}
// Usage
var orders = await _orderRepository.GetListAsync(new CompletedOrdersSpec());
```