Roadmap › .NET Project Architectures & Folder Structures — Layered, Clean, Onion, Vertical Slice › Module: Architecture Styles
Lessons in this course (8)
4. Vertical Slice architecture
Intermediate★ Must knowLesson 4 of 8
Instead of horizontal layers, organise by feature/use case: everything needed for "Place order" lives together.
text
src/Shop.Api/
├── Features/
│ ├── Orders/
│ │ ├── PlaceOrder.cs request + validator + handler + endpoint in one file (or a folder)
│ │ ├── GetOrderById.cs
│ │ ├── CancelOrder.cs
│ │ └── OrderMappings.cs
│ ├── Products/
│ │ ├── SearchProducts.cs may use Dapper for a fast query
│ │ └── CreateProduct.cs may use EF Core
│ └── Customers/ ...
├── Domain/ shared entities (if any rich domain)
├── Infrastructure/ DbContext, external clients
├── Common/ behaviours, result types, exceptions
└── Program.cs maps all feature endpointscsharp
public static class PlaceOrder
{
public sealed record Command(int CustomerId, List<Line> Lines);
public sealed record Line(int ProductId, int Qty);
public sealed class Validator : AbstractValidator<Command>
{
public Validator() { RuleFor(c => c.Lines).NotEmpty(); RuleForEach(c => c.Lines).ChildRules(l => l.RuleFor(x => x.Qty).GreaterThan(0)); }
}
public static void MapEndpoint(IEndpointRouteBuilder app) =>
app.MapPost("/api/orders", async (Command cmd, ShopDbContext db, IValidator<Command> v, CancellationToken ct) =>
{
var result = await v.ValidateAsync(cmd, ct);
if (!result.IsValid) return Results.ValidationProblem(result.ToDictionary());
var order = Order.Create(cmd.CustomerId, cmd.Lines.Select(l => (l.ProductId, l.Qty)));
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
return Results.Created($"/api/orders/{order.Id}", new { order.Id });
});
}Pros: a change touches one folder; features can use different techniques (EF here, Dapper there); easy to delete a feature; fits CQRS. Cons: possible duplication between slices; needs discipline to extract real shared domain logic.