Vertical Slice Architecture patterns and conventions for .NET Minimal API projects...
This skill defines the Vertical Slice Architecture pattern implementation for .NET projects using Minimal APIs. Organize code by features (vertical slices) rather than technical layers, where each feature contains all code from endpoint to database.
Vertical Slice Architecture organizes code by feature rather than by technical layers. Instead of separating concerns into horizontal layers (UI, business logic, data access), each vertical slice contains all code for a specific feature from endpoint to database.
Benefits:
Contrast with N-Tier Architecture:
Organize your project by features (modules) rather than technical layers. Each feature/module is self-contained in its own folder, containing all code from endpoint to database.
ProjectRoot/
├── Program.cs
├── ProjectName.csproj
├── Features/
│ ├── Cars/
│ │ ├── Setup.cs
│ │ ├── CreateCarEndpoint.cs
│ │ ├── GetCarEndpoint.cs
│ │ ├── UpdateCarEndpoint.cs
│ │ ├── DeleteCarEndpoint.cs
│ │ ├── Car.cs (domain model)
│ │ ├── ICarRepository.cs
│ │ └── CarRepository.cs
│ ├── Users/
│ │ ├── Setup.cs
│ │ ├── CreateUserEndpoint.cs
│ │ ├── GetUserEndpoint.cs
│ │ ├── User.cs
│ │ ├── IUserRepository.cs
│ │ └── UserRepository.cs
│ └── Orders/
│ ├── Setup.cs
│ ├── CreateOrderEndpoint.cs
│ ├── GetOrderEndpoint.cs
│ ├── Order.cs
│ ├── IOrderRepository.cs
│ └── OrderRepository.cs
└── Infrastructure/
├── Database/
│ └── AppDbContext.cs (if using EF Core)
├── Logging/
└── ...
Root Level:
Program.cs - Application entry point and configurationProjectName.csproj - Project fileFeatures Folder:
Setup.cs - Module configuration (DI and endpoint mapping)Infrastructure Folder:
Setup.cs at the module level, not at the rootVertical Slice Architecture follows the principle: "Minimize coupling between slices, maximize coupling within a slice." While features should be self-contained, there are legitimate cases where modules need to interact.
Use Minimal APIs instead of Controllers. Treat the Minimal API endpoint as the application layer itself, not just a thin entry point.
Key principles:
Each endpoint is its own vertical slice, organized as a static class:
public static class CreateCarEndpoint
{
public record Request(string Name, string Model);
public record Response(int Id, string Name, string Model);
public static RouteHandlerBuilder MapCreateCar(this IEndpointRouteBuilder app)
=> app.MapPost("/cars", Handler);
private static async Task<Results<Created<Response>, BadRequest>> Handler(
Request request,
ICarRepository repository,
CancellationToken ct)
{
var car = new Car { Name = request.Name, Model = request.Model };
await repository.AddAsync(car, ct);
return TypedResults.Created($"/cars/{car.Id}", new Response(car.Id, car.Name, car.Model));
}
}
Key points:
CreateCarEndpoint)Map{Operation} uses expression body syntax (=>)Handler method with business logic=>) for methods where possibleFor complete endpoint examples, see endpoint-examples.md.
Each module (e.g., Cars, Users, Orders) has a Setup.cs class:
public static class Setup
{
public static IServiceCollection AddCars(this IServiceCollection services)
{
services.AddScoped<ICarRepository, CarRepository>();
return services;
}
public static IEndpointRouteBuilder MapCars(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/cars")
.WithTags("Cars")
.RequireAuthorization();
group.MapCreateCar();
group.MapGetCar();
group.MapUpdateCar();
group.MapDeleteCar();
return app;
}
}
Key points:
Add{Module} method for dependency injection configurationMap{Module} method for endpoint mapping=>) for simple methodsFor complete module examples, see module-examples.md.
Use strongly-typed discriminant union types for handler return values:
private static async Task<Results<Ok<Product>, NotFound>> Handler(
int id,
IProductRepository repository,
CancellationToken ct)
{
var product = await repository.GetByIdAsync(id, ct);
return product is null
? TypedResults.NotFound()
: TypedResults.Ok(product);
}
Expression body syntax for simple methods:
For any function or method that can be written as a single expression (including handlers and utility methods), use the expression-bodied member syntax (=>):
// Good: Simple Map method
public static RouteHandlerBuilder MapGetCar(this IEndpointRouteBuilder app)
=> app.MapGet("/{id:int}", Handler);
// Good: Simple synchronous method
public int CalculateTotal(int a, int b) => a + b;
Important: Expression body syntax and async methods:
Do NOT use expression body syntax for async methods that require await. Pattern matching on Task<T> does not work - you must await the task first.
// BAD: This does NOT work - pattern matching on Task<Product?>
private static Task<Results<Ok<Product>, NotFound>> Handler(
int id,
IProductRepository repository,
CancellationToken ct)
=> repository.GetByIdAsync(id, ct) is { } product // ERROR: Task<Product?> is not Product?
? Task.FromResult<Results<Ok<Product>, NotFound>>(TypedResults.Ok(product))
: Task.FromResult<Results<Ok<Product>, NotFound>>(TypedResults.NotFound());
// GOOD: Use async/await syntax for async methods
private static async Task<Results<Ok<Product>, NotFound>> Handler(
int id,
IProductRepository repository,
CancellationToken ct)
{
var product = await repository.GetByIdAsync(id, ct);
return product is null
? TypedResults.NotFound()
: TypedResults.Ok(product);
}
Guidelines:
=>) for simple synchronous methods and Map methodsTask.FromResult(...) without awaitMore patterns are supported; these are the most common:
Results<Ok<T>, NotFound> – most common pattern (e.g., details or fetch by ID)Results<Ok<T>, NotFound, BadRequest> – multiple possible results (e.g., validation failure + not found)Results<Created<T>, BadRequest, Conflict> – typical for create scenariosUse the static TypedResults class to create result instances.
Declare all possible results explicitly in the return type. For additional result patterns, see the documentation.
Benefits:
For comprehensive Results patterns, see results-patterns.md.
Use MapGroup to apply common properties to groups of endpoints:
var carsGroup = app.MapGroup("/api/cars")
.WithTags("Cars")
.WithOpenApi()
.RequireAuthorization("AdminPolicy")
.WithSummary("Car management endpoints");
carsGroup.MapCreateCar();
carsGroup.MapGetCar();
Common properties applied via MapGroup:
/api/cars applied to all endpointsPattern:
Setup.Map{Module} methodMap method=>) for methods where possible, especially for Map methods and simple handlersCancellationToken in async handlersdotnet-technology-stack skill for technology choicesThis skill includes detailed examples and templates: