@Juhas async i await rozrasta się, gdy potrzebujesz operować na jego wynikach, przez co rozrasta się w górę, a nie w dół wywołań, gdzie możesz przekazywać same Taski. Niestety wiele razy widziałem używanie awaita jak w Twoim kodzie, a jest to błąd, bo taki await niczego nie daje - czekasz na wynik Taska, by go znowu zapakować w Taska i wysłać wyżej. Co innego jakbyś jakoś na tych danych operował.
o if i klamrach @abrakadaber już przytoczył historię i sam miałem podobną. Niby więcej kodu ale jak np taki Ci się trafi, to znalezienie takowego błędu nie jest takie proste. Inaczej zaczną pojawiać się cuda typu else w tej samej linijce, a od tego już niedaleka droga do średnika na końcu ifa.
@gswidwa1 podał fajny przykład z używaniem authorize i też w swoich projektach z tego korzystam, acz sam temat autentyfikacji i autoryzacji jest dosyć szerokim i sam dawno temu wolałem to uprościć sobie flagą dla admina.
Co do zwracania exceptiona, to nie raz widziałem np walidację na Exceptionach, co jest błędem i głównie z tego powodu o tym wspomniałem. Tworzenie obiektu Exceptiona, z całym stacktraceem jest czasochłonne i pamięciożerne i nadaje się tylko do faktycznie wyjątkowej sytuacji. Niestety kod w którym co chwila jest try i catch jest mało czytelny, więc na pewno nie powinno się robić try i catch w kontrolerze. Od tego jest globalny error handler, a try i catche to zaraz przy miejscu gdzie faktycznie przewidujesz, że coś się zdarzy i chcesz to obsłużyć (np przy pracy na plikach). Jeśli robi się pustego catch albo catch(Exception ex) to raczej używa się tego błędnie, catch powinien łapać konkretny wyjątek.
Tak jak wspomniałeś kod z wieloma komentarzami też nie jest dobry. Nazwy metod i zmiennych powinny z reguły wystarczać. Co mi po komentarzach, jak jedna metoda ma 1000+ linii kodu, z czego połowa to komentarze lub ify w ifie i zanim dojdziesz do środka to nie pamiętasz początku. Jednak literka S w SOLID jest bardzo ważna, a w brew pozorów bardzo dużo programistów zupełnie o tym zapomina, bo po co tworzyć osobną klasę dla kilku linijek kodu... a potem to się rozrasta do potworów.
Do zwracania wyniku spodziewanej ale nawet i błędnej akcji najlepiej się nadaje klasa z tym wynikiem. Jeśli dobrze zaprojektuje się klasę resulta i jego obsługę to wtedy prosto każdy przypadek zwrócić i wyświetlić użytkownikowi.