@atlashub/smartstack-cli 4.33.0 → 4.34.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/dist/index.js +45 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/project/claude-md/api.CLAUDE.md.template +315 -0
- package/templates/project/claude-md/application.CLAUDE.md.template +181 -0
- package/templates/project/claude-md/domain.CLAUDE.md.template +125 -0
- package/templates/project/claude-md/infrastructure.CLAUDE.md.template +168 -0
- package/templates/project/claude-md/root.CLAUDE.md.template +339 -0
- package/templates/project/claude-md/web.CLAUDE.md.template +339 -0
package/package.json
CHANGED
|
@@ -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/`
|