Контроллеры: структура и подключение

Контроллеры: классический способ организовать эндпоинты в крупных приложениях.

Суть: контроллер — это класс, объединяющий связанные эндпоинты (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 находит нужный метод.

Проверьте себя
1. Зачем нужен атрибут [ApiController]?
AЧтобы класс компилировался
BВключает удобства для API: автоматическую валидацию модели и стандартные ошибки
CЧтобы вернуть HTML
DЭто обязательно для любого класса
2. Сколько раз создаётся экземпляр контроллера?
AОдин раз на всё приложение
BЗаново на каждый HTTP-запрос
CОдин раз в час
DНикогда, методы статические