siesa-agents 2.1.25 → 2.1.26-dev.1

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 (65) hide show
  1. package/README.md +8 -0
  2. package/bin/install.js +5 -1
  3. package/bmad-core/agents/analyst.md +1 -1
  4. package/bmad-core/agents/architect.md +22 -2
  5. package/bmad-core/agents/backend-agent.md +74 -86
  6. package/bmad-core/agents/bmad-master.md +2 -2
  7. package/bmad-core/agents/bmad-orchestrator.md +2 -2
  8. package/bmad-core/agents/dev.md +1 -1
  9. package/bmad-core/agents/frontend-agent.md +4 -4
  10. package/bmad-core/agents/pm.md +4 -1
  11. package/bmad-core/agents/po.md +4 -1
  12. package/bmad-core/agents/qa.md +1 -1
  13. package/bmad-core/agents/sm.md +1 -1
  14. package/bmad-core/agents/ux-expert.md +1 -1
  15. package/bmad-core/checklists/architect-checklist.md +2 -0
  16. package/bmad-core/checklists/backend-checklist.md +10 -6
  17. package/bmad-core/data/architecture-patterns.md +279 -125
  18. package/bmad-core/data/backend-standards.md +718 -278
  19. package/bmad-core/data/bmad-kb.md +1 -1
  20. package/bmad-core/data/frontend-standards.md +1 -1
  21. package/bmad-core/data/technical-preferences-ux.md +1 -1
  22. package/bmad-core/data/technical-preferences.md +106 -1
  23. package/bmad-core/data/technology-stack.md +155 -1
  24. package/bmad-core/tasks/apply-qa-fixes.md +4 -4
  25. package/bmad-core/tasks/create-doc.md +59 -0
  26. package/bmad-core/tasks/create-entity.md +3 -3
  27. package/bmad-core/tasks/create-next-story.md +1 -1
  28. package/bmad-core/tasks/create-service.md +3 -3
  29. package/bmad-core/tasks/nfr-assess.md +3 -3
  30. package/bmad-core/tasks/qa-gate.md +2 -2
  31. package/bmad-core/tasks/review-story.md +1 -1
  32. package/bmad-core/tasks/scaffold-backend.md +13 -9
  33. package/bmad-core/tasks/scaffold-frontend.md +2 -2
  34. package/bmad-core/templates/architecture-tmpl.yaml +2 -2
  35. package/bmad-core/templates/front-end-architecture-tmpl.yaml +1 -1
  36. package/bmad-core/templates/story-tmpl.yaml +1 -1
  37. package/bmad-core/user-guide.md +1 -1
  38. package/claude/commands/BMad/agents/analyst.md +1 -1
  39. package/claude/commands/BMad/agents/architect.md +21 -1
  40. package/claude/commands/BMad/agents/backend.md +86 -99
  41. package/claude/commands/BMad/agents/bmad-master.md +2 -2
  42. package/claude/commands/BMad/agents/bmad-orchestrator.md +2 -2
  43. package/claude/commands/BMad/agents/dev.md +1 -1
  44. package/claude/commands/BMad/agents/frontend.md +4 -4
  45. package/claude/commands/BMad/agents/pm.md +4 -1
  46. package/claude/commands/BMad/agents/po.md +4 -1
  47. package/claude/commands/BMad/agents/qa.md +1 -1
  48. package/claude/commands/BMad/agents/sm.md +1 -1
  49. package/claude/commands/BMad/agents/ux-expert.md +1 -1
  50. package/claude/commands/BMad/tasks/apply-qa-fixes.md +4 -4
  51. package/claude/commands/BMad/tasks/create-doc.md +48 -0
  52. package/claude/commands/BMad/tasks/create-next-story.md +1 -1
  53. package/claude/commands/BMad/tasks/nfr-assess.md +3 -3
  54. package/claude/commands/BMad/tasks/qa-gate.md +2 -2
  55. package/claude/commands/BMad/tasks/review-story.md +1 -1
  56. package/claude/hooks/file-restriction-hook.py +51 -0
  57. package/claude/hooks/track-agent.py +67 -0
  58. package/claude/settings.local.json +55 -2
  59. package/github/chatmodes/architect.chatmode.md +7 -1
  60. package/github/chatmodes/backend.chatmode.md +133 -109
  61. package/github/chatmodes/dev.chatmode.md +12 -4
  62. package/github/chatmodes/frontend.chatmode.md +3 -3
  63. package/github/chatmodes/qa.chatmode.md +9 -3
  64. package/kiro/steering/agent-detection.md +16 -16
  65. package/package.json +1 -1
@@ -1,332 +1,648 @@
1
1
  # Backend Development Standards
2
2
 
3
- > **Note**: For architecture patterns and principles (Hexagonal Architecture, DDD, folder structure), see [architecture-patterns.md](./architecture-patterns.md)
3
+ > **Note**: For architecture patterns and principles (Clean Architecture, DDD, folder structure), see [architecture-patterns.md](./architecture-patterns.md)
4
4
 
5
5
  ## Technology Stack Standards
6
6
 
7
7
  ### Core Technologies
8
- - **NestJS**: 10+ with TypeScript and decorators
9
- - **TypeScript**: Strict mode enabled, no `any` types
10
- - **Prisma**: ORM for database operations (no raw queries allowed)
11
- - **Jest + Supertest**: Unit, integration, and E2E testing
12
- - **Class-validator + Class-transformer**: DTO validation
8
+ - **.NET 10**: Main framework with C#
9
+ - **C# Minimal API**: Lightweight and modern approach for APIs
10
+ - **Entity Framework Core 10**: ORM for database operations
11
+ - **xUnit**: Unit and integration testing framework
12
+ - **FluentValidation**: Validation for DTOs and models
13
13
 
14
14
  ### Framework Standards
15
- - **Default Framework**: NestJS 10+ with TypeScript
16
- - **Database**: Prisma ORM only - no raw SQL queries
17
- - **Testing**: TDD approach with comprehensive test coverage
18
- - **Documentation**: Swagger/OpenAPI auto-generated from decorators
19
- - **Messaging**: NestJS Microservices (Redis, RabbitMQ, or gRPC)
15
+ - **Default Framework**: .NET 10 with C# Minimal API
16
+ - **Database**: PostgreSQL with Entity Framework Core 10 (mandatory)
17
+ - **ORM Extensions**:
18
+ - **linq2db**: For highly optimized complex queries requiring maximum performance
19
+ - **DynamicLinq**: For runtime dynamic filters from user input or configurable scenarios
20
+ - **LinqKit**: For composable type-safe predicates in DDD repositories with complex business rules
21
+ - **Testing**: TDD approach with xUnit and high test coverage
22
+ - **Documentation**: Scalar (NO Swagger) auto-generated
23
+ - **Primary Keys**: UUID (Guid) for all entities
24
+
25
+ ## CRITICAL: DateTime Type Standards
26
+
27
+ ### ⚠️ Mandatory Rule: Use DateTimeOffset for Timestamps
28
+
29
+ **ALWAYS use `DateTimeOffset` instead of `DateTime`** for all timestamp fields in entities, DTOs, and database models.
30
+
31
+ #### Why DateTimeOffset?
32
+
33
+ - **PostgreSQL Compatibility**: PostgreSQL uses `TIMESTAMP WITH TIME ZONE` which maps correctly to `DateTimeOffset` but NOT to `DateTime`
34
+ - **Timezone Awareness**: `DateTimeOffset` preserves timezone information, preventing timezone conversion issues
35
+ - **UTC Consistency**: Store all timestamps in UTC for consistency across different timezones
36
+ - **Avoiding Bugs**: Using `DateTime` with PostgreSQL causes:
37
+ - Data loss during timezone conversions
38
+ - Inconsistent timestamp comparisons
39
+ - Ambiguous datetime values during DST transitions
40
+
41
+ #### Correct Usage
42
+
43
+ ```csharp
44
+ // ✅ CORRECT - Use DateTimeOffset for timestamps
45
+ public class UserEntity : AggregateRoot
46
+ {
47
+ public Guid Id { get; private set; }
48
+ public string Email { get; private set; }
49
+ public DateTimeOffset CreatedAt { get; private set; } = DateTimeOffset.UtcNow;
50
+ public DateTimeOffset UpdatedAt { get; private set; } = DateTimeOffset.UtcNow;
51
+ }
52
+
53
+ // ✅ CORRECT - Use DateOnly for date-only fields
54
+ public class ExpenseEntity : AggregateRoot
55
+ {
56
+ public Guid Id { get; private set; }
57
+ public decimal Amount { get; private set; }
58
+ public DateOnly ExpenseDate { get; private set; } // Date without time
59
+ public DateTimeOffset CreatedAt { get; private set; } = DateTimeOffset.UtcNow;
60
+ }
61
+
62
+ // ❌ WRONG - Never use DateTime for timestamp fields
63
+ public class UserEntity : AggregateRoot
64
+ {
65
+ public DateTime CreatedAt { get; private set; } // ❌ WRONG - causes PostgreSQL issues
66
+ public DateTime UpdatedAt { get; private set; } // ❌ WRONG - causes PostgreSQL issues
67
+ }
68
+ ```
69
+
70
+ #### Entity Framework Core Configuration
71
+
72
+ ```csharp
73
+ public class UserEntityConfiguration : IEntityTypeConfiguration<UserEntity>
74
+ {
75
+ public void Configure(EntityTypeBuilder<UserEntity> builder)
76
+ {
77
+ builder.ToTable("users");
78
+
79
+ // DateTimeOffset fields - automatically map to TIMESTAMP WITH TIME ZONE
80
+ builder.Property(u => u.CreatedAt)
81
+ .IsRequired()
82
+ .HasDefaultValueSql("NOW()"); // PostgreSQL auto-generates UTC timestamp
83
+
84
+ builder.Property(u => u.UpdatedAt)
85
+ .IsRequired()
86
+ .HasDefaultValueSql("NOW()");
87
+ }
88
+ }
89
+ ```
90
+
91
+ #### Type Selection Guide
92
+
93
+ | Use Case | Type | Example |
94
+ |----------|------|---------|
95
+ | Timestamp with timezone | `DateTimeOffset` | `CreatedAt`, `UpdatedAt`, `DeletedAt`, `LastLoginAt` |
96
+ | Date only (no time) | `DateOnly` | `BirthDate`, `ExpenseDate`, `DueDate` |
97
+ | Time only (no date) | `TimeOnly` | `OpeningTime`, `ClosingTime` |
98
+ | Never use | `DateTime` | ❌ Not compatible with PostgreSQL TIMESTAMP WITH TIME ZONE |
99
+
100
+ #### Best Practices
101
+
102
+ 1. **Always use `DateTimeOffset`** for `CreatedAt`, `UpdatedAt`, and any timestamp field
103
+ 2. **Always use UTC**: Set values with `DateTimeOffset.UtcNow`
104
+ 3. **Configure EF Core**: Use `HasDefaultValueSql("NOW()")` for automatic timestamps
105
+ 4. **DTOs must match**: DTOs also use `DateTimeOffset` (serializes to ISO 8601 with timezone)
106
+ 5. **Never use `DateTime`** for entity timestamp fields in PostgreSQL projects
107
+
108
+ ### ORM Strategy Guidelines
109
+
110
+ #### When to Use Entity Framework Core 10
111
+ ✔ **Primary ORM for:**
112
+ - Standard CRUD operations
113
+ - DDD entities with tracking (Aggregate Roots, Domain Events)
114
+ - Simple to moderate queries
115
+ - Navigation properties and relationships
116
+ - Change tracking scenarios
117
+
118
+ #### When to Use linq2db
119
+ ✔ **Use for maximum performance:**
120
+ - Highly optimized complex queries (subqueries, complex projections, multiple joins)
121
+ - Queries that EF Core cannot translate efficiently
122
+ - High-performance endpoints: dashboards, analytics, reports, high data volumes
123
+ - Pure queries without tracking needs
124
+ - When EF Core generates inefficient SQL
125
+
126
+ 🚫 **Avoid when:**
127
+ - Need complete DDD entities with aggregate lifecycle
128
+ - Query participates in domain events or tracking
129
+
130
+ #### When to Use DynamicLinq
131
+ ✔ **Use for runtime flexibility:**
132
+ - Dynamic filter systems: `?filter=Age > 30 AND Country == "CO"`
133
+ - Simplified OData-style APIs
134
+ - Configurable grids or data explorers
135
+ - Variable column searches
136
+
137
+ 🚫 **Avoid when:**
138
+ - Filters are fully controlled in code
139
+ - Require strict security (needs sanitization)
140
+ - Need maximum performance (use linq2db or LinqKit)
141
+
142
+ #### When to Use LinqKit
143
+ ✔ **Use for type-safe composition:**
144
+ - Complex business rules in queries with composable predicates
145
+ - Dynamic filters with type safety (no strings → expressions)
146
+ - DDD repositories with multiple criteria, conditional searches, reusable queries
147
+ - Enhance EF Core when composing complex expressions
148
+ - 100% compatible with EF Core translation
149
+
150
+ 🚫 **Avoid when:**
151
+ - Filters are extremely simple
152
+ - Query is highly dynamic and text-based (use DynamicLinq)
153
+ - Need maximum raw performance (use linq2db)
20
154
 
21
155
  ### Development Tools
22
- - **Nx**: MonoRepo management and build orchestration
23
- - **ESLint + Prettier**: Code quality and formatting
24
- - **Husky**: Pre-commit hooks for quality gates
25
- - **Winston**: Structured logging with log levels
26
- - **Redis**: Caching, session storage, and message transport
27
- - **Passport + JWT**: Authentication and authorization
156
+ - **dotnet CLI**: Project management, build, test, and restore
157
+ - **NuGet**: Package manager
158
+ - **EF Core 10 Migrations**: Database schema version control
159
+ - **Docker**: For production only
160
+ - **Code Analyzers**: Static C# code analysis
161
+ - **EditorConfig**: Code formatting standards
28
162
 
29
163
  ## Domain-Driven Design Implementation
30
164
 
31
165
  ### Entity Structure
32
- ```typescript
33
- export class UserEntity extends AggregateRoot {
34
- private constructor(
35
- public readonly id: UserId,
36
- private _email: EmailValueObject,
37
- private _name: NameValueObject,
38
- ) {
39
- super();
40
- }
166
+ ```csharp
167
+ public class UserEntity : AggregateRoot
168
+ {
169
+ // Parameterless constructor for EF Core (materialization from DB)
170
+ private UserEntity() : base(Guid.Empty)
171
+ {
172
+ // EF Core needs this to reconstruct the entity from the database
173
+ }
41
174
 
42
- static create(props: CreateUserProps): UserEntity {
43
- const user = new UserEntity(
44
- UserId.generate(),
45
- EmailValueObject.create(props.email),
46
- NameValueObject.create(props.name),
47
- );
48
- user.addDomainEvent(new UserCreatedEvent(user.id));
49
- return user;
50
- }
175
+ private UserEntity(
176
+ Guid id,
177
+ EmailValueObject email,
178
+ NameValueObject name) : base(id)
179
+ {
180
+ Email = email;
181
+ Name = name;
182
+ }
51
183
 
52
- public updateEmail(newEmail: EmailValueObject): void {
53
- if (this._email.equals(newEmail)) return;
54
- this._email = newEmail;
55
- this.addDomainEvent(new UserEmailUpdatedEvent(this.id, newEmail));
56
- }
184
+ public EmailValueObject Email { get; private set; } = null!;
185
+ public NameValueObject Name { get; private set; } = null!;
186
+
187
+ public static UserEntity Create(string email, string name)
188
+ {
189
+ var userEmail = EmailValueObject.Create(email);
190
+ var userName = NameValueObject.Create(name);
191
+
192
+ var user = new UserEntity(Guid.NewGuid(), userEmail, userName);
193
+ user.AddDomainEvent(new UserCreatedEvent(user.Id));
194
+
195
+ return user;
196
+ }
57
197
 
58
- get email(): EmailValueObject {
59
- return this._email;
60
- }
198
+ public void UpdateEmail(string newEmail)
199
+ {
200
+ var newEmailVO = EmailValueObject.Create(newEmail);
201
+ if (Email.Equals(newEmailVO)) return;
202
+
203
+ Email = newEmailVO;
204
+ AddDomainEvent(new UserEmailUpdatedEvent(Id, Email));
205
+ }
61
206
  }
62
207
  ```
63
208
 
64
209
  ### Value Object Structure
65
- ```typescript
66
- export class EmailValueObject {
67
- private constructor(private readonly value: string) {
68
- this.validate(value);
69
- }
210
+ ```csharp
211
+ public class EmailValueObject : ValueObject
212
+ {
213
+ public string Value { get; }
70
214
 
71
- static create(value: string): EmailValueObject {
72
- return new EmailValueObject(value);
73
- }
215
+ private EmailValueObject(string value)
216
+ {
217
+ Value = value;
218
+ }
74
219
 
75
- private validate(value: string): void {
76
- if (!value || !this.isValidEmail(value)) {
77
- throw new InvalidEmailException(value);
220
+ public static EmailValueObject Create(string value)
221
+ {
222
+ if (string.IsNullOrWhiteSpace(value))
223
+ throw new ArgumentException("Email cannot be empty", nameof(value));
224
+
225
+ if (!IsValidEmail(value))
226
+ throw new InvalidEmailException(value);
227
+
228
+ return new EmailValueObject(value);
78
229
  }
79
- }
80
230
 
81
- equals(other: EmailValueObject): boolean {
82
- return this.value === other.value;
83
- }
231
+ private static bool IsValidEmail(string email)
232
+ {
233
+ // Email validation logic
234
+ return System.Text.RegularExpressions.Regex.IsMatch(
235
+ email,
236
+ @"^[^@\s]+@[^@\s]+\.[^@\s]+$");
237
+ }
84
238
 
85
- toString(): string {
86
- return this.value;
87
- }
239
+ protected override IEnumerable<object> GetEqualityComponents()
240
+ {
241
+ yield return Value;
242
+ }
88
243
  }
89
244
  ```
90
245
 
91
246
  ### Repository Pattern
92
- ```typescript
93
- // Interface (in application/ports/repositories)
94
- export interface UserRepositoryInterface {
95
- save(user: UserEntity): Promise<UserEntity>;
96
- findById(id: UserId): Promise<UserEntity | null>;
97
- findByEmail(email: EmailValueObject): Promise<UserEntity | null>;
98
- delete(id: UserId): Promise<void>;
247
+ ```csharp
248
+ // Interface (in Application layer)
249
+ public interface IUserRepository
250
+ {
251
+ Task<UserEntity> SaveAsync(UserEntity user, CancellationToken cancellationToken = default);
252
+ Task<UserEntity?> FindByIdAsync(Guid id, CancellationToken cancellationToken = default);
253
+ Task<UserEntity?> FindByEmailAsync(EmailValueObject email, CancellationToken cancellationToken = default);
254
+ Task DeleteAsync(Guid id, CancellationToken cancellationToken = default);
99
255
  }
100
256
 
101
- // Implementation (in infrastructure/repositories)
102
- @Injectable()
103
- export class PrismaUserRepository implements UserRepositoryInterface {
104
- constructor(private readonly prisma: PrismaService) {}
105
-
106
- async save(user: UserEntity): Promise<UserEntity> {
107
- const data = {
108
- id: user.id.toString(),
109
- email: user.email.toString(),
110
- name: user.name.toString(),
111
- };
112
-
113
- const savedUser = await this.prisma.user.upsert({
114
- where: { id: data.id },
115
- update: data,
116
- create: data,
117
- });
118
-
119
- return this.toDomain(savedUser);
120
- }
257
+ // Implementation (in Infrastructure layer)
258
+ public class UserRepository : IUserRepository
259
+ {
260
+ private readonly ApplicationDbContext _context;
121
261
 
122
- private toDomain(prismaUser: User): UserEntity {
123
- return UserEntity.reconstitute({
124
- id: UserId.create(prismaUser.id),
125
- email: EmailValueObject.create(prismaUser.email),
126
- name: NameValueObject.create(prismaUser.name),
127
- });
128
- }
262
+ public UserRepository(ApplicationDbContext context)
263
+ {
264
+ _context = context;
265
+ }
266
+
267
+ public async Task<UserEntity> SaveAsync(UserEntity user, CancellationToken cancellationToken = default)
268
+ {
269
+ var existingUser = await _context.Users.FindAsync(new object[] { user.Id }, cancellationToken);
270
+
271
+ if (existingUser == null)
272
+ await _context.Users.AddAsync(user, cancellationToken);
273
+ else
274
+ _context.Users.Update(user);
275
+
276
+ await _context.SaveChangesAsync(cancellationToken);
277
+ return user;
278
+ }
279
+
280
+ public async Task<UserEntity?> FindByIdAsync(Guid id, CancellationToken cancellationToken = default)
281
+ {
282
+ return await _context.Users.FindAsync(new object[] { id }, cancellationToken);
283
+ }
284
+
285
+ public async Task<UserEntity?> FindByEmailAsync(EmailValueObject email, CancellationToken cancellationToken = default)
286
+ {
287
+ return await _context.Users
288
+ .FirstOrDefaultAsync(u => u.Email.Value == email.Value, cancellationToken);
289
+ }
290
+
291
+ public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default)
292
+ {
293
+ var user = await FindByIdAsync(id, cancellationToken);
294
+ if (user != null)
295
+ {
296
+ _context.Users.Remove(user);
297
+ await _context.SaveChangesAsync(cancellationToken);
298
+ }
299
+ }
129
300
  }
130
301
  ```
131
302
 
132
- ## Use Case Standards
133
-
134
- ### Use Case Structure
135
- ```typescript
136
- @Injectable()
137
- export class CreateUserUseCase {
138
- constructor(
139
- @Inject(USER_REPOSITORY)
140
- private readonly userRepository: UserRepositoryInterface,
141
- @Inject(EVENT_BUS)
142
- private readonly eventBus: EventBusInterface,
143
- ) {}
144
-
145
- async execute(command: CreateUserCommand): Promise<UserResponseDto> {
146
- // 1. Validate business rules
147
- await this.validateUserDoesNotExist(command.email);
148
-
149
- // 2. Create domain entity
150
- const user = UserEntity.create({
151
- email: command.email,
152
- name: command.name,
153
- });
154
-
155
- // 3. Persist entity
156
- const savedUser = await this.userRepository.save(user);
157
-
158
- // 4. Publish domain events
159
- await this.eventBus.publishAll(savedUser.getUncommittedEvents());
160
-
161
- // 5. Return response DTO
162
- return UserResponseDto.fromEntity(savedUser);
163
- }
303
+ ## Command/Query (CQRS) Standards
164
304
 
165
- private async validateUserDoesNotExist(email: string): Promise<void> {
166
- const emailVO = EmailValueObject.create(email);
167
- const existingUser = await this.userRepository.findByEmail(emailVO);
168
- if (existingUser) {
169
- throw new UserAlreadyExistsException(email);
305
+ ### Command Structure
306
+ ```csharp
307
+ public record CreateUserCommand(string Email, string Name);
308
+
309
+ public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
310
+ {
311
+ public CreateUserCommandValidator()
312
+ {
313
+ RuleFor(x => x.Email)
314
+ .NotEmpty().WithMessage("Email is required")
315
+ .EmailAddress().WithMessage("Email must be valid");
316
+
317
+ RuleFor(x => x.Name)
318
+ .NotEmpty().WithMessage("Name is required")
319
+ .MaximumLength(100).WithMessage("Name must not exceed 100 characters");
320
+ }
321
+ }
322
+
323
+ public class CreateUserCommandHandler
324
+ {
325
+ private readonly IUserRepository _userRepository;
326
+ private readonly IValidator<CreateUserCommand> _validator;
327
+
328
+ public CreateUserCommandHandler(
329
+ IUserRepository userRepository,
330
+ IValidator<CreateUserCommand> validator)
331
+ {
332
+ _userRepository = userRepository;
333
+ _validator = validator;
334
+ }
335
+
336
+ public async Task<UserResponseDto> HandleAsync(
337
+ CreateUserCommand command,
338
+ CancellationToken cancellationToken = default)
339
+ {
340
+ // 1. Validate command
341
+ await _validator.ValidateAndThrowAsync(command, cancellationToken);
342
+
343
+ // 2. Validate business rules
344
+ await ValidateUserDoesNotExistAsync(command.Email, cancellationToken);
345
+
346
+ // 3. Create domain entity
347
+ var user = UserEntity.Create(command.Email, command.Name);
348
+
349
+ // 4. Persist entity
350
+ var savedUser = await _userRepository.SaveAsync(user, cancellationToken);
351
+
352
+ // 5. Return response DTO
353
+ return UserResponseDto.FromEntity(savedUser);
354
+ }
355
+
356
+ private async Task ValidateUserDoesNotExistAsync(string email, CancellationToken cancellationToken)
357
+ {
358
+ var emailVO = EmailValueObject.Create(email);
359
+ var existingUser = await _userRepository.FindByEmailAsync(emailVO, cancellationToken);
360
+ if (existingUser != null)
361
+ throw new UserAlreadyExistsException(email);
170
362
  }
171
- }
172
363
  }
173
364
  ```
174
365
 
175
- ### Command/Query Structure
176
- ```typescript
177
- export class CreateUserCommand {
178
- @IsEmail()
179
- @IsNotEmpty()
180
- readonly email: string;
181
-
182
- @IsString()
183
- @IsNotEmpty()
184
- @Length(2, 50)
185
- readonly name: string;
366
+ ### Query Structure
367
+ ```csharp
368
+ public record GetUserByIdQuery(Guid Id);
369
+
370
+ public class GetUserByIdQueryHandler
371
+ {
372
+ private readonly IUserRepository _userRepository;
373
+
374
+ public GetUserByIdQueryHandler(IUserRepository userRepository)
375
+ {
376
+ _userRepository = userRepository;
377
+ }
378
+
379
+ public async Task<UserResponseDto?> HandleAsync(
380
+ GetUserByIdQuery query,
381
+ CancellationToken cancellationToken = default)
382
+ {
383
+ var user = await _userRepository.FindByIdAsync(query.Id, cancellationToken);
384
+ return user != null ? UserResponseDto.FromEntity(user) : null;
385
+ }
186
386
  }
187
387
  ```
188
388
 
189
- ## Testing Standards
389
+ ### DTO Structure
390
+ ```csharp
391
+ public record UserResponseDto
392
+ {
393
+ public Guid Id { get; init; }
394
+ public string Email { get; init; }
395
+ public string Name { get; init; }
396
+
397
+ public static UserResponseDto FromEntity(UserEntity user)
398
+ {
399
+ return new UserResponseDto
400
+ {
401
+ Id = user.Id,
402
+ Email = user.Email.Value,
403
+ Name = user.Name.Value
404
+ };
405
+ }
406
+ }
190
407
 
191
- ### Testing Strategy
192
- - **Unit Tests**: Domain entities, value objects, use cases (isolated logic)
193
- - **Integration Tests**: Repository implementations, database operations
194
- - **E2E Tests**: Complete API workflows with Supertest
195
- - **TDD Approach**: Write tests before implementation
196
-
197
- ### Test Structure
198
- ```typescript
199
- describe('CreateUserUseCase', () => {
200
- let useCase: CreateUserUseCase;
201
- let userRepository: jest.Mocked<UserRepositoryInterface>;
202
- let eventBus: jest.Mocked<EventBusInterface>;
203
-
204
- beforeEach(async () => {
205
- const module = await Test.createTestingModule({
206
- providers: [
207
- CreateUserUseCase,
208
- { provide: USER_REPOSITORY, useValue: { save: jest.fn(), findByEmail: jest.fn() } },
209
- { provide: EVENT_BUS, useValue: { publishAll: jest.fn() } },
210
- ],
211
- }).compile();
212
-
213
- useCase = module.get(CreateUserUseCase);
214
- userRepository = module.get(USER_REPOSITORY);
215
- eventBus = module.get(EVENT_BUS);
216
- });
217
-
218
- it('should create user successfully', async () => {
219
- // Arrange
220
- const command = new CreateUserCommand();
221
- command.email = 'test@example.com';
222
- command.name = 'Test User';
223
-
224
- userRepository.findByEmail.mockResolvedValue(null);
225
- userRepository.save.mockResolvedValue(UserEntity.create(command));
226
-
227
- // Act
228
- const result = await useCase.execute(command);
229
-
230
- // Assert
231
- expect(result.email).toBe(command.email);
232
- expect(userRepository.save).toHaveBeenCalledWith(expect.any(UserEntity));
233
- expect(eventBus.publishAll).toHaveBeenCalled();
234
- });
235
-
236
- it('should throw error when user already exists', async () => {
237
- // Arrange
238
- const command = new CreateUserCommand();
239
- command.email = 'existing@example.com';
240
-
241
- userRepository.findByEmail.mockResolvedValue(UserEntity.create(command));
242
-
243
- // Act & Assert
244
- await expect(useCase.execute(command)).rejects.toThrow(UserAlreadyExistsException);
245
- });
246
- });
247
408
  ```
248
409
 
249
- ## API Controller Standards
250
-
251
- ### Controller Structure
252
- ```typescript
253
- @Controller('users')
254
- @ApiTags('Users')
255
- export class UserController {
256
- constructor(
257
- private readonly createUserUseCase: CreateUserUseCase,
258
- private readonly getUserUseCase: GetUserUseCase,
259
- ) {}
260
-
261
- @Post()
262
- @ApiOperation({ summary: 'Create a new user' })
263
- @ApiResponse({ status: 201, type: UserResponseDto })
264
- @ApiResponse({ status: 400, description: 'Bad request' })
265
- async createUser(@Body() dto: CreateUserDto): Promise<UserResponseDto> {
266
- const command = new CreateUserCommand();
267
- Object.assign(command, dto);
268
- return this.createUserUseCase.execute(command);
269
- }
410
+ ## Minimal API Standards
270
411
 
271
- @Get(':id')
272
- @ApiOperation({ summary: 'Get user by ID' })
273
- @ApiResponse({ status: 200, type: UserResponseDto })
274
- @ApiResponse({ status: 404, description: 'User not found' })
275
- async getUser(@Param('id', ParseUUIDPipe) id: string): Promise<UserResponseDto> {
276
- return this.getUserUseCase.execute(new GetUserQuery(id));
277
- }
412
+ ### Endpoint Structure
413
+ ```csharp
414
+ // Program.cs or separate endpoint configuration
415
+ public static class UserEndpoints
416
+ {
417
+ public static void MapUserEndpoints(this IEndpointRouteBuilder app)
418
+ {
419
+ var group = app.MapGroup("/api/users")
420
+ .WithTags("Users")
421
+ .WithOpenApi();
422
+
423
+ group.MapPost("/", CreateUser)
424
+ .WithName("CreateUser")
425
+ .WithSummary("Create a new user")
426
+ .Produces<UserResponseDto>(StatusCodes.Status201Created)
427
+ .ProducesValidationProblem();
428
+
429
+ group.MapGet("/{id:guid}", GetUserById)
430
+ .WithName("GetUserById")
431
+ .WithSummary("Get user by ID")
432
+ .Produces<UserResponseDto>()
433
+ .Produces(StatusCodes.Status404NotFound);
434
+ }
435
+
436
+ private static async Task<IResult> CreateUser(
437
+ CreateUserCommand command,
438
+ CreateUserCommandHandler handler,
439
+ CancellationToken cancellationToken)
440
+ {
441
+ try
442
+ {
443
+ var result = await handler.HandleAsync(command, cancellationToken);
444
+ return Results.Created($"/api/users/{result.Id}", result);
445
+ }
446
+ catch (ValidationException ex)
447
+ {
448
+ return Results.ValidationProblem(ex.Errors.ToDictionary(
449
+ e => e.PropertyName,
450
+ e => new[] { e.ErrorMessage }));
451
+ }
452
+ catch (UserAlreadyExistsException ex)
453
+ {
454
+ return Results.Conflict(new { message = ex.Message });
455
+ }
456
+ }
457
+
458
+ private static async Task<IResult> GetUserById(
459
+ Guid id,
460
+ GetUserByIdQueryHandler handler,
461
+ CancellationToken cancellationToken)
462
+ {
463
+ var result = await handler.HandleAsync(new GetUserByIdQuery(id), cancellationToken);
464
+ return result != null ? Results.Ok(result) : Results.NotFound();
465
+ }
278
466
  }
279
467
  ```
280
468
 
281
- ## Database Standards
469
+ ## Testing Standards
470
+
471
+ ### Testing Strategy
472
+ - **Unit Tests**: Domain entities, value objects, command/query handlers (isolated logic)
473
+ - **Integration Tests**: Repository implementations, database operations with EF Core InMemory or Test Containers
474
+ - **TDD Approach**: Write tests before or alongside implementation
475
+ - **High Coverage**: Aim for >80% code coverage
476
+
477
+ ### Test Structure (xUnit)
478
+ ```csharp
479
+ public class CreateUserCommandHandlerTests
480
+ {
481
+ private readonly Mock<IUserRepository> _userRepositoryMock;
482
+ private readonly Mock<IValidator<CreateUserCommand>> _validatorMock;
483
+ private readonly CreateUserCommandHandler _handler;
484
+
485
+ public CreateUserCommandHandlerTests()
486
+ {
487
+ _userRepositoryMock = new Mock<IUserRepository>();
488
+ _validatorMock = new Mock<IValidator<CreateUserCommand>>();
489
+ _handler = new CreateUserCommandHandler(
490
+ _userRepositoryMock.Object,
491
+ _validatorMock.Object);
492
+ }
282
493
 
283
- ### Prisma Schema Patterns
284
- ```prisma
285
- model User {
286
- id String @id @default(cuid())
287
- email String @unique
288
- name String
289
- createdAt DateTime @default(now())
290
- updatedAt DateTime @updatedAt
291
- orders Order[]
494
+ [Fact]
495
+ public async Task HandleAsync_WithValidCommand_ShouldCreateUser()
496
+ {
497
+ // Arrange
498
+ var command = new CreateUserCommand("test@example.com", "John Doe");
499
+ _validatorMock
500
+ .Setup(v => v.ValidateAndThrowAsync(command, It.IsAny<CancellationToken>()))
501
+ .Returns(Task.CompletedTask);
502
+ _userRepositoryMock
503
+ .Setup(r => r.FindByEmailAsync(It.IsAny<EmailValueObject>(), It.IsAny<CancellationToken>()))
504
+ .ReturnsAsync((UserEntity?)null);
505
+ _userRepositoryMock
506
+ .Setup(r => r.SaveAsync(It.IsAny<UserEntity>(), It.IsAny<CancellationToken>()))
507
+ .ReturnsAsync((UserEntity u, CancellationToken _) => u);
508
+
509
+ // Act
510
+ var result = await _handler.HandleAsync(command, CancellationToken.None);
511
+
512
+ // Assert
513
+ Assert.NotNull(result);
514
+ Assert.Equal(command.Email, result.Email);
515
+ Assert.Equal(command.Name, result.Name);
516
+ _userRepositoryMock.Verify(r => r.SaveAsync(It.IsAny<UserEntity>(), It.IsAny<CancellationToken>()), Times.Once);
517
+ }
292
518
 
293
- @@map("users")
519
+ [Fact]
520
+ public async Task HandleAsync_WithExistingUser_ShouldThrowException()
521
+ {
522
+ // Arrange
523
+ var command = new CreateUserCommand("test@example.com", "John Doe");
524
+ var existingUser = UserEntity.Create("test@example.com", "Existing User");
525
+ _validatorMock
526
+ .Setup(v => v.ValidateAndThrowAsync(command, It.IsAny<CancellationToken>()))
527
+ .Returns(Task.CompletedTask);
528
+ _userRepositoryMock
529
+ .Setup(r => r.FindByEmailAsync(It.IsAny<EmailValueObject>(), It.IsAny<CancellationToken>()))
530
+ .ReturnsAsync(existingUser);
531
+
532
+ // Act & Assert
533
+ await Assert.ThrowsAsync<UserAlreadyExistsException>(() =>
534
+ _handler.HandleAsync(command, CancellationToken.None));
535
+ }
294
536
  }
537
+ ```
538
+
539
+ ### Integration Test with EF Core InMemory
540
+ ```csharp
541
+ public class UserRepositoryIntegrationTests : IDisposable
542
+ {
543
+ private readonly ApplicationDbContext _context;
544
+ private readonly UserRepository _repository;
295
545
 
296
- model Order {
297
- id String @id @default(cuid())
298
- total Decimal @db.Decimal(10, 2)
299
- status OrderStatus
300
- userId String
301
- user User @relation(fields: [userId], references: [id])
302
- items OrderItem[]
546
+ public UserRepositoryIntegrationTests()
547
+ {
548
+ var options = new DbContextOptionsBuilder<ApplicationDbContext>()
549
+ .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
550
+ .Options;
303
551
 
304
- @@map("orders")
552
+ _context = new ApplicationDbContext(options);
553
+ _repository = new UserRepository(_context);
554
+ }
555
+
556
+ [Fact]
557
+ public async Task SaveAsync_ShouldPersistUser()
558
+ {
559
+ // Arrange
560
+ var user = UserEntity.Create("test@example.com", "John Doe");
561
+
562
+ // Act
563
+ var savedUser = await _repository.SaveAsync(user);
564
+
565
+ // Assert
566
+ var retrievedUser = await _repository.FindByIdAsync(savedUser.Id);
567
+ Assert.NotNull(retrievedUser);
568
+ Assert.Equal(user.Email.Value, retrievedUser.Email.Value);
569
+ }
570
+
571
+ public void Dispose()
572
+ {
573
+ _context.Database.EnsureDeleted();
574
+ _context.Dispose();
575
+ }
305
576
  }
577
+ ```
306
578
 
307
- enum OrderStatus {
308
- PENDING
309
- CONFIRMED
310
- SHIPPED
311
- DELIVERED
312
- CANCELLED
579
+ ## EF Core Configuration Standards
580
+
581
+ ### DbContext Configuration
582
+ ```csharp
583
+ public class ApplicationDbContext : DbContext
584
+ {
585
+ public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
586
+ : base(options)
587
+ {
588
+ }
589
+
590
+ public DbSet<UserEntity> Users { get; set; }
591
+ public DbSet<QuoteEntity> Quotes { get; set; }
592
+
593
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
594
+ {
595
+ base.OnModelCreating(modelBuilder);
596
+
597
+ // Apply all configurations from assembly
598
+ modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
599
+ }
313
600
  }
314
601
  ```
315
602
 
316
- ### Prisma Best Practices
317
- - Use enums for fixed value sets
318
- - Always add indexes on foreign keys
319
- - Use `@@map` for table naming (plural snake_case)
320
- - Include `createdAt` and `updatedAt` timestamps
321
- - Use `cuid()` for primary keys
322
- - No raw SQL queries - use Prisma Client only
603
+ ### Entity Configuration
604
+ ```csharp
605
+ public class UserEntityConfiguration : IEntityTypeConfiguration<UserEntity>
606
+ {
607
+ public void Configure(EntityTypeBuilder<UserEntity> builder)
608
+ {
609
+ builder.ToTable("Users");
610
+
611
+ builder.HasKey(u => u.Id);
612
+
613
+ builder.Property(u => u.Id)
614
+ .HasColumnType("uuid")
615
+ .IsRequired();
616
+
617
+ // Configure Value Object as owned entity
618
+ builder.OwnsOne(u => u.Email, email =>
619
+ {
620
+ email.Property(e => e.Value)
621
+ .HasColumnName("Email")
622
+ .HasMaxLength(256)
623
+ .IsRequired();
624
+ });
625
+
626
+ builder.OwnsOne(u => u.Name, name =>
627
+ {
628
+ name.Property(n => n.Value)
629
+ .HasColumnName("Name")
630
+ .HasMaxLength(100)
631
+ .IsRequired();
632
+ });
633
+
634
+ // Ignore domain events (not persisted)
635
+ builder.Ignore(u => u.DomainEvents);
636
+ }
637
+ }
638
+ ```
323
639
 
324
640
  ## Security Standards
325
641
 
326
642
  ### Authentication & Authorization
327
643
  - **JWT Tokens**: Proper expiration and refresh token handling
328
- - **RBAC**: Role-based access control with Guards
329
- - **Validation**: Input validation on all endpoints (class-validator)
644
+ - **RBAC**: Role-based access control
645
+ - **Validation**: Input validation on all endpoints (FluentValidation)
330
646
  - **Rate Limiting**: Throttle public endpoints to prevent abuse
331
647
  - **Environment**: HTTPS only in production, secrets in env variables
332
648
 
@@ -335,20 +651,21 @@ enum OrderStatus {
335
651
  - Never commit secrets to repository
336
652
  - Implement audit logging for critical operations
337
653
  - OWASP Top 10 compliance
338
- - Regular dependency security audits
654
+ - Regular dependency security audits with `dotnet list package --vulnerable`
339
655
 
340
656
  ## Performance Standards
341
657
 
342
658
  ### Database Optimization
343
659
  - Proper indexing on frequently queried fields
344
- - Connection pooling via Prisma
345
- - Pagination for large datasets (cursor-based preferred)
346
- - Avoid N+1 queries with Prisma `include`
660
+ - Connection pooling via EF Core 10
661
+ - Pagination for large datasets (Skip/Take or cursor-based)
662
+ - Avoid N+1 queries with `.Include()` and `.ThenInclude()`
347
663
  - Query monitoring and slow query logging
664
+ - Use `AsNoTracking()` for read-only queries
348
665
 
349
666
  ### Caching Strategy
350
- - **Redis**: Session data, rate limiting, and frequently accessed data
351
- - **Application Cache**: In-memory caching for configuration
667
+ - **Distributed Cache**: Redis for session data and frequently accessed data
668
+ - **Memory Cache**: In-memory caching for configuration
352
669
  - **TTL Strategy**: Appropriate time-to-live for different data types
353
670
  - **Invalidation**: Event-driven cache invalidation
354
671
 
@@ -358,15 +675,138 @@ enum OrderStatus {
358
675
  - Domain exceptions for business rule violations
359
676
  - Application exceptions for use case errors
360
677
  - Infrastructure exceptions for external service failures
361
- - HTTP exception filters for API responses
678
+ - Global exception middleware for API responses
362
679
 
363
- ### Error Response Format
364
- ```typescript
680
+ ### Error Response Format (Problem Details RFC 7807)
681
+ ```csharp
682
+ public record ProblemDetailsResponse
365
683
  {
366
- "statusCode": 400,
367
- "message": "User with email already exists",
368
- "error": "UserAlreadyExistsException",
369
- "timestamp": "2024-01-15T10:30:00Z",
370
- "path": "/api/users"
684
+ public int Status { get; init; }
685
+ public string Title { get; init; }
686
+ public string Detail { get; init; }
687
+ public string Instance { get; init; }
688
+ public DateTimeOffset Timestamp { get; init; }
689
+ public Dictionary<string, string[]>? Errors { get; init; }
371
690
  }
372
- ```
691
+
692
+ // Example response
693
+ {
694
+ "status": 400,
695
+ "title": "Validation Error",
696
+ "detail": "One or more validation errors occurred",
697
+ "instance": "/api/users",
698
+ "timestamp": "2025-11-26T10:30:00Z",
699
+ "errors": {
700
+ "Email": ["Email is required", "Email must be valid"],
701
+ "Name": ["Name is required"]
702
+ }
703
+ }
704
+ ```
705
+
706
+ ### Global Exception Middleware
707
+ ```csharp
708
+ public class ExceptionHandlingMiddleware
709
+ {
710
+ private readonly RequestDelegate _next;
711
+ private readonly ILogger<ExceptionHandlingMiddleware> _logger;
712
+
713
+ public ExceptionHandlingMiddleware(
714
+ RequestDelegate next,
715
+ ILogger<ExceptionHandlingMiddleware> logger)
716
+ {
717
+ _next = next;
718
+ _logger = logger;
719
+ }
720
+
721
+ public async Task InvokeAsync(HttpContext context)
722
+ {
723
+ try
724
+ {
725
+ await _next(context);
726
+ }
727
+ catch (ValidationException ex)
728
+ {
729
+ await HandleValidationExceptionAsync(context, ex);
730
+ }
731
+ catch (DomainException ex)
732
+ {
733
+ await HandleDomainExceptionAsync(context, ex);
734
+ }
735
+ catch (Exception ex)
736
+ {
737
+ _logger.LogError(ex, "Unhandled exception occurred");
738
+ await HandleUnhandledExceptionAsync(context, ex);
739
+ }
740
+ }
741
+
742
+ private static Task HandleValidationExceptionAsync(HttpContext context, ValidationException exception)
743
+ {
744
+ context.Response.StatusCode = StatusCodes.Status400BadRequest;
745
+ context.Response.ContentType = "application/problem+json";
746
+
747
+ var problemDetails = new ProblemDetailsResponse
748
+ {
749
+ Status = StatusCodes.Status400BadRequest,
750
+ Title = "Validation Error",
751
+ Detail = "One or more validation errors occurred",
752
+ Instance = context.Request.Path,
753
+ Timestamp = DateTimeOffset.UtcNow,
754
+ Errors = exception.Errors.GroupBy(e => e.PropertyName)
755
+ .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray())
756
+ };
757
+
758
+ return context.Response.WriteAsJsonAsync(problemDetails);
759
+ }
760
+ }
761
+ ```
762
+
763
+ ## Security Standards
764
+
765
+ ### Authentication & Authorization
766
+ - **JWT Tokens**: Proper expiration and refresh token handling
767
+ - **RBAC**: Role-based access control with Authorization Policies
768
+ - **Validation**: Input validation on all endpoints (FluentValidation)
769
+ - **Rate Limiting**: ASP.NET Core Rate Limiting middleware
770
+ - **Environment**: HTTPS only in production, secrets in Azure Key Vault or User Secrets
771
+
772
+ ### Data Protection
773
+ - Encrypt sensitive data at rest and in transit
774
+ - Never commit secrets to repository
775
+ - Implement audit logging for critical operations
776
+ - OWASP Top 10 compliance
777
+ - Regular dependency security audits with `dotnet list package --vulnerable`
778
+
779
+ ## Performance Standards
780
+
781
+ ### Database Optimization
782
+ - Proper indexing on frequently queried fields
783
+ - Connection pooling via EF Core (configured in connection string)
784
+ - Pagination for large datasets (Skip/Take or Keyset pagination)
785
+ - Avoid N+1 queries with `.Include()` and `.ThenInclude()`
786
+ - Query monitoring and slow query logging
787
+ - Use `AsNoTracking()` for read-only queries
788
+
789
+ ### Caching Strategy
790
+ - **Distributed Cache**: Redis or SQL Server for distributed scenarios
791
+ - **Memory Cache**: `IMemoryCache` for single-instance caching
792
+ - **TTL Strategy**: Appropriate time-to-live for different data types
793
+ - **Invalidation**: Event-driven cache invalidation
794
+ - **Response Caching**: Use `[ResponseCache]` attribute for GET endpoints
795
+
796
+ ## Additional Standards
797
+
798
+ ### Logging
799
+ - Use `ILogger<T>` for structured logging
800
+ - Log levels: Trace, Debug, Information, Warning, Error, Critical
801
+ - Include correlation IDs for request tracing
802
+ - Never log sensitive information (passwords, tokens, PII)
803
+
804
+ ### API Versioning
805
+ - Use URL versioning: `/api/v1/users`
806
+ - Support multiple versions simultaneously during migration
807
+ - Deprecation notices in response headers
808
+
809
+ ### Health Checks
810
+ - Implement `/health` endpoint for liveness
811
+ - Implement `/health/ready` for readiness checks
812
+ - Include database, cache, and external service checks