Контроллеры .NET должны быть легкими

Постоянно повторяющаяся избитая фраза с тремя тоннами багажа, который нужно распаковывать.

Почему они должны быть легкими? Какая от этого польза? Какие шаги я могу предпринять, чтобы их облегчить, если они еще не являются таковыми? Как мне сохранить их легкими?

Все это закономерные (и распространенные) вопросы, которые следуют за этим утверждением. Я обсуждал часть «почему» в некоторых предыдущих статьях, поэтому в этой статье мы собираемся взглянуть на это под другим углом.

Чтобы начать процесс определения шагов, необходимых для облегчения контроллеров, нам нужно понять некоторые из распространенных способов, посредством которых они могут стать толще.

На своем опыте я обнаружил 6 распространенных типов кода, которые просачиваются в наши контроллеры, и которым в конечном итоге лучше находится в других местах. Этот список не является исчерпывающим, я уверен, что их гораздо больше.

1. Маппинг объектов передачи данных (DTO)

Поскольку наши контроллеры находятся на образной передней линии конвейера запросов, часто возникает необходимость в создании объектов запроса и ответа, если требования запроса более сложные, чем простое использование URL аргументов и HTTP ответов с кодом состояния.

Каждый раз, когда у вас есть DTO, вам также необходимо преобразовать значения, которые они хранят, во что-то более функциональное, например, сущность домена.

Ну вы знаете, вот это вот все:

public IActionResult CheckOutBook([FromBody]BookRequest bookRequest)
{
    var book = new Book();

    book.Title = bookRequest.Title;
    book.Rating = bookRequest.Rating.ToString();
    book.AuthorID = bookRequest.AuthorID;

    //...
}

Эта логика маппинга достаточно невинна, но она быстро раздувает контроллер и добавляет дополнительную ответственность. В идеале единственная ответственность нашего контроллера - исключительно делегировать следующее действие на уровень HTTP-запроса.

2. Валидация

Очевидно, что плохие входные данные не могут проникнуть во внутренние стены замка нашего домена. От этого нас защищает валидация, обычно сначала на клиенте, а затем повторно на сервере.

Мне нравится относиться к контролерам как к шеф-поварам. У них есть помощники, которые готовят для них все ингредиенты, чтобы они могли творить свои чудеса с окончательной сервировкой блюд. Существует множество способов настроить валидаторы в конвейере запросов в ASP.NET MVC, чтобы контроллер мог предположить, что запрос валиден, и делегировать следующее действие.

Такой код непростителен!

public IActionResult Register([FromBody]AutomobileRegistrationRequest request)
{
    // //validating that a VIN number was provided...
    if (string.IsNullOrEmpty(request.VIN))
    {
        return BadRequest();
    }
    
    //...
}

Фуфло! (голосом Гордона Рамзи)

3. Бизнес-логика

Если у вас в контроллере есть что-то, связанное с бизнесом, вам, вероятнее всего, придется написать это снова в другом месте.

Иногда она даже наслаивается на валидацию. Если в вашей логике валидации есть правила, которые продиктованы бизнес-требованиями (а не простые вещи, такие как диапазоны чисел или то, чем может быть строка), вы рискуете продублировать этот код.

4. Авторизация

Авторизация похожа на валидацию в том смысле, что она является защитным барьером. Только вместо защиты от попадания в домен невалидных запросов, авторизация предотвращает попадание невалидных пользователей туда, куда им не разрешено.

Также, как и для валидации, ASP.NET предлагает множество способов отделения авторизации (например, промежуточное ПО и фильтры).

Если вы проверяете свойства своего пользователя (User) в рамках работы контроллера, чтобы предоставить/заблокировать что-то, вам, возможно, придется провести некоторый рефакторинг.

5. Обработка ошибок

Пожар, ПОЖАР!

public IActionResult GetBookById(int id)
{
    try
    {
      // важные вещи, которые делал бы шеф-повар...
    }
    catch (DoesNotExistException)
    {
      // что-то, что должен делать помощник ...
    }
    catch (Exception e)
    {
      // пожалуйста, не надо...
    }
}

Это довольно общий вариант, и иногда обработка исключений должна выполняться в контроллере, но я обнаружил на практике, что почти всегда есть лучшее, более локализованное место. А если нет, вы можете воспользоваться преимуществами глобального промежуточного программного обеспечения для обработки исключений, чтобы отловить наиболее распространенные ошибки и предоставить вашим клиентам что-то более стабильное.

Возвращаясь к метафоре шеф-повара, я хотел бы, что мой шеф-повар не беспокоился о подобных вещах. Позвольте им выполнять свою единственную ответственность и предполагать, что кто-то другой справится с неожиданностями.

6. Хранение/извлечение данных

Работа, подобная получению или сохранению сущностей с использованием репозитория (Repository), часто оказывается в контроллере, чисто чтобы сэкономить время. Если контроллеры - это просто конечные точки CRUD, то почему бы и нет.

У меня даже есть более старая статья, в которой показаны такие контроллеры.

Вместо того, чтобы просто заклеймить это плохим поведением, я думаю, что указание на альтернативный способ подчеркнет, почему это может излишне раздувать ваш контроллер.

Во-первых, если взглянуть на это с точки зрения дизайна (с акцентом на принцип единой ответственности), если у вас есть объекты, которые предназначены для долговременного хранения, используемые вашими контроллерами, тогда у ваших контроллеров будет более одной причины для изменения.

public IActionResult CheckOutBook(BookRequest request)
{
    var book = _bookRepository.GetBookByTitleAndAuthor(request.Title, request.Author);
    // если у вас уже есть вышеуказанный оператор, то у вас будет
    // соблазн добавить сюда бизнес-логику оформления заказа...
    //...
    return Ok(book);
}

Помимо базового CRUD, логика персистентности в контроллере — это зачатки кода для дополнительных отростков логики, которые лучше подходят для более глубоких этапов цепочки.

Здесь мне больше по душе использовать либо какую-то службу приложения (спрятанную за интерфейсом) для обработки этой работы, либо делегировать какому-либо CQRS-объект команд/запросов.

Вот и все!

Вам на ум приходят еще какие-нибудь типы?

Если вы хотите углубиться в эту тему, я собрал набор рецептов с примерами кода и упражнение специально для того, чтобы сделать ваши контроллеры легче. Это не только то, как, но и глубокое исследование того, почему.

Вы можете получить один из рецептов ниже (вместе с кодом скидки на весь набор).


Перевод подготовлен в рамках курса "C# ASP.NET Core разработчик".

Всех желающих приглашаем на двухдневный онлайн-интенсив «Serverless на базе azure». День 1: обзор облачных сервисов, что такое serverless computing, serverless computing на базе azure сервисов, создания azure function. День 2: выбор базы, добавление azure storage, добавление безопасности, ARM шаблоны. Присоединяйтесь к участию!