# Vertical Slice Architecture (VSA)

> **Scope:** universal
> **Layer:** 2
> **Keywords:** vertical-slice, vsa, minimal-api, ihandler, result-pattern, feature-slice, scrutor, fluentvalidation

**Verified against:** .NET 10 + EF Core 10 + FluentValidation 12 + Scrutor 7. Last-verified: 2026-05-20.

---

Padrão arquitetural baseado em **KanaiyaKatarmal/VerticalSliceArchitectureTemplate** (.NET 10, Minimal APIs, EF Core 10).
Cada feature é uma fatia vertical completa: endpoint → handler → validator → entity → DB.
Sem MediatR, sem AggregateRoot, sem Domain Events, sem CQRS formal, sem repository genérico
(divergência intencional do template base: o handler usa `ApplicationDbContext` direto).

---

## Estrutura de Pastas

```
{ProjectName}/
├── Abstractions/
│   ├── IHandler.cs              # Interface central: IHandler<TRequest, TResponse>
│   ├── IApiEndpoint.cs          # Interface de endpoint: void MapEndpoint(IEndpointRouteBuilder app)
│   ├── Result.cs                # Result<T> / Result (non-generic)
│   └── Errors/
│       ├── Error.cs             # record Error com factory methods + ErrorType enum
│       └── ValidationError.cs   # ValidationError : Error com array de erros
├── Constants/
│   └── ApiTags.cs               # Constantes de tags para OpenAPI/Swagger
├── Database/
│   └── ApplicationDbContext.cs  # DbContext com DbSet<Entity>
├── Entities/
│   └── {Entity}.cs              # Entidade simples (sem AggregateRoot)
├── Exceptions/
│   └── CustomExceptionHandler.cs # IExceptionHandler para ProblemDetails
├── Extensions/
│   ├── HandlerRegistrationExtensions.cs  # AddHandlersFromAssembly() com Scrutor Decorate
│   ├── MapEndpointExtensions.cs          # RegisterApiEndpointsFromAssembly() + MapApiEndpoints()
│   └── ResultExtensions.cs              # result.Match(onSuccess, onFailure)
├── Features/
│   └── {Entity}Feature/
│       ├── {Entity}Errors.cs            # static class {Entity}Errors (factory methods)
│       ├── Create{Entity}/
│       │   ├── Create{Entity}Request.cs  # sealed record request
│       │   ├── ...Response.cs            # sealed record response
│       │   ├── Create{Entity}Handler.cs  # IHandler<Request, Result<Response>>
│       │   ├── Create{Entity}Validator.cs # AbstractValidator<Request>
│       │   └── Create{Entity}Endpoint.cs # IApiEndpoint implementação
│       ├── GetAll{Entity}s/              # Handler (sem validator)
│       ├── Get{Entity}ById/              # Handler + Validator
│       ├── Update{Entity}/               # Handler + Validator
│       └── Delete{Entity}/               # Handler + Validator
├── Migrations/                           # EF Core migrations
├── Pipelines/
│   ├── ValidationDecorator.cs            # Decorator que executa validators
│   └── LoggingDecorator.cs               # Decorator que loga request/failure
└── Program.cs
```

---

## Abstractions (código real)

### `IHandler<TRequest, TResponse>`

```csharp
namespace {ProjectName}.Abstractions
{
    public interface IHandler<in TRequest, TResponse>
    {
        Task<TResponse> HandleAsync(TRequest command, CancellationToken cancellationToken);
    }
}
```

### `IApiEndpoint`

```csharp
namespace {ProjectName}.Abstractions
{
    public interface IApiEndpoint
    {
        void MapEndpoint(IEndpointRouteBuilder app);
    }
}
```

### `Error` (record com factory methods)

```csharp
namespace {ProjectName}.Abstractions.Errors;

public record Error(string Code, string? Description = default, ErrorType Type = ErrorType.Failure)
{
    public static readonly Error None = new(string.Empty);
    public static readonly Error Null = new("Error.NullValue", "The specified result value is null.");

    public static implicit operator Result(Error error) => Result.Failure(error);

    public static Error Failure(string code, string description) =>
        new(code, description, ErrorType.Failure);
    public static Error Unexpected(string code, string description) =>
        new(code, description, ErrorType.Unexpected);
    public static Error Validation(string code, string description) =>
        new(code, description, ErrorType.Validation);
    public static Error Conflict(string code, string description) =>
        new(code, description, ErrorType.Conflict);
    public static Error NotFound(string code, string description) =>
        new(code, description, ErrorType.NotFound);
    public static Error Unauthorized(string code, string description) =>
        new(code, description, ErrorType.Unauthorized);
    public static Error Forbidden(string code, string description) =>
        new(code, description, ErrorType.Forbidden);
}

public enum ErrorType
{
    Failure, Unexpected, Validation, Conflict, NotFound, Unauthorized, Forbidden, Custom
}
```

### `ValidationError`

```csharp
namespace {ProjectName}.Abstractions.Errors;

public sealed record ValidationError : Error
{
    public ValidationError(Error[] errors)
        : base("Validation.General", "One or more validation errors occurred", ErrorType.Validation)
    {
        Errors = errors;
    }
    public Error[] Errors { get; }
    public static ValidationError FromResults(IEnumerable<Result> results) =>
        new(results.Where(r => r.IsFailure).Select(r => r.Error).ToArray());
}
```

### `Result<T>` / `Result`

```csharp
namespace {ProjectName}.Abstractions;

public class Result
{
    protected internal Result(bool isSuccess, Error error) { ... }
    public bool IsSuccess { get; }
    public bool IsFailure => !IsSuccess;
    public Error Error { get; }

    public static Result Success() => new(true, Error.None);
    public static Result<TValue> Success<TValue>(TValue value) => new(value, true, Error.None);
    public static Result Failure(Error error) => new(false, error);
    public static Result<TValue> Failure<TValue>(Error error) => new(default, false, error);
    public static Result<TValue> Create<TValue>(TValue? value) =>
        value is not null ? Success(value) : Failure<TValue>(Error.Null);
}

public class Result<TValue> : Result
{
    private readonly TValue? _value;
    public TValue Value => IsSuccess ? _value! : throw new InvalidOperationException("...");

    // Implicit conversions
    public static implicit operator Result<TValue>(TValue? value) => Create(value);
    public static implicit operator Result<TValue>(Error error) => Failure<TValue>(error);
}
```

---

## Anatomia de uma Slice (5 arquivos por operação)

Cada operação CRUD vive num subfolder dentro de `Features/{Entity}Feature/`:

### 1. Request + Response + Handler (1 arquivo)

```csharp
// Features/BookFeature/CreateBook/CreateBookHandler.cs
namespace {ProjectName}.Features.BookFeature.CreateBook;

// Records definidos no mesmo arquivo do Handler
public sealed record CreateBookRequest(string Title, string Author, string ISBN, decimal Price, int PublishedYear);
public sealed record CreateBookResponse(Guid Id, string Title, string Author, string ISBN, decimal Price, int PublishedYear);

public sealed class CreateBookHandler(
    ApplicationDbContext _db) : IHandler<CreateBookRequest, Result<CreateBookResponse>>
{
    public async Task<Result<CreateBookResponse>> HandleAsync(
        CreateBookRequest command, CancellationToken cancellationToken)
    {
        var book = new Book
        {
            Id = Guid.CreateVersion7(),
            Title = command.Title,
            Author = command.Author,
            ISBN = command.ISBN,
            Price = command.Price,
            PublishedYear = command.PublishedYear
        };
        _db.Books.Add(book);
        await _db.SaveChangesAsync(cancellationToken);
        return Result.Success(new CreateBookResponse(
            book.Id, book.Title, book.Author, book.ISBN, book.Price, book.PublishedYear));
    }
}
```

### 2. Validator

```csharp
// Features/BookFeature/CreateBook/CreateBookValidator.cs
using FluentValidation;
namespace {ProjectName}.Features.BookFeature.CreateBook;

public class CreateBookValidator : AbstractValidator<CreateBookRequest>
{
    public CreateBookValidator()
    {
        RuleFor(c => c.Title).NotEmpty().MaximumLength(200);
        RuleFor(c => c.Author).NotEmpty().MaximumLength(100);
        RuleFor(c => c.ISBN).NotEmpty();
        RuleFor(c => c.Price).GreaterThan(0);
        RuleFor(c => c.PublishedYear)
            .GreaterThan(1000)
            .LessThanOrEqualTo(DateTime.UtcNow.Year);
    }
}
```

### 3. Endpoint (Minimal API)

```csharp
// Features/BookFeature/CreateBook/CreateBookEndpoint.cs
namespace {ProjectName}.Features.BookFeature.CreateBook;

internal sealed class CreateBookEndpoint : IApiEndpoint
{
    public void MapEndpoint(IEndpointRouteBuilder app)
    {
        app.MapPost("books", async (
            IHandler<CreateBookRequest, Result<CreateBookResponse>> handler,
            CreateBookRequest command,
            CancellationToken cancellationToken) =>
        {
            var result = await handler.HandleAsync(command, cancellationToken);
            return result.Match(
                onSuccess: () => Results.Ok(result.Value),
                onFailure: error => Results.BadRequest(error));
        })
        .WithTags(ApiTags.Books)
        .Produces<CreateBookResponse>(StatusCodes.Status200OK)
        .Produces(StatusCodes.Status400BadRequest);
    }
}
```

### 4. Errors (por feature, não por operação)

```csharp
// Features/BookFeature/BookErrors.cs
using {ProjectName}.Abstractions.Errors;
namespace {ProjectName}.Features.BookFeature;

public static class BookErrors
{
    public static Error NotFound(Guid id) =>
        Error.NotFound("Books.NotFound", $"The Book with Id '{id}' was not found");
}
```

---

## Pipelines (Decorators)

Os handlers são decorados automaticamente via Scrutor:

### ValidationDecorator

- Executa todos os `IValidator<TRequest>` registrados
- Se houver falhas → retorna `Result.Failure(new ValidationError([...]))` sem chamar o handler
- Se `TResponse` não for `Result` ou `Result<T>` → lança `InvalidOperationException`

### LoggingDecorator

- Loga `Handling request {RequestName}` antes
- Se `result.IsFailure` → `LogWarning` com `result.Error.Code`
- Caso contrário → `LogInformation` "Handled request"

### Ordem de registro (via `AddHandlersFromAssembly`)

```csharp
// Extensions/HandlerRegistrationExtensions.cs
services.Decorate(typeof(IHandler<,>), typeof(ValidationDecorator<,>));
services.Decorate(typeof(IHandler<,>), typeof(LoggingDecorator<,>));
```

Ordem de execução (innermost-first): `LoggingDecorator → ValidationDecorator → Handler`

---

## Acesso a Dados — DbContext direto no slice

O handler injeta `ApplicationDbContext` direto. Sobre EF Core um repository genérico é
redundante: `DbSet<T>` já é repository, `DbContext` já é unit of work. **Nunca crie
`IRepository<T>`/`IUnitOfWork`** — a auditoria de projetos reais mostrou que o par nasce
registrado no DI e morre órfão, sem consumidores.

```csharp
// CRUD por chave — direto no DbSet
var book = await _db.Books.FindAsync([command.Id], cancellationToken);
_db.Books.Add(book);
await _db.SaveChangesAsync(cancellationToken);

// Queries específicas — LINQ no slice (joins, projeção, paginação, filtros compostos)
var page = await _db.Books
    .Where(b => b.Author == command.Author)
    .OrderBy(b => b.Title)
    .Skip(command.Skip).Take(command.Take)
    .Select(b => new BookDto(b.Id, b.Title))
    .ToListAsync(cancellationToken);
```

**Stores dedicados (exceção justificada):** quando um slice exige controle de concorrência
(ex.: `SELECT ... FOR UPDATE`, retry de `DbUpdateConcurrencyException`) ou SQL específico
que o LINQ não expressa, escreva um store hand-written e purpose-built (ex.: `OutboxStore`,
`LeaseStore`) com métodos nomeados pela intenção — e registre a justificativa em
`decisions.md`. Nunca um genérico por entidade.

Veja `backend/dotnet/vsa-handler-patterns.md` → "Data Access: DbContext in the Slice".

---

## DI Registration (Program.cs)

```csharp
// EF Core (SQLite/SqlServer/PostgreSQL)
builder.Services.AddSQLDatabaseConfiguration(builder.Configuration);

// Endpoints (auto-discovery via reflection)
builder.Services.RegisterApiEndpointsFromAssembly(Assembly.GetExecutingAssembly());

// FluentValidation (auto-discovery)
builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);

// Handlers + Scrutor decorators (ValidationDecorator + LoggingDecorator)
builder.Services.AddHandlersFromAssembly(typeof(Program).Assembly);

// Exception handler + ProblemDetails
builder.Services.AddExceptionHandler<CustomExceptionHandler>().AddProblemDetails();

// Map endpoints
app.MapApiEndpoints();
```

---

## Convenções de Nomenclatura

| Elemento | Convenção | Exemplo |
|----------|-----------|---------|
| Feature folder | `{Entity}Feature` | `BookFeature` |
| Operation folder | `{Verb}{Entity}` | `CreateBook`, `GetBookById` |
| Request record | `{Verb}{Entity}Request` | `CreateBookRequest` |
| Response record | `{Verb}{Entity}Response` | `CreateBookResponse` |
| Handler class | `{Verb}{Entity}Handler` | `CreateBookHandler` |
| Validator class | `{Verb}{Entity}Validator` | `CreateBookValidator` |
| Endpoint class | `{Verb}{Entity}Endpoint` | `CreateBookEndpoint` |
| Errors class | `{Entity}Errors` | `BookErrors` (static, shared por feature) |
| DTO record (GetAll) | `{Entity}Dto` | `BookDto` |
| API tag constant | `ApiTags.{Entities}` | `ApiTags.Books` |
| Entity class | `{Entity}` (`sealed`) | `Book` |

---

## Padrões Obrigatórios

1. **Handler records no mesmo arquivo**: `Request` e `Response` são declarados no mesmo arquivo `.cs` que o `Handler`.
2. **`sealed` em tudo**: handlers, endpoints, validators, request/response records usam `sealed`.
3. **`Guid.CreateVersion7()`** para gerar IDs (não `Guid.NewGuid()`).
4. **`result.Match()`** nos endpoints — nunca acessar `result.Value` sem checar `IsSuccess`.
5. **IApiEndpoint `internal sealed`** — endpoints não são públicos.
6. **`{Entity}Errors` compartilhado** por todas as operações da feature (1 arquivo por feature, não por slice).
7. **`ApplicationDbContext` direto no slice**: CRUD por chave via `DbSet<T>`; queries específicas (joins, projeção, paginação) via LINQ no handler. Nunca uma Application Service layer entre handler e dados; nunca repository genérico (`IRepository<T>`/`IUnitOfWork`).
8. **`SaveChangesAsync()`** explícito em operações de escrita (Create, Update, Delete).
9. **Sem validator em GetAll** — GetAllBooksHandler não tem validator (sem parâmetros para validar).

---

## Anti-patterns (NUNCA fazer em VSA)

| Anti-pattern | Por quê não |
|--------------|-------------|
| `AggregateRoot` | VSA não usa DDD — entidade simples com `sealed class` |
| `DomainEvent` | VSA não tem Domain Events — reações são handlers separados |
| `MediatR` / `IRequest` / `IRequestHandler` | Substituído por `IHandler<TRequest, TResponse>` próprio |
| DTOs compartilhados entre features | Cada slice define seus próprios records |
| Application Service layer | Handler acessa o `ApplicationDbContext` diretamente |
| Repository genérico (`IRepository<T>`/`IUnitOfWork`) | `DbSet<T>` já é repository, `DbContext` já é unit of work — vira código morto |
| CQRS formal (Commands/Queries) | Handler simples, sem distinção Command/Query |
| Shared Validators em Application layer | Validator fica no mesmo namespace do slice |

---

## NuGet Packages (versões exatas do template)

```xml
<PackageReference Include="FluentValidation" Version="12.1.1" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.*" /> <!-- use wildcard — Npgsql releases lag behind EF Core -->
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.3" />
<PackageReference Include="Scalar.AspNetCore" Version="2.12.40" />
<PackageReference Include="Scrutor" Version="7.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.3" />
```

**Target Framework:** `net10.0`

---

## Quando Usar VSA vs DDD

| Critério | VSA | DDD (Levels 1-3) |
|----------|-----|------------------|
| Feature é CRUD com validações | ✅ Ideal | Overkill |
| Feature tem regras de negócio complexas | ⚠️ Possível mas complexo | ✅ Ideal |
| Feature tem Domain Events com consumidores | ❌ Fora do escopo | ✅ Ideal |
| Time prefere simplicidade / explicitness | ✅ | ⚠️ |
| Projeto com múltiplos Bounded Contexts | ❌ | ✅ Ideal |
| Stack .NET 10 + Minimal APIs | ✅ Native | ✅ Adaptável |

**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.
