@malamute/ai-rules 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +270 -121
  2. package/bin/cli.js +5 -2
  3. package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
  4. package/configs/_shared/.claude/rules/conventions/git.md +265 -0
  5. package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
  6. package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
  7. package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
  8. package/configs/_shared/.claude/rules/devops/docker.md +275 -0
  9. package/configs/_shared/.claude/rules/devops/nx.md +194 -0
  10. package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
  11. package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
  12. package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
  13. package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
  14. package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
  15. package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
  16. package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
  17. package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
  18. package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
  19. package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
  20. package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
  21. package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
  22. package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
  23. package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
  24. package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
  25. package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
  26. package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
  27. package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
  28. package/configs/_shared/.claude/rules/quality/logging.md +45 -0
  29. package/configs/_shared/.claude/rules/quality/observability.md +240 -0
  30. package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
  31. package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
  32. package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
  33. package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
  34. package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
  35. package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
  36. package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
  37. package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
  38. package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
  39. package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
  40. package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
  41. package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
  42. package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
  43. package/configs/_shared/CLAUDE.md +52 -149
  44. package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
  45. package/configs/angular/.claude/rules/core/resource.md +285 -0
  46. package/configs/angular/.claude/rules/core/signals.md +323 -0
  47. package/configs/angular/.claude/rules/http.md +338 -0
  48. package/configs/angular/.claude/rules/routing.md +291 -0
  49. package/configs/angular/.claude/rules/ssr.md +312 -0
  50. package/configs/angular/.claude/rules/state/signal-store.md +408 -0
  51. package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
  52. package/configs/angular/.claude/rules/testing.md +7 -7
  53. package/configs/angular/.claude/rules/ui/aria.md +422 -0
  54. package/configs/angular/.claude/rules/ui/forms.md +424 -0
  55. package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
  56. package/configs/angular/.claude/settings.json +1 -0
  57. package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
  58. package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
  59. package/configs/angular/CLAUDE.md +24 -216
  60. package/configs/dotnet/.claude/rules/background-services.md +552 -0
  61. package/configs/dotnet/.claude/rules/configuration.md +426 -0
  62. package/configs/dotnet/.claude/rules/ddd.md +447 -0
  63. package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
  64. package/configs/dotnet/.claude/rules/mediatr.md +320 -0
  65. package/configs/dotnet/.claude/rules/middleware.md +489 -0
  66. package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
  67. package/configs/dotnet/.claude/rules/validation.md +388 -0
  68. package/configs/dotnet/.claude/settings.json +21 -3
  69. package/configs/dotnet/CLAUDE.md +53 -286
  70. package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
  71. package/configs/fastapi/.claude/rules/dependencies.md +170 -0
  72. package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
  73. package/configs/fastapi/.claude/rules/lifespan.md +274 -0
  74. package/configs/fastapi/.claude/rules/middleware.md +229 -0
  75. package/configs/fastapi/.claude/rules/pydantic.md +433 -0
  76. package/configs/fastapi/.claude/rules/responses.md +251 -0
  77. package/configs/fastapi/.claude/rules/routers.md +202 -0
  78. package/configs/fastapi/.claude/rules/security.md +222 -0
  79. package/configs/fastapi/.claude/rules/testing.md +251 -0
  80. package/configs/fastapi/.claude/rules/websockets.md +298 -0
  81. package/configs/fastapi/.claude/settings.json +33 -0
  82. package/configs/fastapi/CLAUDE.md +144 -0
  83. package/configs/flask/.claude/rules/blueprints.md +208 -0
  84. package/configs/flask/.claude/rules/cli.md +285 -0
  85. package/configs/flask/.claude/rules/configuration.md +281 -0
  86. package/configs/flask/.claude/rules/context.md +238 -0
  87. package/configs/flask/.claude/rules/error-handlers.md +278 -0
  88. package/configs/flask/.claude/rules/extensions.md +278 -0
  89. package/configs/flask/.claude/rules/flask.md +171 -0
  90. package/configs/flask/.claude/rules/marshmallow.md +206 -0
  91. package/configs/flask/.claude/rules/security.md +267 -0
  92. package/configs/flask/.claude/rules/testing.md +284 -0
  93. package/configs/flask/.claude/settings.json +33 -0
  94. package/configs/flask/CLAUDE.md +166 -0
  95. package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
  96. package/configs/nestjs/.claude/rules/filters.md +376 -0
  97. package/configs/nestjs/.claude/rules/interceptors.md +317 -0
  98. package/configs/nestjs/.claude/rules/middleware.md +321 -0
  99. package/configs/nestjs/.claude/rules/modules.md +26 -0
  100. package/configs/nestjs/.claude/rules/pipes.md +351 -0
  101. package/configs/nestjs/.claude/rules/websockets.md +451 -0
  102. package/configs/nestjs/.claude/settings.json +16 -2
  103. package/configs/nestjs/CLAUDE.md +57 -215
  104. package/configs/nextjs/.claude/rules/api-routes.md +358 -0
  105. package/configs/nextjs/.claude/rules/authentication.md +355 -0
  106. package/configs/nextjs/.claude/rules/components.md +52 -0
  107. package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
  108. package/configs/nextjs/.claude/rules/database.md +400 -0
  109. package/configs/nextjs/.claude/rules/middleware.md +303 -0
  110. package/configs/nextjs/.claude/rules/routing.md +324 -0
  111. package/configs/nextjs/.claude/rules/seo.md +350 -0
  112. package/configs/nextjs/.claude/rules/server-actions.md +353 -0
  113. package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
  114. package/configs/nextjs/.claude/settings.json +5 -0
  115. package/configs/nextjs/CLAUDE.md +69 -331
  116. package/package.json +23 -9
  117. package/src/cli.js +220 -0
  118. package/src/config.js +29 -0
  119. package/src/index.js +13 -0
  120. package/src/installer.js +361 -0
  121. package/src/merge.js +116 -0
  122. package/src/tech-config.json +29 -0
  123. package/src/utils.js +96 -0
  124. package/configs/python/.claude/rules/flask.md +0 -332
  125. package/configs/python/.claude/settings.json +0 -18
  126. package/configs/python/CLAUDE.md +0 -273
  127. package/src/install.js +0 -315
  128. /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
  129. /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
  130. /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
  131. /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
  132. /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
  133. /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
@@ -0,0 +1,447 @@
1
+ ---
2
+ paths:
3
+ - "src/Domain/**/*.cs"
4
+ - "src/**/Entities/**/*.cs"
5
+ - "src/**/ValueObjects/**/*.cs"
6
+ - "src/**/Aggregates/**/*.cs"
7
+ ---
8
+
9
+ # Domain-Driven Design (.NET)
10
+
11
+ ## Entity Base Class
12
+
13
+ ```csharp
14
+ // Domain/Common/Entity.cs
15
+ public abstract class Entity : IEquatable<Entity>
16
+ {
17
+ protected Entity(Guid id) => Id = id;
18
+ protected Entity() { } // For EF Core
19
+
20
+ public Guid Id { get; protected init; }
21
+
22
+ private readonly List<IDomainEvent> _domainEvents = [];
23
+ public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
24
+
25
+ public void AddDomainEvent(IDomainEvent domainEvent) =>
26
+ _domainEvents.Add(domainEvent);
27
+
28
+ public void ClearDomainEvents() =>
29
+ _domainEvents.Clear();
30
+
31
+ public override bool Equals(object? obj) =>
32
+ obj is Entity entity && Equals(entity);
33
+
34
+ public bool Equals(Entity? other) =>
35
+ other is not null && Id == other.Id;
36
+
37
+ public override int GetHashCode() =>
38
+ Id.GetHashCode();
39
+
40
+ public static bool operator ==(Entity? left, Entity? right) =>
41
+ left?.Equals(right) ?? right is null;
42
+
43
+ public static bool operator !=(Entity? left, Entity? right) =>
44
+ !(left == right);
45
+ }
46
+ ```
47
+
48
+ ## Aggregate Root
49
+
50
+ ```csharp
51
+ // Domain/Common/AggregateRoot.cs
52
+ public abstract class AggregateRoot : Entity
53
+ {
54
+ protected AggregateRoot(Guid id) : base(id) { }
55
+ protected AggregateRoot() { }
56
+
57
+ // Aggregate roots control all access to child entities
58
+ // Only aggregate roots are returned from repositories
59
+ }
60
+
61
+ // Domain/Orders/Order.cs (Aggregate Root)
62
+ public class Order : AggregateRoot
63
+ {
64
+ private readonly List<OrderLine> _lines = [];
65
+
66
+ private Order(Guid id, Guid customerId) : base(id)
67
+ {
68
+ CustomerId = customerId;
69
+ Status = OrderStatus.Draft;
70
+ CreatedAt = DateTime.UtcNow;
71
+ }
72
+
73
+ public Guid CustomerId { get; }
74
+ public OrderStatus Status { get; private set; }
75
+ public DateTime CreatedAt { get; }
76
+ public DateTime? ConfirmedAt { get; private set; }
77
+ public IReadOnlyList<OrderLine> Lines => _lines.AsReadOnly();
78
+
79
+ public Money Total => _lines
80
+ .Select(l => l.Subtotal)
81
+ .Aggregate(Money.Zero, (acc, m) => acc + m);
82
+
83
+ public static Result<Order> Create(Guid customerId)
84
+ {
85
+ if (customerId == Guid.Empty)
86
+ {
87
+ return Result.Failure<Order>(Error.Validation("CustomerId", "Invalid customer"));
88
+ }
89
+
90
+ return new Order(Guid.NewGuid(), customerId);
91
+ }
92
+
93
+ public Result AddLine(Product product, int quantity)
94
+ {
95
+ if (Status != OrderStatus.Draft)
96
+ {
97
+ return Result.Failure(OrderErrors.CannotModifyConfirmedOrder);
98
+ }
99
+
100
+ if (quantity <= 0)
101
+ {
102
+ return Result.Failure(Error.Validation("Quantity", "Quantity must be positive"));
103
+ }
104
+
105
+ var existingLine = _lines.FirstOrDefault(l => l.ProductId == product.Id);
106
+ if (existingLine is not null)
107
+ {
108
+ existingLine.UpdateQuantity(existingLine.Quantity + quantity);
109
+ }
110
+ else
111
+ {
112
+ _lines.Add(OrderLine.Create(product, quantity));
113
+ }
114
+
115
+ return Result.Success();
116
+ }
117
+
118
+ public Result RemoveLine(Guid productId)
119
+ {
120
+ if (Status != OrderStatus.Draft)
121
+ {
122
+ return Result.Failure(OrderErrors.CannotModifyConfirmedOrder);
123
+ }
124
+
125
+ var line = _lines.FirstOrDefault(l => l.ProductId == productId);
126
+ if (line is null)
127
+ {
128
+ return Result.Failure(Error.NotFound("OrderLine", productId));
129
+ }
130
+
131
+ _lines.Remove(line);
132
+ return Result.Success();
133
+ }
134
+
135
+ public Result Confirm()
136
+ {
137
+ if (Status != OrderStatus.Draft)
138
+ {
139
+ return Result.Failure(OrderErrors.AlreadyConfirmed);
140
+ }
141
+
142
+ if (!_lines.Any())
143
+ {
144
+ return Result.Failure(OrderErrors.EmptyOrder);
145
+ }
146
+
147
+ Status = OrderStatus.Confirmed;
148
+ ConfirmedAt = DateTime.UtcNow;
149
+
150
+ AddDomainEvent(new OrderConfirmedEvent(Id, CustomerId, Total));
151
+
152
+ return Result.Success();
153
+ }
154
+ }
155
+ ```
156
+
157
+ ## Value Object
158
+
159
+ ```csharp
160
+ // Domain/Common/ValueObject.cs
161
+ public abstract class ValueObject : IEquatable<ValueObject>
162
+ {
163
+ protected abstract IEnumerable<object> GetAtomicValues();
164
+
165
+ public override bool Equals(object? obj) =>
166
+ obj is ValueObject other && Equals(other);
167
+
168
+ public bool Equals(ValueObject? other) =>
169
+ other is not null &&
170
+ GetAtomicValues().SequenceEqual(other.GetAtomicValues());
171
+
172
+ public override int GetHashCode() =>
173
+ GetAtomicValues()
174
+ .Aggregate(default(int), HashCode.Combine);
175
+
176
+ public static bool operator ==(ValueObject? left, ValueObject? right) =>
177
+ left?.Equals(right) ?? right is null;
178
+
179
+ public static bool operator !=(ValueObject? left, ValueObject? right) =>
180
+ !(left == right);
181
+ }
182
+
183
+ // Domain/Common/ValueObjects/Money.cs
184
+ public sealed class Money : ValueObject
185
+ {
186
+ private Money(decimal amount, string currency)
187
+ {
188
+ Amount = amount;
189
+ Currency = currency;
190
+ }
191
+
192
+ public decimal Amount { get; }
193
+ public string Currency { get; }
194
+
195
+ public static Money Zero => new(0, "USD");
196
+
197
+ public static Result<Money> Create(decimal amount, string currency = "USD")
198
+ {
199
+ if (amount < 0)
200
+ {
201
+ return Result.Failure<Money>(Error.Validation("Amount", "Amount cannot be negative"));
202
+ }
203
+
204
+ if (string.IsNullOrWhiteSpace(currency) || currency.Length != 3)
205
+ {
206
+ return Result.Failure<Money>(Error.Validation("Currency", "Invalid currency code"));
207
+ }
208
+
209
+ return new Money(Math.Round(amount, 2), currency.ToUpperInvariant());
210
+ }
211
+
212
+ public static Money operator +(Money left, Money right)
213
+ {
214
+ if (left.Currency != right.Currency)
215
+ throw new InvalidOperationException("Cannot add different currencies");
216
+
217
+ return new Money(left.Amount + right.Amount, left.Currency);
218
+ }
219
+
220
+ public static Money operator *(Money money, int quantity) =>
221
+ new(money.Amount * quantity, money.Currency);
222
+
223
+ protected override IEnumerable<object> GetAtomicValues()
224
+ {
225
+ yield return Amount;
226
+ yield return Currency;
227
+ }
228
+ }
229
+
230
+ // Domain/Common/ValueObjects/Address.cs
231
+ public sealed class Address : ValueObject
232
+ {
233
+ private Address(string street, string city, string postalCode, string country)
234
+ {
235
+ Street = street;
236
+ City = city;
237
+ PostalCode = postalCode;
238
+ Country = country;
239
+ }
240
+
241
+ public string Street { get; }
242
+ public string City { get; }
243
+ public string PostalCode { get; }
244
+ public string Country { get; }
245
+
246
+ public static Result<Address> Create(
247
+ string street, string city, string postalCode, string country)
248
+ {
249
+ if (string.IsNullOrWhiteSpace(street))
250
+ return Result.Failure<Address>(Error.Validation("Street", "Street is required"));
251
+
252
+ if (string.IsNullOrWhiteSpace(city))
253
+ return Result.Failure<Address>(Error.Validation("City", "City is required"));
254
+
255
+ if (string.IsNullOrWhiteSpace(postalCode))
256
+ return Result.Failure<Address>(Error.Validation("PostalCode", "Postal code is required"));
257
+
258
+ if (string.IsNullOrWhiteSpace(country))
259
+ return Result.Failure<Address>(Error.Validation("Country", "Country is required"));
260
+
261
+ return new Address(street.Trim(), city.Trim(), postalCode.Trim(), country.Trim().ToUpperInvariant());
262
+ }
263
+
264
+ protected override IEnumerable<object> GetAtomicValues()
265
+ {
266
+ yield return Street;
267
+ yield return City;
268
+ yield return PostalCode;
269
+ yield return Country;
270
+ }
271
+ }
272
+ ```
273
+
274
+ ## Domain Events
275
+
276
+ ```csharp
277
+ // Domain/Common/IDomainEvent.cs
278
+ public interface IDomainEvent
279
+ {
280
+ Guid Id { get; }
281
+ DateTime OccurredAt { get; }
282
+ }
283
+
284
+ public abstract record DomainEvent : IDomainEvent
285
+ {
286
+ public Guid Id { get; } = Guid.NewGuid();
287
+ public DateTime OccurredAt { get; } = DateTime.UtcNow;
288
+ }
289
+
290
+ // Domain/Orders/Events/OrderConfirmedEvent.cs
291
+ public sealed record OrderConfirmedEvent(
292
+ Guid OrderId,
293
+ Guid CustomerId,
294
+ Money Total
295
+ ) : DomainEvent;
296
+
297
+ // Publish events via MediatR after SaveChanges
298
+ public class DomainEventDispatcher(IPublisher publisher)
299
+ {
300
+ public async Task DispatchEventsAsync(IEnumerable<Entity> entities, CancellationToken ct)
301
+ {
302
+ var domainEvents = entities
303
+ .SelectMany(e => e.DomainEvents)
304
+ .ToList();
305
+
306
+ foreach (var entity in entities)
307
+ {
308
+ entity.ClearDomainEvents();
309
+ }
310
+
311
+ foreach (var domainEvent in domainEvents)
312
+ {
313
+ await publisher.Publish(domainEvent, ct);
314
+ }
315
+ }
316
+ }
317
+ ```
318
+
319
+ ## Repository Interface (Domain Layer)
320
+
321
+ ```csharp
322
+ // Domain/Common/IRepository.cs
323
+ public interface IRepository<T> where T : AggregateRoot
324
+ {
325
+ Task<T?> GetByIdAsync(Guid id, CancellationToken ct = default);
326
+ Task AddAsync(T entity, CancellationToken ct = default);
327
+ void Update(T entity);
328
+ void Remove(T entity);
329
+ }
330
+
331
+ // Domain/Orders/IOrderRepository.cs
332
+ public interface IOrderRepository : IRepository<Order>
333
+ {
334
+ Task<IReadOnlyList<Order>> GetByCustomerIdAsync(Guid customerId, CancellationToken ct = default);
335
+ Task<Order?> GetWithLinesAsync(Guid orderId, CancellationToken ct = default);
336
+ }
337
+
338
+ // Implementation in Infrastructure layer
339
+ public class OrderRepository(ApplicationDbContext context) : IOrderRepository
340
+ {
341
+ public async Task<Order?> GetByIdAsync(Guid id, CancellationToken ct) =>
342
+ await context.Orders.FindAsync([id], ct);
343
+
344
+ public async Task<Order?> GetWithLinesAsync(Guid orderId, CancellationToken ct) =>
345
+ await context.Orders
346
+ .Include(o => o.Lines)
347
+ .FirstOrDefaultAsync(o => o.Id == orderId, ct);
348
+
349
+ public async Task AddAsync(Order entity, CancellationToken ct) =>
350
+ await context.Orders.AddAsync(entity, ct);
351
+
352
+ public void Update(Order entity) =>
353
+ context.Orders.Update(entity);
354
+
355
+ public void Remove(Order entity) =>
356
+ context.Orders.Remove(entity);
357
+
358
+ public async Task<IReadOnlyList<Order>> GetByCustomerIdAsync(Guid customerId, CancellationToken ct) =>
359
+ await context.Orders
360
+ .Where(o => o.CustomerId == customerId)
361
+ .ToListAsync(ct);
362
+ }
363
+ ```
364
+
365
+ ## Domain Service
366
+
367
+ ```csharp
368
+ // Domain/Orders/Services/OrderPricingService.cs
369
+ public class OrderPricingService
370
+ {
371
+ public Result<Money> CalculateDiscount(Order order, Customer customer)
372
+ {
373
+ var total = order.Total;
374
+ var discountPercent = customer.Tier switch
375
+ {
376
+ CustomerTier.Gold => 10,
377
+ CustomerTier.Silver => 5,
378
+ CustomerTier.Bronze => 2,
379
+ _ => 0
380
+ };
381
+
382
+ if (total.Amount > 1000)
383
+ {
384
+ discountPercent += 5; // Additional bulk discount
385
+ }
386
+
387
+ var discountAmount = total.Amount * discountPercent / 100;
388
+ return Money.Create(discountAmount, total.Currency);
389
+ }
390
+ }
391
+ ```
392
+
393
+ ## Anti-Patterns
394
+
395
+ ```csharp
396
+ // BAD: Anemic domain model
397
+ public class Order
398
+ {
399
+ public Guid Id { get; set; }
400
+ public OrderStatus Status { get; set; } // Public setter!
401
+ public List<OrderLine> Lines { get; set; } = []; // Exposed collection!
402
+ }
403
+
404
+ // Service with all logic
405
+ public class OrderService
406
+ {
407
+ public void Confirm(Order order)
408
+ {
409
+ if (order.Status != OrderStatus.Draft) throw new Exception("...");
410
+ order.Status = OrderStatus.Confirmed; // Logic outside entity!
411
+ }
412
+ }
413
+
414
+
415
+ // GOOD: Rich domain model
416
+ public class Order
417
+ {
418
+ private readonly List<OrderLine> _lines = [];
419
+
420
+ public OrderStatus Status { get; private set; } // Private setter
421
+ public IReadOnlyList<OrderLine> Lines => _lines.AsReadOnly(); // Encapsulated
422
+
423
+ public Result Confirm() // Behavior inside entity
424
+ {
425
+ if (Status != OrderStatus.Draft)
426
+ return Result.Failure(OrderErrors.AlreadyConfirmed);
427
+
428
+ Status = OrderStatus.Confirmed;
429
+ AddDomainEvent(new OrderConfirmedEvent(Id));
430
+ return Result.Success();
431
+ }
432
+ }
433
+
434
+
435
+ // BAD: Exposing IQueryable from repository
436
+ public interface IOrderRepository
437
+ {
438
+ IQueryable<Order> GetAll(); // Leaks persistence!
439
+ }
440
+
441
+ // GOOD: Specific query methods
442
+ public interface IOrderRepository
443
+ {
444
+ Task<Order?> GetByIdAsync(Guid id, CancellationToken ct);
445
+ Task<IReadOnlyList<Order>> GetByStatusAsync(OrderStatus status, CancellationToken ct);
446
+ }
447
+ ```