@atlashub/smartstack-cli 4.33.0 → 4.35.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlashub/smartstack-cli",
3
- "version": "4.33.0",
3
+ "version": "4.35.0",
4
4
  "description": "SmartStack Claude Code automation toolkit - GitFlow, EF Core migrations, prompts and more",
5
5
  "author": {
6
6
  "name": "SmartStack",
@@ -0,0 +1,315 @@
1
+ # {{ProjectName}}.Api - Memory
2
+
3
+ ## Purpose
4
+
5
+ HTTP entry point. REST API. Receives requests, delegates to Application layer, returns responses.
6
+
7
+ ## Dependencies
8
+
9
+ - **{{ProjectName}}.Application** (commands, queries, DTOs)
10
+ - **{{ProjectName}}.Infrastructure** (DI registration)
11
+ - **MediatR** (sending commands/queries)
12
+ - **Scalar.AspNetCore** (OpenAPI UI)
13
+
14
+ ---
15
+
16
+ ## OpenAPI / Swagger Configuration (CRITICAL)
17
+
18
+ ### Package Requirements
19
+
20
+ ```xml
21
+ <!-- {{ProjectName}}.Api.csproj -->
22
+ <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
23
+ <PackageReference Include="Scalar.AspNetCore" Version="2.0.0" />
24
+ ```
25
+
26
+ ### Program.cs Configuration
27
+
28
+ ```csharp
29
+ // Services
30
+ builder.Services.AddOpenApi(options =>
31
+ {
32
+ options.AddDocumentTransformer((document, context, ct) =>
33
+ {
34
+ document.Info = new OpenApiInfo
35
+ {
36
+ Title = "{{ProjectName}} API",
37
+ Version = "v1",
38
+ Description = "{{ProjectName}} REST API"
39
+ };
40
+ return Task.CompletedTask;
41
+ });
42
+ });
43
+
44
+ // Middleware (after app.Build())
45
+ if (app.Environment.IsDevelopment())
46
+ {
47
+ app.MapOpenApi(); // /openapi/v1.json
48
+ app.MapScalarApiReference(options => // /scalar/v1
49
+ {
50
+ options.WithTitle("{{ProjectName}} API")
51
+ .WithTheme(ScalarTheme.BluePlanet)
52
+ .WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient);
53
+ });
54
+ }
55
+ ```
56
+
57
+ ### XML Documentation (MANDATORY for all Controllers)
58
+
59
+ ```xml
60
+ <!-- {{ProjectName}}.Api.csproj -->
61
+ <PropertyGroup>
62
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
63
+ <NoWarn>$(NoWarn);1591</NoWarn>
64
+ </PropertyGroup>
65
+ ```
66
+
67
+ ### Controller Documentation Pattern
68
+
69
+ ```csharp
70
+ /// <summary>
71
+ /// Manages orders
72
+ /// </summary>
73
+ [ApiController]
74
+ [Route("api/[controller]")]
75
+ [Produces("application/json")]
76
+ [Tags("Orders")]
77
+ public class OrdersController : ControllerBase
78
+ {
79
+ /// <summary>
80
+ /// Retrieves all orders with optional filtering
81
+ /// </summary>
82
+ [HttpGet]
83
+ [ProducesResponseType(typeof(IReadOnlyList<OrderDto>), StatusCodes.Status200OK)]
84
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
85
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status403Forbidden)]
86
+ public async Task<IActionResult> GetAll(CancellationToken ct)
87
+ {
88
+ var result = await _mediator.Send(new GetOrdersQuery(), ct);
89
+ return Ok(result);
90
+ }
91
+ }
92
+ ```
93
+
94
+ ### ProducesResponseType Cheat Sheet
95
+
96
+ | Scenario | Attributes |
97
+ |----------|------------|
98
+ | **GET (list)** | `[ProducesResponseType(typeof(List<Dto>), 200)]` |
99
+ | **GET (single)** | `[ProducesResponseType(typeof(Dto), 200)]`<br>`[ProducesResponseType(typeof(ProblemDetails), 404)]` |
100
+ | **POST (create)** | `[ProducesResponseType(typeof(Guid), 201)]`<br>`[ProducesResponseType(typeof(ValidationProblemDetails), 400)]` |
101
+ | **PUT (update)** | `[ProducesResponseType(204)]`<br>`[ProducesResponseType(typeof(ProblemDetails), 404)]` |
102
+ | **DELETE** | `[ProducesResponseType(204)]`<br>`[ProducesResponseType(typeof(ProblemDetails), 404)]` |
103
+ | **Auth required** | Add `[ProducesResponseType(typeof(ProblemDetails), 401)]` |
104
+ | **Permission required** | Add `[ProducesResponseType(typeof(ProblemDetails), 403)]` |
105
+ | **Conflict possible** | Add `[ProducesResponseType(typeof(ProblemDetails), 409)]` |
106
+
107
+ ### OpenAPI URLs
108
+
109
+ | Environment | URL | Purpose |
110
+ |-------------|-----|---------|
111
+ | Development | `/openapi/v1.json` | Raw OpenAPI spec |
112
+ | Development | `/scalar/v1` | Interactive UI (Scalar) |
113
+ | Production | Disabled | Security best practice |
114
+
115
+ ---
116
+
117
+ ## Structure
118
+
119
+ ```
120
+ {{ProjectName}}.Api/
121
+ ├── Controllers/ → Organized by Application/Module
122
+ ├── Authorization/ → RequirePermissionAttribute
123
+ ├── Middleware/
124
+ │ ├── GlobalExceptionHandlerMiddleware.cs
125
+ │ └── SessionValidationMiddleware.cs
126
+ ├── Extensions/
127
+ ├── RateLimiting/
128
+ ├── Program.cs
129
+ ├── appsettings.json
130
+ └── appsettings.Development.json
131
+ ```
132
+
133
+ ## Patterns
134
+
135
+ ### Controller Template
136
+
137
+ ```csharp
138
+ namespace {{ProjectName}}.Api.Controllers;
139
+
140
+ using MediatR;
141
+ using Microsoft.AspNetCore.Http;
142
+ using Microsoft.AspNetCore.Mvc;
143
+ using {{ProjectName}}.Api.Authorization;
144
+
145
+ [ApiController]
146
+ [Route("api/[controller]")]
147
+ [Produces("application/json")]
148
+ [Tags("Orders")]
149
+ public class OrdersController : ControllerBase
150
+ {
151
+ private readonly ISender _mediator;
152
+
153
+ public OrdersController(ISender mediator)
154
+ {
155
+ _mediator = mediator;
156
+ }
157
+
158
+ [HttpGet]
159
+ [ProducesResponseType(typeof(IReadOnlyList<OrderDto>), StatusCodes.Status200OK)]
160
+ public async Task<IActionResult> GetAll(CancellationToken ct)
161
+ {
162
+ var result = await _mediator.Send(new GetOrdersQuery(), ct);
163
+ return Ok(result);
164
+ }
165
+
166
+ [HttpGet("{id:guid}")]
167
+ [ProducesResponseType(typeof(OrderDto), StatusCodes.Status200OK)]
168
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
169
+ public async Task<IActionResult> GetById(Guid id, CancellationToken ct)
170
+ {
171
+ var result = await _mediator.Send(new GetOrderByIdQuery(id), ct);
172
+ return result is null ? NotFound() : Ok(result);
173
+ }
174
+
175
+ [HttpPost]
176
+ [ProducesResponseType(typeof(Guid), StatusCodes.Status201Created)]
177
+ [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
178
+ public async Task<IActionResult> Create([FromBody] CreateOrderCommand command, CancellationToken ct)
179
+ {
180
+ var id = await _mediator.Send(command, ct);
181
+ return CreatedAtAction(nameof(GetById), new { id }, id);
182
+ }
183
+
184
+ [HttpPut("{id:guid}")]
185
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
186
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
187
+ public async Task<IActionResult> Update(Guid id, [FromBody] UpdateOrderCommand command, CancellationToken ct)
188
+ {
189
+ if (id != command.Id) return BadRequest();
190
+ await _mediator.Send(command, ct);
191
+ return NoContent();
192
+ }
193
+
194
+ [HttpDelete("{id:guid}")]
195
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
196
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
197
+ public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
198
+ {
199
+ await _mediator.Send(new DeleteOrderCommand(id), ct);
200
+ return NoContent();
201
+ }
202
+ }
203
+ ```
204
+
205
+ ### Exception Middleware
206
+
207
+ ```csharp
208
+ namespace {{ProjectName}}.Api.Middleware;
209
+
210
+ using System.Net;
211
+ using FluentValidation;
212
+ using Microsoft.AspNetCore.Mvc;
213
+ using {{ProjectName}}.Application.Common.Exceptions;
214
+
215
+ public class GlobalExceptionHandlerMiddleware
216
+ {
217
+ private readonly RequestDelegate _next;
218
+ private readonly ILogger<GlobalExceptionHandlerMiddleware> _logger;
219
+
220
+ public GlobalExceptionHandlerMiddleware(RequestDelegate next, ILogger<GlobalExceptionHandlerMiddleware> logger)
221
+ {
222
+ _next = next;
223
+ _logger = logger;
224
+ }
225
+
226
+ public async Task InvokeAsync(HttpContext context)
227
+ {
228
+ try
229
+ {
230
+ await _next(context);
231
+ }
232
+ catch (ValidationException ex)
233
+ {
234
+ context.Response.StatusCode = StatusCodes.Status400BadRequest;
235
+ context.Response.ContentType = "application/json";
236
+ var problemDetails = new ValidationProblemDetails(
237
+ ex.Errors.GroupBy(e => e.PropertyName)
238
+ .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray()))
239
+ {
240
+ Status = StatusCodes.Status400BadRequest,
241
+ Title = "Validation failed"
242
+ };
243
+ await context.Response.WriteAsJsonAsync(problemDetails);
244
+ }
245
+ catch (NotFoundException ex)
246
+ {
247
+ context.Response.StatusCode = StatusCodes.Status404NotFound;
248
+ context.Response.ContentType = "application/json";
249
+ var problemDetails = new ProblemDetails
250
+ {
251
+ Title = "Not Found",
252
+ Detail = ex.Message,
253
+ Status = StatusCodes.Status404NotFound
254
+ };
255
+ await context.Response.WriteAsJsonAsync(problemDetails);
256
+ }
257
+ catch (DomainException ex)
258
+ {
259
+ context.Response.StatusCode = StatusCodes.Status400BadRequest;
260
+ context.Response.ContentType = "application/json";
261
+ var problemDetails = new ProblemDetails
262
+ {
263
+ Title = "Business rule violation",
264
+ Detail = ex.Message,
265
+ Status = StatusCodes.Status400BadRequest
266
+ };
267
+ await context.Response.WriteAsJsonAsync(problemDetails);
268
+ }
269
+ catch (Exception ex)
270
+ {
271
+ _logger.LogError(ex, "Unhandled exception: {Message}", ex.Message);
272
+ context.Response.StatusCode = StatusCodes.Status500InternalServerError;
273
+ context.Response.ContentType = "application/json";
274
+ var problemDetails = new ProblemDetails
275
+ {
276
+ Title = "Internal Server Error",
277
+ Detail = "An unexpected error occurred",
278
+ Status = StatusCodes.Status500InternalServerError
279
+ };
280
+ await context.Response.WriteAsJsonAsync(problemDetails);
281
+ }
282
+ }
283
+ }
284
+ ```
285
+
286
+ ## API Response Conventions
287
+
288
+ | Action | Status Code | Response |
289
+ |--------|-------------|----------|
290
+ | GET (list) | 200 | `List<Dto>` |
291
+ | GET (single) | 200 / 404 | `Dto` / ProblemDetails |
292
+ | POST | 201 | Id + Location header |
293
+ | PUT | 204 | Empty |
294
+ | DELETE | 204 | Empty |
295
+ | Validation error | 400 | ValidationProblemDetails |
296
+ | Server error | 500 | ProblemDetails |
297
+
298
+ ## Rules
299
+
300
+ 1. **Controllers are thin** - delegate to MediatR immediately
301
+ 2. **NO business logic** in controllers
302
+ 3. **Use `CancellationToken`** in all async methods
303
+ 4. **Document with XML comments** for OpenAPI
304
+ 5. **Return `IActionResult`** for flexibility
305
+ 6. **ALWAYS add `[ProducesResponseType]`** for all possible responses
306
+ 7. **ALWAYS add `[Tags]`** to group endpoints in OpenAPI
307
+
308
+ ## When Adding New Endpoint
309
+
310
+ 1. Create controller in `Controllers/` (or add to existing)
311
+ 2. Inject `ISender` (MediatR)
312
+ 3. Create action method with proper HTTP verb
313
+ 4. Document with XML comments
314
+ 5. Add `[ProducesResponseType]` attributes
315
+ 6. Use command/query from Application layer
@@ -0,0 +1,181 @@
1
+ # {{ProjectName}}.Application - Memory
2
+
3
+ ## Purpose
4
+
5
+ Use cases orchestration. Defines WHAT the application does. Contains business logic that doesn't belong to entities.
6
+
7
+ ## Dependencies
8
+
9
+ - **{{ProjectName}}.Domain** (entities, interfaces)
10
+ - **MediatR** (CQRS) - add when needed
11
+ - **FluentValidation** - add when needed
12
+ - **AutoMapper** - add when needed
13
+
14
+ ## Structure
15
+
16
+ ```
17
+ {{ProjectName}}.Application/
18
+ ├── Common/
19
+ │ ├── Authorization/ → Permissions constants
20
+ │ ├── Behaviors/ → MediatR pipeline behaviors
21
+ │ ├── Exceptions/ → Application exceptions
22
+ │ ├── Interfaces/
23
+ │ │ ├── Identity/ → ICurrentUserService, IJwtService
24
+ │ │ └── Persistence/ → IExtensionsDbContext
25
+ │ └── Licensing/ → License validation
26
+ ├── {Feature}/ → Feature-specific commands/queries
27
+ │ ├── Commands/
28
+ │ ├── Queries/
29
+ │ └── DTOs/
30
+ └── DependencyInjection.cs
31
+ ```
32
+
33
+ ## Patterns
34
+
35
+ ### Command Template (with MediatR)
36
+
37
+ ```csharp
38
+ namespace {{ProjectName}}.Application.{Feature}.Commands;
39
+
40
+ // Command
41
+ public record CreateOrderCommand(string Name, decimal Amount) : IRequest<Guid>;
42
+
43
+ // Handler
44
+ public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>
45
+ {
46
+ private readonly IExtensionsDbContext _context;
47
+
48
+ public CreateOrderCommandHandler(IExtensionsDbContext context)
49
+ {
50
+ _context = context;
51
+ }
52
+
53
+ public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken ct)
54
+ {
55
+ var order = Order.Create(request.Name, request.Amount);
56
+
57
+ await _context.Orders.AddAsync(order, ct);
58
+ await _context.SaveChangesAsync(ct);
59
+
60
+ return order.Id;
61
+ }
62
+ }
63
+
64
+ // Validator (FluentValidation)
65
+ public class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
66
+ {
67
+ public CreateOrderCommandValidator()
68
+ {
69
+ RuleFor(x => x.Name)
70
+ .NotEmpty().WithMessage("Name is required")
71
+ .MaximumLength(200).WithMessage("Name max 200 chars");
72
+
73
+ RuleFor(x => x.Amount)
74
+ .GreaterThan(0).WithMessage("Amount must be positive");
75
+ }
76
+ }
77
+ ```
78
+
79
+ ### Query Template
80
+
81
+ ```csharp
82
+ namespace {{ProjectName}}.Application.{Feature}.Queries;
83
+
84
+ // Query
85
+ public record GetOrdersQuery(string? SearchTerm = null) : IRequest<IReadOnlyList<OrderDto>>;
86
+
87
+ // Handler
88
+ public class GetOrdersQueryHandler : IRequestHandler<GetOrdersQuery, IReadOnlyList<OrderDto>>
89
+ {
90
+ private readonly IExtensionsDbContext _context;
91
+ private readonly IMapper _mapper;
92
+
93
+ public GetOrdersQueryHandler(IExtensionsDbContext context, IMapper mapper)
94
+ {
95
+ _context = context;
96
+ _mapper = mapper;
97
+ }
98
+
99
+ public async Task<IReadOnlyList<OrderDto>> Handle(GetOrdersQuery request, CancellationToken ct)
100
+ {
101
+ var query = _context.Orders.AsNoTracking();
102
+
103
+ if (!string.IsNullOrWhiteSpace(request.SearchTerm))
104
+ query = query.Where(o => o.Name.Contains(request.SearchTerm));
105
+
106
+ return await query
107
+ .ProjectTo<OrderDto>(_mapper.ConfigurationProvider)
108
+ .ToListAsync(ct);
109
+ }
110
+ }
111
+ ```
112
+
113
+ ### DTO Template
114
+
115
+ ```csharp
116
+ namespace {{ProjectName}}.Application.{Feature}.DTOs;
117
+
118
+ public record OrderDto(Guid Id, string Name, decimal Amount, DateTime CreatedAt);
119
+
120
+ // AutoMapper Profile
121
+ public class OrderProfile : Profile
122
+ {
123
+ public OrderProfile()
124
+ {
125
+ CreateMap<Order, OrderDto>();
126
+ }
127
+ }
128
+ ```
129
+
130
+ ### Interface Template (ExtensionsDbContext)
131
+
132
+ ```csharp
133
+ namespace {{ProjectName}}.Application.Common.Interfaces.Persistence;
134
+
135
+ public interface IExtensionsDbContext
136
+ {
137
+ DbSet<Order> Orders { get; }
138
+ // Add DbSets for your entities here
139
+ Task<int> SaveChangesAsync(CancellationToken ct = default);
140
+ }
141
+ ```
142
+
143
+ ## DependencyInjection Setup
144
+
145
+ ```csharp
146
+ public static class DependencyInjection
147
+ {
148
+ public static IServiceCollection AddApplication(this IServiceCollection services)
149
+ {
150
+ var assembly = typeof(DependencyInjection).Assembly;
151
+
152
+ services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(assembly));
153
+ services.AddValidatorsFromAssembly(assembly);
154
+ services.AddAutoMapper(assembly);
155
+ services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
156
+ services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
157
+
158
+ return services;
159
+ }
160
+ }
161
+ ```
162
+
163
+ ## Rules
164
+
165
+ 1. **NO** direct database access - use `IExtensionsDbContext` (from Common/Interfaces/Persistence)
166
+ 2. **NO** HttpContext, Controllers - that's API layer
167
+ 3. **Commands** modify state, return Id or nothing
168
+ 4. **Queries** read-only, return DTOs
169
+ 5. **Handlers** are the use cases
170
+ 6. **One handler per command/query**
171
+
172
+ ## When Adding New Feature
173
+
174
+ 1. Create folder in `{Feature}/` (appropriate namespace path)
175
+ 2. Add `Commands/` and/or `Queries/` subfolders
176
+ 3. Add `DTOs/` subfolder if needed
177
+ 4. Create Command/Query record
178
+ 5. Create Handler class
179
+ 6. Create Validator (optional but recommended)
180
+ 7. Add DTO record and AutoMapper profile if needed
181
+ 8. Register in `DependencyInjection.cs`
@@ -0,0 +1,125 @@
1
+ # {{ProjectName}}.Domain - Memory
2
+
3
+ ## Purpose
4
+
5
+ Core business logic. **ZERO external dependencies**. This layer defines WHAT the business does, not HOW.
6
+
7
+ ## Structure
8
+
9
+ ```
10
+ {{ProjectName}}.Domain/
11
+ ├── Common/
12
+ │ ├── BaseEntity.cs → Base for all entities (Id, CreatedAt, UpdatedAt)
13
+ │ └── IAuditableEntity.cs → Audit tracking interface
14
+ ├── Enums/ → Domain enumerations
15
+ ├── {Feature}/ → Feature-specific entities
16
+ │ ├── Order.cs → Aggregate root
17
+ │ └── OrderItem.cs → Child entity
18
+ ├── Exceptions/ → DomainException, custom exceptions
19
+ └── Interfaces/ → Repository contracts
20
+ ```
21
+
22
+ ## Rules (STRICT)
23
+
24
+ 1. **NO** NuGet packages except BCL
25
+ 2. **NO** `using` statements referencing other projects
26
+ 3. **NO** EF Core attributes (`[Key]`, `[Required]`) - use Fluent API in Infrastructure
27
+ 4. **NO** DTOs - entities only
28
+ 5. **ALL** business rules live here
29
+
30
+ ## Patterns
31
+
32
+ ### Entity Template
33
+
34
+ ```csharp
35
+ namespace {{ProjectName}}.Domain.{Feature};
36
+
37
+ public class Order : BaseEntity
38
+ {
39
+ public string Name { get; private set; } = null!;
40
+ public decimal Amount { get; private set; }
41
+ public bool IsActive { get; private set; }
42
+
43
+ private Order() { } // EF Core
44
+
45
+ public static Order Create(string name, decimal amount)
46
+ {
47
+ if (string.IsNullOrWhiteSpace(name))
48
+ throw new DomainException("Name is required");
49
+ if (amount <= 0)
50
+ throw new DomainException("Amount must be positive");
51
+
52
+ return new Order
53
+ {
54
+ Id = Guid.NewGuid(),
55
+ Name = name,
56
+ Amount = amount,
57
+ IsActive = true,
58
+ CreatedAt = DateTime.UtcNow
59
+ };
60
+ }
61
+
62
+ public void Deactivate()
63
+ {
64
+ IsActive = false;
65
+ UpdatedAt = DateTime.UtcNow;
66
+ }
67
+ }
68
+ ```
69
+
70
+ ### Value Object Template
71
+
72
+ ```csharp
73
+ namespace {{ProjectName}}.Domain.Common.ValueObjects;
74
+
75
+ public record Money(decimal Amount, string Currency)
76
+ {
77
+ public static Money Create(decimal amount, string currency)
78
+ {
79
+ if (amount < 0)
80
+ throw new DomainException("Amount cannot be negative");
81
+ if (string.IsNullOrWhiteSpace(currency))
82
+ throw new DomainException("Currency is required");
83
+
84
+ return new Money(amount, currency);
85
+ }
86
+ }
87
+ ```
88
+
89
+ ### Domain Event Template
90
+
91
+ ```csharp
92
+ namespace {{ProjectName}}.Domain.Common.Events;
93
+
94
+ public record OrderCreatedEvent(Guid OrderId, string Name, DateTime OccurredAt);
95
+ ```
96
+
97
+ ### Repository Interface Template
98
+
99
+ ```csharp
100
+ namespace {{ProjectName}}.Domain.Interfaces;
101
+
102
+ public interface IRepository<T> where T : BaseEntity
103
+ {
104
+ Task<T?> GetByIdAsync(Guid id, CancellationToken ct = default);
105
+ Task<IReadOnlyList<T>> GetAllAsync(CancellationToken ct = default);
106
+ Task AddAsync(T entity, CancellationToken ct = default);
107
+ void Update(T entity);
108
+ void Remove(T entity);
109
+ }
110
+ ```
111
+
112
+ ## Validation
113
+
114
+ - **Entities validate themselves** in constructors and methods
115
+ - Throw `DomainException` for business rule violations
116
+ - Use **Guard clauses** at method start
117
+
118
+ ## When Adding New Entity
119
+
120
+ 1. Create in the appropriate domain folder
121
+ 2. Inherit from `BaseEntity`
122
+ 3. Private parameterless constructor for EF
123
+ 4. Static `Create()` factory method
124
+ 5. Encapsulate all state changes in methods
125
+ 6. Add repository interface in `Interfaces/`