Простой вопрос: делая задачу, касающуюся API - вы чаще работаете с одним эндпоинтом, или пишете, условные, репозитории, которые используются сразу в нескольких эндпоинтах? Скорее всего, первое, тогда почему мы разбиваем проект по слоям, а не по фичам (эндпоинтам)?
Это видно в часто используемых нынче архитектурных подходах: Layered, Clean Architecture, Onion, и так далее. Не буду выделять что-то конкретное и объясню общую разницу в подходах:
Vertical Slice Architecture (VSA) строится вокруг каждого отдельного feature-слайса (эндпоинта, как самый простой пример), а не вокруг слоев.
То есть, если код относится к конкретному эндпоинту, мы не размазываем его по всему проекту в папках Commands/Services/Repositories/DTOs и т.п., а кладем в одно место, там где и будет находиться эндпоинт. На картинке это выглядит так:

Главный принцип - уменьшаем связанность между слайсами (фичами), увеличиваем связанность внутри слайса
Зачем?
Вопрос, ответ на который я вижу довольно редко в статьях об архитектурах. Не будем плодить карго-культ "потому что так все делают" и ответим:
Главное преимущество VSA - продуктивность и минимум оверхэда, выраженного в бесконечной навигации по коду: все в одном месте, без беготни по проекту, лишних абстракций (а они вам вот все действительно нужны?), тысяч файлов, с минимальным пересечением логики.
Принцип KISS хорошо выполняется и позволяет быстро разобраться - не обязательно понимать все тонкости архитектуры проекта, прежде чем начинать писать код.
От огромных сервисов то мы ушли, только вот теперь скроллим не тысячи строк когда, а сотни папок.
При этом, основные преимущества более привычных подходов сохраняются: модульность (которая, на мой взгляд, в VSA выражена даже лучше), отсутствие огромных сервисов, легкость тестирования и т.п.
Данный подход довольно новый (опустим про хорошо забытое старое - я уверен многие сами его давно используют неосознанно), даже на хабре сложно найти авторскую статью. В 2018 году, например, о нем написал Jimmy Bogard, автор всем нам знакомого MediatR и AutoMapper, а теперь уже и тренинги по VSA проводит.
Как знать, может через несколько лет и очередной eShop попробует данную архитектуру.
Пример организации проекта
Поскольку архитектура довольно новая, устоявшихся названий и понятий еще нет, поэтому код ниже не стоит воспринимать как единственно-верный вариант
Также буду использовать Minimal API (но никто не мешает пользоваться контроллерами)
Немного о Minimal API
Кто еще не успел познакомиться с этим нововведением dotnet - не помешает. Это не просто желание "мы тоже хотим писать на c# API в 4 строчки кода", а действительно хороший инструмент, который призван заменить устаревшие контроллеры. На хабре мало инфы, если данная статья зайдет, напишу о том, как использовать Minimal API в реальном проекте с кучей инфраструктуры, а не просто Hello World
Структура будет выглядеть следующим образом:
Program.cs
Endpoints (или, как часто называют, Features)
| User
| GetUserInfo.cs
| UserInfoResponse.cs
GetUserInfo.cs:
public class GetUserInfo : IEndpoint
{
public void Register(IEndpointRouteBuilder endpointsBuilder)
{
// Регистрация эндпоинта. В случае контроллера это был бы стандартный метод с атрибутом [HttpGet("user/info")].
endpointsBuilder.MapGet("/user/info", HandleAsync)
.RequireAuthorization()
.WithDescription("Get user basic info")
.WithTags("User");
}
// Параметры этого метода почти аналогичны параметрам метода контроллера, DI зарезолвит все необходимые зависимости.
public static Task<UserInfoResponse> HandleAsync(ExampleDbContext db, UserContext userContext, CancellationToken ct)
{
return db.Users.Where(x => x.Id == userContext.UserId)
.Select(x => new UserInfoResponse
{
UserId = x.Id,
Username = x.Username,
})
.SingleAsync(ct);
}
}
Где в HandleAsync() - вся необходимая логика запроса. Этот метод сделан публичным для возможности тестирования, об этом чуть ниже.
Такая наглядность позволит подпускать к проекту не только сеньоров с солидным архитектурным бэкграундом, но и разработчиков с опытом поменьше. Да и для вас самих GitHub Copilot, или чем вы пользуетесь, сможет банально лучше анализировать весь нужный код т.к. будет иметь более полный контекст.
IEndpoint - просто интерфейс для удобства регистрации эндпоинта Minimal API с единственным методом Register(): в отличии от контроллеров, магической рефлексии из коробки нет. Подробнее, как я говорил, стоит разобрать в отдельной статье.
Можно заметить, что в описанной мной структуре UserInfoResponse.cs находится внутри GetUserInfo.cs: если что, да, так можно делать, просто перетащив файл в другой в IDE. В .csproj для этого появится соответствующая запись с DependentUpon. Парой абзацев ниже скриншот для примера.
Но если не нравится - можно создать папку GetUserInfo и сложить в нее все файлы, относящиеся к данному эндпоинту (и сам эндпоинт)
MediatR
Кто не увидел MediatR - не беспокойтесь - ничто не мешает использовать его с паттерном CQRS в данной схеме. Все необходимые файлы просто лягут под EndpointName.cs, а не размажутся тонким слоем по проекту.

Ниже я оставлю ссылки на репозитории, делают именно так. Или же, как можно заметить в этих репозиториях, кто-то вообще создает эти классы прямо внутри файла EndpointName.cs. А почему бы и нет? Несколько классов в одном файле - это, конечно, вполне себе антиппатерн. Но как и всегда в таких случаях, это не значит что это автоматически "плохо". Это плохо только если не понимаешь, почему обычно так не делают.
Однако, я бы задумался: действительно ли нужен MediatR с таким подходом, особенно учитывая что он становится платным? Проблему сервисов на тысячи строк мы решаем и без него. Это просто еще один аргумент против его бездумного использования.
Domain-Driven Design и другие архитектуры
Я хоть и намеренно противопоставил VSA другим подходам, на практике же они совместимы во многих аспектах: хоть немного сложный проект быстро выйдет за пределы набора API эндпоинтов. В конце статьи я привел примеры, которые вполне сочетают в себе эти подходы.
Вообще, принципы DDD хорошо работают с VSA: у нас все еще остается большое количество кода, прежде всего инфраструктурного, который хорошо ложится на эту модель: разумеется, не надо тащить в каждый эндпоинт общую для проекта обработку ошибок/транзакции/валидацию и т.п.: это все так и продолжит жить в своей области.
Тестирование
Тут все просто - вызываем наш HandleAsync, передаем нужные параметры и моки сервисов.
Может, не так красиво, как с использованием DI и MediatR, но зато при изменении эндпоинта больше вероятность отловить ошибку в тесте на этапе компиляции.
Если хочется красиво и с вызовами API - можно так.
Использование за пределами API
Конечно, большинство проектов не состоит из одних только эндпоинтов: у нас есть очереди, job'ы и т.п. Но ничто не мешает использовать там такой же подход. Более того: я думаю многие заметят, что они сами, осознанно или нет, уже так делают. А если нет - время задуматься.
Ну и в целом, такой подход легко выходит не только за пределы не только бэкенда (Feature-Sliced Design - пример из мира фронтэнда), но и кода вообще: мы же, например, не разбиваем команды в проекте по принципу "эта команда пишет контроллеры, а эта - хэндлеры".
Микросервисы и монолиты
Вполне логично, что VSA может работать и так и так. И такая архитектура проще разбивается на микросервисы: ведь мы выделяем их не по слоям "это у нас микросервис Domain", а по фичам. Может, с таким подходом "у нас монолит, но мы переходим на микросервисы" в каждой второй компании было бы чуть меньше.
Границы тоже есть
На заглавной картинке видно, как слайс проходит прямо по базе данных. Кто-то может сделать вывод, что Entity/модели, представляющие таблицы в БД, тоже нужно ограничивать областью слайса.
Если у вас на проекте подход Code-first (или просто полная репрезентация БД в коде), не соглашусь: это все еще должен быть отдельный слой. Если же вы пишете DTO для данных из базы под конкретный эндпоинт - этот DTO прекрасно ляжет в слайс.
Аналогично, если используете паттерн репозитория, тут, конечно, зависит от вашей реализации: если метод репозитория используется в одном-единственном эндпоинте/группе эндпоинтов, и его можно выделить не в ущерб тестированию, то, вероятно, так и стоит сделать: общий принцип - не держать код конкретной фичи вне ее пределов.
Похожие подходы
На хабре уже была статья-перевод, вдохновленная VSA, только с группированием больше по целым модулям, а не конкретным эндпоинтам. Для наглядности, приведу пример структуры оттуда:
WebApplication
│ appsettings.json
│ Program.cs
│ WebApplication.csproj
│
├───Modules
│ └───Orders
│ │ OrdersModule.cs
│ ├───Models
│ │ Order.cs
│ └───Endpoints
│ GetOrders.cs
│ PostOrder.cs
Слайс тут - не отдельный эндпоинт, а группа. Принципиально подход не меняется. Такой подход предлагал небезызвестный Роберт Мартин и назвал это Screaming Architecture. Главная идея тут - что архитектура "кричит": по структуре проекта мы можем понять не просто, какой архитектурный подход использовался, а и то, какой функционал выполняет данный проект.
На практике, как и везде, конечно, не все гладко
Что если у двух эндпоинтов какой-то общий между собой код? Использовать сервисы/хэлперы никто не запрещает. Поместить их можно рядом с эндпоинтами: зачем выносить уникальный для них код куда-то за их пределы. Ну или, вот статья на эту тему для вдохновения.
Или, если эндпоинты почти идентичны (например, из-за версионирования), может совсем объединить их в один файл.
Однако, не забываем, что принцип DRY, конечно, важен, но 3 строчки одинакового кода - еще не повод пересмотреть архитектуру проекта - все равно она не будет идеальной. Даже если кажется что будет.
В целом, если в проекте большое количество внутренних связей, хитрых транзакций и т.п., которые тяжело разбить по фичам, такая архитектура может выглядеть уже не так красиво. Впрочем, как и любая другая архитектура в неподходящем для нее контексте.
Главное, что хочется отметить: при проектировании нужно думать не о том, как написали вон в той авторитетной статье/книге, а головой.
Напоследок, поскольку я привел только весьма упрощенный пример, а не инструкцию к действию, несколько проектов VSA на GitHub:
ContosoUniversity от Jimmy Bogard. Давненько не обновлялся, но как реф подойдет.
Еще пример VSA со своей статьей
Food Delivery Microservices - work in progress, но общие концепции можно почерпнуть.