Skip to content
Back to skills

Vertical Slice

ASecurity

Vertical Slice Architecture (VSA) for .NET APIs — feature-first folder structure, IEndpointGroup pattern, cross-cutting concerns, and testing approach.

  • 5 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added May 28, 2026
ai-agentssqltestingapidatabase

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 23, 2026

npx -y skills add zdanovichnick/dotnet-pilot --skill vertical-slice --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Vertical Slice?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Vertical Slice
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/zdanovichnick-vertical-slice/badge)](https://www.skillsdirectory.com/skills/zdanovichnick-vertical-slice)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: vertical-slice
description: Vertical Slice Architecture (VSA) for .NET APIs — feature-first folder structure, IEndpointGroup pattern, cross-cutting concerns, and testing approach.
when_to_use: Building or reviewing a feature-folder API — a new slice, an IEndpointGroup, or cross-cutting behavior shared between slices.
---

# Vertical Slice Architecture

Reference material for structuring .NET APIs by feature rather than by layer.

## Core Concept

Organize code by **feature (vertical slice)**, not by technical layer (horizontal). Each slice owns everything needed for one feature: endpoint, handler, DTOs, validation. Two features never share request/response types — coupling through shared DTOs is how VSA projects collapse back into layered code.

**Use when** most operations read or write a single entity with little business logic (CRUD-heavy APIs), or Clean Architecture feels like over-engineering.

**Avoid when** invariants span several entities, business rules outgrow CRUD, or event sourcing is on the roadmap — see "When to Use DDD" in `skills/ddd/SKILL.md`. Aggregate count alone doesn't decide it.

## Folder Structure

```
src/MyApp/
  Features/
    Orders/
      CreateOrder/
        CreateOrderEndpoint.cs     # IEndpointGroup implementation
        CreateOrderHandler.cs      # Command handler (or inline in endpoint)
        CreateOrderRequest.cs      # Input DTO record
        CreateOrderResponse.cs     # Output DTO record
        CreateOrderValidator.cs    # FluentValidation validator
      GetOrder/
        GetOrderEndpoint.cs
        GetOrderQuery.cs
        GetOrderResponse.cs
      CancelOrder/
        CancelOrderEndpoint.cs
        CancelOrderCommand.cs
  Domain/                          # Shared only if 2+ features use it
    Orders/
      Order.cs
      OrderStatus.cs
  Infrastructure/
    Persistence/
      AppDbContext.cs
      Configurations/
  Program.cs
```

The `Domain/` folder is for shared domain concepts only. If only one feature references an entity, keep it inside that feature's folder.

## IEndpointGroup Pattern

Each feature registers its own route via `IEndpointGroup`. The interface is registered and scanned in `Program.cs`.

```csharp
public interface IEndpointGroup
{
    void MapEndpoints(IEndpointRouteBuilder app);
}
```

```csharp
// Features/Orders/CreateOrder/CreateOrderEndpoint.cs
public class CreateOrderEndpoint : IEndpointGroup
{
    public void MapEndpoints(IEndpointRouteBuilder app)
    {
        app.MapPost("/orders", HandleAsync)
            .WithName("CreateOrder")
            .AddEndpointFilter<ValidationFilter<CreateOrderRequest>>()
            .RequireAuthorization();
    }

    private static async Task<IResult> HandleAsync(
        CreateOrderRequest request,
        AppDbContext db,
        CancellationToken ct)
    {
        var order = new Order(request.CustomerId, request.Items);
        db.Orders.Add(order);
        await db.SaveChangesAsync(ct);
        return TypedResults.Created($"/orders/{order.Id}", new CreateOrderResponse(order.Id));
    }
}
```

```csharp
// Program.cs — scan and register all IEndpointGroup implementations
var endpointGroups = typeof(Program).Assembly
    .GetTypes()
    .Where(t => t.IsAssignableTo(typeof(IEndpointGroup)) && !t.IsAbstract)
    .Select(Activator.CreateInstance)
    .Cast<IEndpointGroup>();

foreach (var group in endpointGroups)
    group.MapEndpoints(app);
```

## DTOs

Use records. Never share request/response types between features — duplication here is intentional and correct.

```csharp
// CreateOrderRequest.cs
public record CreateOrderRequest(Guid CustomerId, IReadOnlyList<OrderItemRequest> Items);
public record OrderItemRequest(Guid ProductId, int Quantity);

// CreateOrderResponse.cs
public record CreateOrderResponse(Guid OrderId);
```

## Validation

Use FluentValidation per-request. Wire via a generic endpoint filter so individual endpoints don't repeat the call.

```csharp
// Features/Orders/CreateOrder/CreateOrderValidator.cs
public class CreateOrderValidator : AbstractValidator<CreateOrderRequest>
{
    public CreateOrderValidator()
    {
        RuleFor(x => x.CustomerId).NotEmpty();
        RuleFor(x => x.Items).NotEmpty();
        RuleForEach(x => x.Items).ChildRules(item =>
        {
            item.RuleFor(i => i.Quantity).GreaterThan(0);
        });
    }
}
```

```csharp
// Infrastructure/Filters/ValidationFilter.cs
public class ValidationFilter<T>(IValidator<T> validator) : IEndpointFilter
{
    public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext ctx, EndpointFilterDelegate next)
    {
        var request = ctx.GetArgument<T>(0);
        var result = await validator.ValidateAsync(request);
        if (!result.IsValid)
            return TypedResults.ValidationProblem(result.ToDictionary());
        return await next(ctx);
    }
}
```

Register validators and the filter in DI:

```csharp
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
builder.Services.AddScoped(typeof(ValidationFilter<>));
```

## Cross-Cutting Concerns

Handle via endpoint filters, not base classes:

| Concern | Approach |
|---------|----------|
| Validation | `ValidationFilter<TRequest>` |
| Authentication | `.RequireAuthorization()` or policy |
| Logging / timing | `LoggingFilter` via `AddEndpointFilter` |
| Rate limiting | `RateLimiterOptions` + `.RequireRateLimiting()` |

## Extracting to Shared Domain

Extract to `Domain/` **only** when two or more feature slices reference the same concept and it carries meaningful behavior (not just a DTO shape):

- `Order` entity referenced by `CreateOrder`, `CancelOrder`, `GetOrder` → extract to `Domain/Orders/`
- `OrderStatus` enum used across multiple features → extract
- `CreateOrderRequest` used by only one feature → keep it in the feature folder

The `Domain/` folder must stay infrastructure-free: no EF Core attributes, no HTTP types, no Polly.

## Testing

Test each slice independently. Use `WebApplicationFactory` for full-stack slice tests.

```csharp
public class CreateOrderTests(WebApplicationFactory<Program> factory)
    : IClassFixture<WebApplicationFactory<Program>>
{
    [Fact]
    public async Task CreateOrder_ValidRequest_ReturnsCreated()
    {
        var client = factory.CreateClient();
        var request = new CreateOrderRequest(Guid.NewGuid(), [new(Guid.NewGuid(), 2)]);

        var response = await client.PostAsJsonAsync("/orders", request);

        response.StatusCode.Should().Be(HttpStatusCode.Created);
    }
}
```

For handler-only tests, instantiate the handler directly with a `DbContext` pointed at a Testcontainers database (see `skills/testing-dotnet/SKILL.md`). Don't mock `DbContext` or use the EF in-memory provider — it skips constraints, transactions and SQL translation, so tests pass against behaviour production doesn't have.

## Do / Don't

| Do | Don't |
|----|-------|
| Keep each feature folder self-contained | Share Request/Response DTOs between features |
| Use endpoint filters for cross-cutting concerns | Create `BaseHandler` or `BaseEndpoint` base classes |
| Extract to `Domain/` only when truly shared | Put infrastructure code (EF, HTTP) in `Domain/` |
| Name files `<Action><Feature><Suffix>` (e.g., `CreateOrderHandler.cs`) | Mix suffix conventions (`CreateOrderService.cs` + `GetOrderHandler.cs`) |
| Test slices via `WebApplicationFactory` | Mock `DbContext` in slice tests |

## See Also

- `knowledge/decisions/adr-001-vsa-default.md` — why VSA is the default for new APIs
- `knowledge/decisions/adr-005-multi-architecture.md` — when to choose Clean Architecture or DDD instead
- `knowledge/common-infrastructure.md` — `IEndpointGroup` interface definition and bootstrap

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.