@polymorphism-tech/morph-spec 4.8.7 → 4.8.8
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/README.md +2 -2
- package/bin/morph-spec.js +22 -1
- package/bin/task-manager.cjs +120 -16
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/agents.json +1854 -1815
- package/framework/hooks/claude-code/pre-compact/save-morph-context.js +141 -23
- package/framework/hooks/claude-code/statusline.py +0 -12
- package/framework/hooks/claude-code/statusline.sh +6 -2
- package/framework/hooks/claude-code/stop/validate-completion.js +70 -23
- package/framework/hooks/dev/guard-version-numbers.js +1 -1
- package/framework/skills/level-0-meta/morph-init/SKILL.md +44 -6
- package/framework/skills/level-0-meta/tool-usage-guide/SKILL.md +67 -16
- package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +77 -7
- package/framework/skills/level-1-workflows/phase-design/SKILL.md +114 -50
- package/framework/skills/level-1-workflows/phase-implement/SKILL.md +139 -1
- package/framework/skills/level-1-workflows/phase-setup/SKILL.md +29 -6
- package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +4 -3
- package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +944 -933
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +429 -0
- package/framework/templates/REGISTRY.json +1909 -1888
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +282 -0
- package/package.json +1 -1
- package/src/commands/agents/dispatch-agents.js +430 -0
- package/src/commands/agents/index.js +2 -1
- package/src/commands/project/doctor.js +137 -2
- package/src/commands/state/state.js +20 -4
- package/src/commands/templates/generate-contracts.js +445 -0
- package/src/commands/templates/index.js +1 -0
- package/src/lib/validators/validation-runner.js +19 -7
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
# Vertical Slice Architecture (VSA)
|
|
2
|
+
|
|
3
|
+
> **Scope:** universal
|
|
4
|
+
> **Layer:** 2
|
|
5
|
+
> **Keywords:** vertical-slice, vsa, minimal-api, ihandler, result-pattern, feature-slice, scrutor, fluentvalidation
|
|
6
|
+
|
|
7
|
+
Padrão arquitetural baseado em **KanaiyaKatarmal/VerticalSliceArchitectureTemplate** (.NET 10, Minimal APIs, EF Core 10).
|
|
8
|
+
Cada feature é uma fatia vertical completa: endpoint → handler → validator → entity → DB.
|
|
9
|
+
Sem MediatR, sem AggregateRoot, sem Domain Events, sem CQRS formal.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Estrutura de Pastas
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
{ProjectName}/
|
|
17
|
+
├── Abstractions/
|
|
18
|
+
│ ├── IHandler.cs # Interface central: IHandler<TRequest, TResponse>
|
|
19
|
+
│ ├── IApiEndpoint.cs # Interface de endpoint: void MapEndpoint(WebApplication app)
|
|
20
|
+
│ ├── Result.cs # Result<T> / Result (non-generic)
|
|
21
|
+
│ └── Errors/
|
|
22
|
+
│ ├── Error.cs # record Error com factory methods + ErrorType enum
|
|
23
|
+
│ └── ValidationError.cs # ValidationError : Error com array de erros
|
|
24
|
+
├── Constants/
|
|
25
|
+
│ └── ApiTags.cs # Constantes de tags para OpenAPI/Swagger
|
|
26
|
+
├── Database/
|
|
27
|
+
│ └── ApplicationDbContext.cs # DbContext com DbSet<Entity>
|
|
28
|
+
├── Entities/
|
|
29
|
+
│ └── {Entity}.cs # Entidade simples (sem AggregateRoot)
|
|
30
|
+
├── Exceptions/
|
|
31
|
+
│ └── CustomExceptionHandler.cs # IExceptionHandler para ProblemDetails
|
|
32
|
+
├── Extensions/
|
|
33
|
+
│ ├── HandlerRegistrationExtensions.cs # AddHandlersFromAssembly() com Scrutor Decorate
|
|
34
|
+
│ ├── MapEndpointExtensions.cs # RegisterApiEndpointsFromAssembly() + MapApiEndpoints()
|
|
35
|
+
│ └── ResultExtensions.cs # result.Match(onSuccess, onFailure)
|
|
36
|
+
├── Features/
|
|
37
|
+
│ └── {Entity}Feature/
|
|
38
|
+
│ ├── {Entity}Errors.cs # static class {Entity}Errors (factory methods)
|
|
39
|
+
│ ├── Create{Entity}/
|
|
40
|
+
│ │ ├── Create{Entity}Request.cs # sealed record request
|
|
41
|
+
│ │ ├── ...Response.cs # sealed record response
|
|
42
|
+
│ │ ├── Create{Entity}Handler.cs # IHandler<Request, Result<Response>>
|
|
43
|
+
│ │ ├── Create{Entity}Validator.cs # AbstractValidator<Request>
|
|
44
|
+
│ │ └── Create{Entity}Endpoint.cs # IApiEndpoint implementação
|
|
45
|
+
│ ├── GetAll{Entity}s/ # Handler (sem validator)
|
|
46
|
+
│ ├── Get{Entity}ById/ # Handler + Validator
|
|
47
|
+
│ ├── Update{Entity}/ # Handler + Validator
|
|
48
|
+
│ └── Delete{Entity}/ # Handler + Validator
|
|
49
|
+
├── Migrations/ # EF Core migrations
|
|
50
|
+
├── Pipelines/
|
|
51
|
+
│ ├── ValidationDecorator.cs # Decorator que executa validators
|
|
52
|
+
│ └── LoggingDecorator.cs # Decorator que loga request/failure
|
|
53
|
+
├── Repository/
|
|
54
|
+
│ ├── IRepository.cs # IRepository<T> genérico
|
|
55
|
+
│ ├── Repository.cs # Repository<T> : IRepository<T>
|
|
56
|
+
│ ├── IUnitOfWork.cs # IUnitOfWork
|
|
57
|
+
│ └── UnitOfWork.cs # UnitOfWork
|
|
58
|
+
└── Program.cs
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Abstractions (código real)
|
|
64
|
+
|
|
65
|
+
### `IHandler<TRequest, TResponse>`
|
|
66
|
+
|
|
67
|
+
```csharp
|
|
68
|
+
namespace {ProjectName}.Abstractions
|
|
69
|
+
{
|
|
70
|
+
public interface IHandler<in TRequest, TResponse>
|
|
71
|
+
{
|
|
72
|
+
Task<TResponse> HandleAsync(TRequest command, CancellationToken cancellationToken);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### `IApiEndpoint`
|
|
78
|
+
|
|
79
|
+
```csharp
|
|
80
|
+
namespace {ProjectName}.Abstractions
|
|
81
|
+
{
|
|
82
|
+
public interface IApiEndpoint
|
|
83
|
+
{
|
|
84
|
+
void MapEndpoint(WebApplication app);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### `Error` (record com factory methods)
|
|
90
|
+
|
|
91
|
+
```csharp
|
|
92
|
+
namespace {ProjectName}.Abstractions.Errors;
|
|
93
|
+
|
|
94
|
+
public record Error(string Code, string? Description = default, ErrorType Type = ErrorType.Failure)
|
|
95
|
+
{
|
|
96
|
+
public static readonly Error None = new(string.Empty);
|
|
97
|
+
public static readonly Error Null = new("Error.NullValue", "The specified result value is null.");
|
|
98
|
+
|
|
99
|
+
public static implicit operator Result(Error error) => Result.Failure(error);
|
|
100
|
+
|
|
101
|
+
public static Error Failure(string code, string description) =>
|
|
102
|
+
new(code, description, ErrorType.Failure);
|
|
103
|
+
public static Error Unexpected(string code, string description) =>
|
|
104
|
+
new(code, description, ErrorType.Unexpected);
|
|
105
|
+
public static Error Validation(string code, string description) =>
|
|
106
|
+
new(code, description, ErrorType.Validation);
|
|
107
|
+
public static Error Conflict(string code, string description) =>
|
|
108
|
+
new(code, description, ErrorType.Conflict);
|
|
109
|
+
public static Error NotFound(string code, string description) =>
|
|
110
|
+
new(code, description, ErrorType.NotFound);
|
|
111
|
+
public static Error Unauthorized(string code, string description) =>
|
|
112
|
+
new(code, description, ErrorType.Unauthorized);
|
|
113
|
+
public static Error Forbidden(string code, string description) =>
|
|
114
|
+
new(code, description, ErrorType.Forbidden);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public enum ErrorType
|
|
118
|
+
{
|
|
119
|
+
Failure, Unexpected, Validation, Conflict, NotFound, Unauthorized, Forbidden, Custom
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### `ValidationError`
|
|
124
|
+
|
|
125
|
+
```csharp
|
|
126
|
+
namespace {ProjectName}.Abstractions.Errors;
|
|
127
|
+
|
|
128
|
+
public sealed record ValidationError : Error
|
|
129
|
+
{
|
|
130
|
+
public ValidationError(Error[] errors)
|
|
131
|
+
: base("Validation.General", "One or more validation errors occurred", ErrorType.Validation)
|
|
132
|
+
{
|
|
133
|
+
Errors = errors;
|
|
134
|
+
}
|
|
135
|
+
public Error[] Errors { get; }
|
|
136
|
+
public static ValidationError FromResults(IEnumerable<Result> results) =>
|
|
137
|
+
new(results.Where(r => r.IsFailure).Select(r => r.Error).ToArray());
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### `Result<T>` / `Result`
|
|
142
|
+
|
|
143
|
+
```csharp
|
|
144
|
+
namespace {ProjectName}.Abstractions;
|
|
145
|
+
|
|
146
|
+
public class Result
|
|
147
|
+
{
|
|
148
|
+
protected internal Result(bool isSuccess, Error error) { ... }
|
|
149
|
+
public bool IsSuccess { get; }
|
|
150
|
+
public bool IsFailure => !IsSuccess;
|
|
151
|
+
public Error Error { get; }
|
|
152
|
+
|
|
153
|
+
public static Result Success() => new(true, Error.None);
|
|
154
|
+
public static Result<TValue> Success<TValue>(TValue value) => new(value, true, Error.None);
|
|
155
|
+
public static Result Failure(Error error) => new(false, error);
|
|
156
|
+
public static Result<TValue> Failure<TValue>(Error error) => new(default, false, error);
|
|
157
|
+
public static Result<TValue> Create<TValue>(TValue? value) =>
|
|
158
|
+
value is not null ? Success(value) : Failure<TValue>(Error.Null);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public class Result<TValue> : Result
|
|
162
|
+
{
|
|
163
|
+
private readonly TValue? _value;
|
|
164
|
+
public TValue Value => IsSuccess ? _value! : throw new InvalidOperationException("...");
|
|
165
|
+
|
|
166
|
+
// Implicit conversions
|
|
167
|
+
public static implicit operator Result<TValue>(TValue? value) => Create(value);
|
|
168
|
+
public static implicit operator Result<TValue>(Error error) => Failure<TValue>(error);
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Anatomia de uma Slice (5 arquivos por operação)
|
|
175
|
+
|
|
176
|
+
Cada operação CRUD vive num subfolder dentro de `Features/{Entity}Feature/`:
|
|
177
|
+
|
|
178
|
+
### 1. Request + Response + Handler (1 arquivo)
|
|
179
|
+
|
|
180
|
+
```csharp
|
|
181
|
+
// Features/BookFeature/CreateBook/CreateBookHandler.cs
|
|
182
|
+
namespace {ProjectName}.Features.BookFeature.CreateBook;
|
|
183
|
+
|
|
184
|
+
// Records definidos no mesmo arquivo do Handler
|
|
185
|
+
public sealed record CreateBookRequest(string Title, string Author, string ISBN, decimal Price, int PublishedYear);
|
|
186
|
+
public sealed record CreateBookResponse(Guid Id, string Title, string Author, string ISBN, decimal Price, int PublishedYear);
|
|
187
|
+
|
|
188
|
+
public sealed class CreateBookHandler(
|
|
189
|
+
IRepository<Book> _bookRepo,
|
|
190
|
+
IUnitOfWork _unitOfWork) : IHandler<CreateBookRequest, Result<CreateBookResponse>>
|
|
191
|
+
{
|
|
192
|
+
public async Task<Result<CreateBookResponse>> HandleAsync(
|
|
193
|
+
CreateBookRequest command, CancellationToken cancellationToken)
|
|
194
|
+
{
|
|
195
|
+
var book = new Book
|
|
196
|
+
{
|
|
197
|
+
Id = Guid.CreateVersion7(),
|
|
198
|
+
Title = command.Title,
|
|
199
|
+
Author = command.Author,
|
|
200
|
+
ISBN = command.ISBN,
|
|
201
|
+
Price = command.Price,
|
|
202
|
+
PublishedYear = command.PublishedYear
|
|
203
|
+
};
|
|
204
|
+
await _bookRepo.AddAsync(book, cancellationToken);
|
|
205
|
+
await _unitOfWork.CommitAsync(cancellationToken);
|
|
206
|
+
return Result.Success(new CreateBookResponse(
|
|
207
|
+
book.Id, book.Title, book.Author, book.ISBN, book.Price, book.PublishedYear));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### 2. Validator
|
|
213
|
+
|
|
214
|
+
```csharp
|
|
215
|
+
// Features/BookFeature/CreateBook/CreateBookValidator.cs
|
|
216
|
+
using FluentValidation;
|
|
217
|
+
namespace {ProjectName}.Features.BookFeature.CreateBook;
|
|
218
|
+
|
|
219
|
+
public class CreateBookValidator : AbstractValidator<CreateBookRequest>
|
|
220
|
+
{
|
|
221
|
+
public CreateBookValidator()
|
|
222
|
+
{
|
|
223
|
+
RuleFor(c => c.Title).NotEmpty().MaximumLength(200);
|
|
224
|
+
RuleFor(c => c.Author).NotEmpty().MaximumLength(100);
|
|
225
|
+
RuleFor(c => c.ISBN).NotEmpty();
|
|
226
|
+
RuleFor(c => c.Price).GreaterThan(0);
|
|
227
|
+
RuleFor(c => c.PublishedYear)
|
|
228
|
+
.GreaterThan(1000)
|
|
229
|
+
.LessThanOrEqualTo(DateTime.UtcNow.Year);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### 3. Endpoint (Minimal API)
|
|
235
|
+
|
|
236
|
+
```csharp
|
|
237
|
+
// Features/BookFeature/CreateBook/CreateBookEndpoint.cs
|
|
238
|
+
namespace {ProjectName}.Features.BookFeature.CreateBook;
|
|
239
|
+
|
|
240
|
+
internal sealed class CreateBookEndpoint : IApiEndpoint
|
|
241
|
+
{
|
|
242
|
+
public void MapEndpoint(WebApplication app)
|
|
243
|
+
{
|
|
244
|
+
app.MapPost("books", async (
|
|
245
|
+
IHandler<CreateBookRequest, Result<CreateBookResponse>> handler,
|
|
246
|
+
CreateBookRequest command,
|
|
247
|
+
CancellationToken cancellationToken) =>
|
|
248
|
+
{
|
|
249
|
+
var result = await handler.HandleAsync(command, cancellationToken);
|
|
250
|
+
return result.Match(
|
|
251
|
+
onSuccess: () => Results.Ok(result.Value),
|
|
252
|
+
onFailure: error => Results.BadRequest(error));
|
|
253
|
+
})
|
|
254
|
+
.WithTags(ApiTags.Books)
|
|
255
|
+
.Produces<CreateBookResponse>(StatusCodes.Status200OK)
|
|
256
|
+
.Produces(StatusCodes.Status400BadRequest);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### 4. Errors (por feature, não por operação)
|
|
262
|
+
|
|
263
|
+
```csharp
|
|
264
|
+
// Features/BookFeature/BookErrors.cs
|
|
265
|
+
using {ProjectName}.Abstractions.Errors;
|
|
266
|
+
namespace {ProjectName}.Features.BookFeature;
|
|
267
|
+
|
|
268
|
+
public static class BookErrors
|
|
269
|
+
{
|
|
270
|
+
public static Error NotFound(Guid id) =>
|
|
271
|
+
Error.NotFound("Books.NotFound", $"The Book with Id '{id}' was not found");
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Pipelines (Decorators)
|
|
278
|
+
|
|
279
|
+
Os handlers são decorados automaticamente via Scrutor:
|
|
280
|
+
|
|
281
|
+
### ValidationDecorator
|
|
282
|
+
|
|
283
|
+
- Executa todos os `IValidator<TRequest>` registrados
|
|
284
|
+
- Se houver falhas → retorna `Result.Failure(new ValidationError([...]))` sem chamar o handler
|
|
285
|
+
- Se `TResponse` não for `Result` ou `Result<T>` → lança `InvalidOperationException`
|
|
286
|
+
|
|
287
|
+
### LoggingDecorator
|
|
288
|
+
|
|
289
|
+
- Loga `Handling request {RequestName}` antes
|
|
290
|
+
- Se `result.IsFailure` → `LogWarning` com `result.Error.Code`
|
|
291
|
+
- Caso contrário → `LogInformation` "Handled request"
|
|
292
|
+
|
|
293
|
+
### Ordem de registro (via `AddHandlersFromAssembly`)
|
|
294
|
+
|
|
295
|
+
```csharp
|
|
296
|
+
// Extensions/HandlerRegistrationExtensions.cs
|
|
297
|
+
services.Decorate(typeof(IHandler<,>), typeof(ValidationDecorator<,>));
|
|
298
|
+
services.Decorate(typeof(IHandler<,>), typeof(LoggingDecorator<,>));
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Ordem de execução (innermost-first): `LoggingDecorator → ValidationDecorator → Handler`
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## Repository Pattern
|
|
306
|
+
|
|
307
|
+
```csharp
|
|
308
|
+
// Repository/IRepository.cs
|
|
309
|
+
public interface IRepository<T> where T : class
|
|
310
|
+
{
|
|
311
|
+
Task<IEnumerable<T>> GetAllAsync(CancellationToken cancellationToken = default);
|
|
312
|
+
Task<T?> GetByIdAsync(object id, CancellationToken cancellationToken = default);
|
|
313
|
+
Task<T> AddAsync(T entity, CancellationToken cancellationToken = default);
|
|
314
|
+
Task UpdateAsync(T entity, CancellationToken cancellationToken = default);
|
|
315
|
+
Task DeleteAsync(T entity, CancellationToken cancellationToken = default);
|
|
316
|
+
IEnumerable<T> ExecuteQuery(string query, DbParameter[] dbParams);
|
|
317
|
+
Task AddRangeAsync(IEnumerable<T> entities, CancellationToken cancellationToken = default);
|
|
318
|
+
void RemoveRange(IEnumerable<T> entities);
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## DI Registration (Program.cs)
|
|
325
|
+
|
|
326
|
+
```csharp
|
|
327
|
+
// Repositories + Unit of Work
|
|
328
|
+
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
|
|
329
|
+
builder.Services.AddScoped(typeof(IUnitOfWork), typeof(UnitOfWork));
|
|
330
|
+
|
|
331
|
+
// EF Core (SQLite/SqlServer/PostgreSQL)
|
|
332
|
+
builder.Services.AddSQLDatabaseConfiguration(builder.Configuration);
|
|
333
|
+
|
|
334
|
+
// Endpoints (auto-discovery via reflection)
|
|
335
|
+
builder.Services.RegisterApiEndpointsFromAssembly(Assembly.GetExecutingAssembly());
|
|
336
|
+
|
|
337
|
+
// FluentValidation (auto-discovery)
|
|
338
|
+
builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);
|
|
339
|
+
|
|
340
|
+
// Handlers + Scrutor decorators (ValidationDecorator + LoggingDecorator)
|
|
341
|
+
builder.Services.AddHandlersFromAssembly(typeof(Program).Assembly);
|
|
342
|
+
|
|
343
|
+
// Exception handler + ProblemDetails
|
|
344
|
+
builder.Services.AddExceptionHandler<CustomExceptionHandler>().AddProblemDetails();
|
|
345
|
+
|
|
346
|
+
// Map endpoints
|
|
347
|
+
app.MapApiEndpoints();
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Convenções de Nomenclatura
|
|
353
|
+
|
|
354
|
+
| Elemento | Convenção | Exemplo |
|
|
355
|
+
|----------|-----------|---------|
|
|
356
|
+
| Feature folder | `{Entity}Feature` | `BookFeature` |
|
|
357
|
+
| Operation folder | `{Verb}{Entity}` | `CreateBook`, `GetBookById` |
|
|
358
|
+
| Request record | `{Verb}{Entity}Request` | `CreateBookRequest` |
|
|
359
|
+
| Response record | `{Verb}{Entity}Response` | `CreateBookResponse` |
|
|
360
|
+
| Handler class | `{Verb}{Entity}Handler` | `CreateBookHandler` |
|
|
361
|
+
| Validator class | `{Verb}{Entity}Validator` | `CreateBookValidator` |
|
|
362
|
+
| Endpoint class | `{Verb}{Entity}Endpoint` | `CreateBookEndpoint` |
|
|
363
|
+
| Errors class | `{Entity}Errors` | `BookErrors` (static, shared por feature) |
|
|
364
|
+
| DTO record (GetAll) | `{Entity}Dto` | `BookDto` |
|
|
365
|
+
| API tag constant | `ApiTags.{Entities}` | `ApiTags.Books` |
|
|
366
|
+
| Entity class | `{Entity}` (`sealed`) | `Book` |
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## Padrões Obrigatórios
|
|
371
|
+
|
|
372
|
+
1. **Handler records no mesmo arquivo**: `Request` e `Response` são declarados no mesmo arquivo `.cs` que o `Handler`.
|
|
373
|
+
2. **`sealed` em tudo**: handlers, endpoints, validators, request/response records usam `sealed`.
|
|
374
|
+
3. **`Guid.CreateVersion7()`** para gerar IDs (não `Guid.NewGuid()`).
|
|
375
|
+
4. **`result.Match()`** nos endpoints — nunca acessar `result.Value` sem checar `IsSuccess`.
|
|
376
|
+
5. **IApiEndpoint `internal sealed`** — endpoints não são públicos.
|
|
377
|
+
6. **`{Entity}Errors` compartilhado** por todas as operações da feature (1 arquivo por feature, não por slice).
|
|
378
|
+
7. **Repository injetado pelo handler** diretamente (sem Application Service layer).
|
|
379
|
+
8. **`IUnitOfWork.CommitAsync()`** em operações de escrita (Create, Update, Delete).
|
|
380
|
+
9. **Sem validator em GetAll** — GetAllBooksHandler não tem validator (sem parâmetros para validar).
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
## Anti-patterns (NUNCA fazer em VSA)
|
|
385
|
+
|
|
386
|
+
| Anti-pattern | Por quê não |
|
|
387
|
+
|--------------|-------------|
|
|
388
|
+
| `AggregateRoot` | VSA não usa DDD — entidade simples com `sealed class` |
|
|
389
|
+
| `DomainEvent` | VSA não tem Domain Events — reações são handlers separados |
|
|
390
|
+
| `MediatR` / `IRequest` / `IRequestHandler` | Substituído por `IHandler<TRequest, TResponse>` próprio |
|
|
391
|
+
| DTOs compartilhados entre features | Cada slice define seus próprios records |
|
|
392
|
+
| Application Service layer | Handler acessa Repository diretamente |
|
|
393
|
+
| CQRS formal (Commands/Queries) | Handler simples, sem distinção Command/Query |
|
|
394
|
+
| Shared Validators em Application layer | Validator fica no mesmo namespace do slice |
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## NuGet Packages (versões exatas do template)
|
|
399
|
+
|
|
400
|
+
```xml
|
|
401
|
+
<PackageReference Include="FluentValidation" Version="12.1.1" />
|
|
402
|
+
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
|
|
403
|
+
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.3" />
|
|
404
|
+
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.3" />
|
|
405
|
+
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.3" />
|
|
406
|
+
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
|
407
|
+
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.3" />
|
|
408
|
+
<PackageReference Include="Scalar.AspNetCore" Version="2.12.40" />
|
|
409
|
+
<PackageReference Include="Scrutor" Version="7.0.0" />
|
|
410
|
+
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="9.0.0" />
|
|
411
|
+
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.3" />
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**Target Framework:** `net10.0`
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## Quando Usar VSA vs DDD
|
|
419
|
+
|
|
420
|
+
| Critério | VSA | DDD (Levels 1-3) |
|
|
421
|
+
|----------|-----|------------------|
|
|
422
|
+
| Feature é CRUD com validações | ✅ Ideal | Overkill |
|
|
423
|
+
| Feature tem regras de negócio complexas | ⚠️ Possível mas complexo | ✅ Ideal |
|
|
424
|
+
| Feature tem Domain Events com consumidores | ❌ Fora do escopo | ✅ Ideal |
|
|
425
|
+
| Time prefere simplicidade / explicitness | ✅ | ⚠️ |
|
|
426
|
+
| Projeto com múltiplos Bounded Contexts | ❌ | ✅ Ideal |
|
|
427
|
+
| Stack .NET 10 + Minimal APIs | ✅ Native | ✅ Adaptável |
|
|
428
|
+
|
|
429
|
+
**Regra prática:** Se o projeto já usa VSA (detectado por `Abstractions/IHandler.cs`), continue com VSA. Não misture VSA e DDD no mesmo projeto.
|