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