Контроллеры: структура и подключение
Контроллеры: классический способ организовать эндпоинты в крупных приложениях.
Суть: контроллер — это класс, объединяющий связанные эндпоинты (action-методы). Когда сервис вырастает за пределы пары маршрутов, контроллеры дают структуру: группировку, общие настройки, наследование.
Minimal API хорош для маленьких сервисов, но в большом приложении с десятками эндпоинтов всё в Program.cs не уместишь. Контроллеры решают это: один класс — одна область (пользователи, заказы, товары), внутри — методы под каждое действие.
Анатомия контроллера
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[HttpGet]
public IActionResult GetAll() => Ok(new[] { "Аня", "Борис" });
[HttpGet("{id}")]
public IActionResult GetById(int id) => Ok(new { Id = id });
[HttpPost]
public IActionResult Create(UserDto dto) => Created("/api/users/1", dto);
}
Разберём по частям. [ApiController] включает удобства для API: автоматическую валидацию модели и понятные ошибки. [Route("api/[controller]")] задаёт базовый путь, где [controller] подставляет имя класса без суффикса — здесь users. Наследование от ControllerBase даёт хелперы Ok(), NotFound(), Created().
Подключение контроллеров
builder.Services.AddControllers();
// ...
app.MapControllers();
Как работает под капотом
При старте ASP.NET Core сканирует сборку, находит все классы с [ApiController] и строит таблицу маршрутов: какой URL плюс HTTP-метод ведёт к какому action. Когда приходит запрос, маршрутизатор находит подходящий action, создаёт экземпляр контроллера (через DI-контейнер!), биндит параметры и вызывает метод. После выполнения результат (IActionResult) превращается в HTTP-ответ. Важно: контроллер создаётся заново на каждый запрос — поэтому в нём нельзя хранить состояние между запросами.
Частые ошибки
- Забыть
app.MapControllers(). Тогда контроллеры есть, но маршруты не зарегистрированы — все запросы дают 404. - Хранить поля-состояние в контроллере. Новый экземпляр на каждый запрос — данные не переживут запрос.
- Наследоваться от
ControllerвместоControllerBaseв API.Controllerтянет поддержку View (для MVC с HTML), для чистого API это лишнее.
Best practices
- Один контроллер — одна предметная область. Не сваливайте всё в один God-контроллер.
- Тонкие контроллеры: бизнес-логику выносите в сервисы, контроллер только принимает запрос и возвращает ответ.
- Всегда ставьте
[ApiController]для API — он автоматически возвращает 400 при невалидной модели.
Тонкие контроллеры и слои приложения
Хороший контроллер — тонкий: он принимает запрос, передаёт данные в сервис, превращает результат в HTTP-ответ. Вся бизнес-логика (правила, расчёты, работа с БД) живёт в отдельном слое сервисов. Зачем так? Логику в сервисе легко переиспользовать (из контроллера, из фонового задания, из другого сервиса) и удобно тестировать без поднятия HTTP. А контроллер остаётся простой «прихожей», за которой видно структуру запроса и кодов ответа.
Типичная многослойность выглядит так: контроллер (HTTP) → сервис (бизнес-логика) → репозиторий/DbContext (данные). Каждый слой зависит только от нижнего через интерфейсы. Контроллер не знает, как именно сервис достаёт данные, а сервис не знает про HTTP. Такое разделение ответственности делает приложение понятным и устойчивым к изменениям.
Фильтры: сквозная логика контроллеров
У контроллеров есть мощный механизм — фильтры (action filters, exception filters, authorization filters). Они выполняются вокруг action-методов и позволяют вынести сквозную логику: логирование времени выполнения, валидацию, обработку ошибок, кэширование. Например, фильтр может замерить, сколько работал каждый метод, не засоряя сами методы этим кодом. Фильтры — это «middleware уровня MVC», заточенный под контекст контроллеров и action.
Поскольку контроллер создаётся через DI на каждый запрос, в его конструктор можно внедрять любые зарегистрированные сервисы — IUserService, ILogger, AppDbContext. Именно так контроллер получает доступ к данным и логике, оставаясь при этом без собственного состояния. Это прямое следствие правила «новый экземпляр на каждый запрос»: состояние держать негде и не нужно, всё приходит через зависимости.
Итог: контроллеры структурируют крупные приложения, создаются на каждый запрос и работают через атрибуты маршрутизации. Дальше разберём, как именно URL находит нужный метод.