Следует ли всегда объявлять действия контроллера веб-API, которые не выполняют асинхронную работу, как асинхронные?

Следует ли всегда объявлять действия контроллера веб-API, которые не выполняют асинхронную работу, как асинхронные?
Следует ли всегда объявлять действия контроллера веб-API, которые не выполняют асинхронную работу, как асинхронные? - farrelnobel @ Unsplash

Я просмотрел веб-API нашего приложения и убедился, что вся асинхронная работа полностью асинхронна — и никакая искусственная асинхронность не применяется.

Скажем, у меня есть следующий контроллер веб-API:

    [HttpGet]
    public async Task<IActionResult> Foo()
    {
        //no async work
        return OK(...);
    }

Я планировал удалить ключевое слово async из действий, подобных приведенному выше, — тех, которые не выполняют никакой асинхронной работы, — изменив его на

    [HttpGet]
    public IActionResult Foo()
    {
        //no async work
        return OK(...);
    }

Затем один из моих товарищей по команде утверждает, что действия контроллера всегда должны быть асинхронными. Я не смог найти явных доказательств в Интернете для этого аргумента, но даже если это правда, у него есть недостаток, заключающийся в том, что он загрязняет мое решение, добавляя предупреждения CS1998 (в этом асинхронном методе отсутствуют операторы «ожидания», и он будет работать синхронно. Рассмотрите возможность использования ' await» для ожидания неблокирующих вызовов API или «ожидание Task.Run(...)» для выполнения работы с привязкой к процессору в фоновом потоке) повсюду (можно подавить их, но я нахожу это противоречивым, что на с одной стороны, было бы лучше, если бы они были асинхронными, а с другой стороны, компилятор поднимет флаг, указывающий на то, что на них нужно обратить внимание).

Действительно ли рекомендуется использовать неасинхронные действия контроллера - async ?

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


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

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

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

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

Возвращаясь к конечным точкам API, аргумент немного слабее, поскольку инфраструктура обрабатывает конечные точки асинхронно/синхронно за вас. Нет реального потребителя, о котором вам нужно заботиться, поскольку потребителем является .NET, и он может адаптироваться к любому из них.
Возможно, вы считаете это причиной игнорировать приведенные выше рассуждения. Я мог видеть, как ты туда попал. Но я сторонник последовательности в кодовой базе, и поскольку применимы те же рассуждения, пусть даже только в принципе, я предпочитаю сохранять согласованность, а не вводить исключительные случаи синхронизации здесь и там.

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


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

Ключевое слово async не влияет на контракт (вы можете свободно изменять его, не заставляя потребителя вносить какие-либо изменения), но такие вещи, как добавление/удаление возвращаемого типа Task<T>, передача CancellationToken, ... действительно влияют на контракт, и это те вещи, которые я бы добавил, даже если вы знаете, что тело метода в настоящее время синхронно.

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

[HttpGet]
public Task<IActionResult> Foo()
{
    IActionResult result = OK(...);
    return Task.FromResult(result); 
}

Нет ключевого слова async, так как await не происходит. Однако я сохранил возвращаемый тип Task<T>, поскольку он является частью контракта.


Кроме того, я думаю, что частично это связано с тем, что асинхронность появилась на сцене намного позже, чем синхронное выполнение.

Из-за этого использование асинхронной подписи похоже на выполнение чего-то дополнительного поверх (синхронного) базового уровня, что, в свою очередь, вызывает у разработчиков потребность быть эффективными и заставляет их думать: «Зачем мне делать эту дополнительную вещь, когда мне это не нужно? это прямо сейчас? ЯГНИ!".

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

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


LetsCodeIt, 19 января 2023 г., 11:37