Комментарии 16
Конфигурация эндпоинтов происходит через методы, явно вызывающиеся в вашем коде, а не поиске контроллеров по всему проекту и получению аттрибутов
Как раз таки контроллеры искать и не надо. И атрибуты сразу все видны.
Я работал с большими проектами и с обоими подходами. MinimalAPI это больше кода и и больше файлов с конфигурацией эндпоинтов. Мне не понравилось.
"Поиске" - я имею ввиду как у контроллеров, автоматического. Для регистрации эндпоинтов из моей статьи это выглядело бы приблизительно так:
var endpoints = Assembly.GetExecutingAssembly().DefinedTypes
.Where(type => type is { IsClass: true, IsAbstract: false } && typeof(IEndpoint).IsAssignableFrom(type));
Заглянул под капот контроллеров, там примерно тот же принцип в чуть более красивой обертке.
И, ничто не мешает так сделать для эндпоинтов. И разные сервисы для DI подхватывать например, или что еще вам нужно. Красиво - просто написал условный Handler для MediatR, а он автоматом уже зарегистрирован, никаких "опять забыл в DI прокинуть".
Вот только, злоупотребление такой черной магией несет за собой негативные последствия: меньшую гибкость, усложнение восприятия для новых разработчиков (в т.ч. разрыв цепочки usage, не проследишь что откуда вызывается). И старых, когда все нюансы работы проекта начнут забываться, и рефлексию, от которой стараемся избавиться
Стоит ли того 1 сэкономленная строчка кода на эндпоинт/сервис? Microsoft вот раньше, с контроллерами, считали, что можно сделать исключение. Сейчас, не безосновательно, они так не делают
Вот только, злоупотребление такой черной магией несет за собой негативные последствия
Можно подумать, что у вас тут, в Minimal API, не магия (ну, или та самая достаточно развитая технология, которая от магии неотличима). Или она - белая, а потому негативных последствий не несет?
В MVC используется маршрутизация основанная на соглашениях - хорошо документированных и известных. Многие этим пользуются, давно, и им все нормально, без негативных последствий.
Вот и вы, если вы нормально продумаете и документируете для своего проекта соглашения об именовании классов с точками назначения ("эндпонтами"), то негативных последствий будет не больше, чем от схемы маршрутизации в MVC.
А такие соглашения создают немалые удобства: не надо каждую точку назначения регистрировать отдельно - а в большом проекте их ох как много быть может...
что у вас тут, в Minimal API, не магия
Не магия. В статье простая реализация REPR паттерна с явной регистрацией (которую, имхо, можно сделать изящнее). Этот паттерн можно сделать и контроллерами (правда это не будет полный эквивалент, ведь там используются атрибуты)
немалые удобства
И немалые неудобства, ведь точка в вакууме неудобна. Ну для навигации по проекту они структурно лежат по папкам. Но что делать, если мы хотим навесить что-то дополнительно на пару точек из группы? Ну можно в каждом закопипастить, а потом просто забыть про это. Или замудрить какой-то костыль, который объединит эти пару точек, но это уже и будет магия.
А ведь можно сделать просто и красиво сведя регистрацию в единое место. Если очень хочется, то можно и этот класс разбить (и разложить по тем же папкам) - все равно это будет явная схема, где видно "кто куда и с чем" и возможностью реконфигурации не трогая сами точки.
Тут с автором еще можно поспорить с его "делаем в HandleAsync всю работу", но это уже оффтоп.
Тут с автором еще можно поспорить с его "делаем в HandleAsync всю работу", но это уже оффтоп.
Ну это, как я написал, с примером из моей статьи про VSA. Тут просто для наглядности, каким-нибудь MediatR, валидаторами или репозиториями, как раньше, никто пользоваться не запрещает.
Как можно посмотреть в eShop, похожий подход теперь самими Microsoft одобрен =)
Не магия. В статье простая реализация REPR паттерна с явной регистрацией (которую, имхо, можно сделать изящнее).
Я не про статью, а про сам Minimal API. Сама по себе, без этой магии, подсистема маршрутизации в качестве точки назначения в MapXXX может использовать только RequestDelegate: delegate Task RequestDelegate(HttpContext context)
, а для MinimalApi можно передать любой делегат, так что прелбразование его в RequestDelegate - самая что ни на есть магия.
Но что делать, если мы хотим навесить что-то дополнительно на пару точек из группы?
Использовать атрибуты. Раз уж всё равно для поиска методов точек назначения используется отражение, ничто,в принципе, не мешает читать атрибуты этих методов, а потом в методе, реально создающем точки назначения для подсистемы маршрутизации - их читать и обрабатывать: хотя бы, в метаданные этих точек назначения запихивать.
А в едином месте для регистрации в проекте любит образовываться куча всякой всячины. И в реально большом проекте это будет реально большая куча. Не, для настоящих программистов (тех, кто не использует Паскаль(см. одноименную статью от 1983 года), и кого не напрягает написать, а потом - читать и перечитывать - цикл из трех сотен строк кода) эта куча - не преграда. Но ведь менеджеры так и норовят вместо настоящих программистов нанять неженок, которым такие подвиги не по силам, чисто потому, что им платить можно меньше. ;-)
Ну, никто не запрещает так делать (сами Microsoft по сути раньше так и делали), так что вопрос тут скорее о мнениях. (Если, конечно, не нужна гибкость, которую дает ручная регистрация: навешивать метадату на группы эндпоинтов, разбивать эндпоинты по проектам, подключать только часть из них, и т.п. - но это штуки ситуативные и не то чтобы совсем с автоматической регистрацией не совместимы.)
С моей точки зрения, в контроллерах это - задокументированная фича фреймворка, о которой разработчик узнает на этапе "пишем hello world", поэтому там такой подход логичен. В эндпоинтах же - это перемещается на уровень соглашений в проекте.
нормально продумаете и документируете для своего проекта
Уже проблема: к сожалению, нормально задокументированный проект - это скорее редкость, чем норма. И, даже если хорошая документация есть, это дополнительное время на прочтение, усваивание инфы.
И если у нас в проекте одно такое исключение, для эндпоинтов - ничего страшного, правда. Но, идут годы, проект растет, таких исключений уже куча. Вы так не планировали? Ну так другой разработчик подумал, раз есть одно, то почему бы и не два. А вы были в отпуске, или PR прошел мимо вас. А теперь это задача на рефакторинг, на которую менеджер не дает времени, потому что "что нам это даст?", и т.п.
И вот уже незаметненько, новые разработчики сидят и по несколько месяцев онбордятся, пытаясь разобраться что тут происходит. Еще и без возможности пройтись по всей цепочке Usages и отследить, потому что цепочки разорваны рефлексией.
А ведь есть неплохое правило "хороший код должен сам себя документировать". Конечно, его не везде получится соблюсти, но зачем просто так его нарушать.
Спасибо за обзор! Приходилось раньше несколько раз делать легковесные веб-серверы как раз с использованием NativeAOT и Reflection-free mode (ныне <IlcDisableReflection>
вырезан начиная с .NET 9), используя только Kestrel. Для этого просто ссылаемся на ASP.NET в нашем .NET проекте примерно таким образом:
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App"/>
</ItemGroup>
</Project>
И пишем свою собственную логику используя неймспейс Microsoft.AspNetCore.Server.Kestrel
- там все достаточно просто.
Похоже, что когда в следующий раз придется это делать, можно будет остановиться на Minimal API, что несомненно удобнее, ведь в нем из коробки уже реализован роутинг и аутентификация. Один момент в вашей статье хотел уточнить - если уж мы используем такой подход, наверное следует отказаться от Reflection целиком, в т.ч. от сериализации моделей средствами ASP.NET (если они еще не перешли на source generators) и получение MethodInfo в вашем примере с OpenAPI. Наконец, вместо ORM и Queriable использовать ADO - DbConnection, DbCommand. Не так уж много кода потребует это все, тем более что достаточно выработать паттерн один раз и переиспользовать его в дальнейшей разработке. Поэтому подойдет, наверное, даже для средне-крупных проектов.
К слову, на этапе компоновки бинаря (таргет LinkNative) весь лишний код вырезался и получался бинарь размером не более 5 Мб. Minimal API через <Project Sdk="..."/>
давал минимум 12 Мб. Надо проверить, как сейчас с этим дело обстоит у Minimal API.
Избавление от рефлексии целиком и переход на NativeAOT в большом проекте - это тема для отдельной статьи, и переход на Minimal API тут был бы лишь один из шагов. У меня тут меньше опыта, поэтому сразу и то и то я делать не пытался.
Да и, многим сейчас NuGet-зависимости не позволят, которые еще не стать совместимыми.
Но, пара моментов:
Json source generators опционально есть, вот пример для Minimal API. В идеале конечно, да, надо бы их использовать. MethodInfo я там, если посмотрите, парой абзацев ниже как раз показал на что заменить.
EF тоже может работать c Native AOT. Правда, с заметными ограничениями, и в целом, выстрелить себе в колено стало еще проще. Зато с такими ограничениями разница в производительности "в среднем" между EF и raw sql, которая и так стремительно уменьшается последние годы, еще меньше. В общем, вкусовщина
Minimal API, имхо, отлично подходит для микросервисов с совсем небольшии количеством эндпоинтов.
Но вот для развеститого api будет скорее больший проигрыш в поддержке и прозрачности.
В Minimal API есть важная концепция - метадата.
Эта важная концепция - она не из Minimal API, а из базовой для .NET подсистемы маршрутизации (endpoint routing). MVC тоже использует этот же механизм маршрутизации, а потому и метаданные тоже использует. И если написать маршрут к контроллеру API на MVC через MapControllerRoute, а не через атрибуты (это вполне возможно, если не использвать [ApiController], правда при этом надо и другие функции, реализуемые [ApiController] самому реализовать), то на полученный от него IEndpointConventionBuilder можно навесить любые метаданные. Более того, например, навесить именованную политику ограничения запросов (Rate Limiting) можно как и атрибутом [EnableRateLimiting] для контроллера/действия, так и методом расширения RequireRateLimiting для IEndpointConventionBuilder - в обоих случаях в метаданных будет один и тот же класс EnableRateLimitingAttribute с именем политики, который Rate Limiting Middleware увидит и применит.
PS Если кому надо узнать, как работает Minimal API - есть хороший цикл статей от Andrew Lock, в котором объясняется вся эта белая магия. упомянутая в данной статье. Но букв там много, да.
Я, к сожалению, так и не уловил профита. Сперва рассказывается о тяжеловесности контроллеров - фильтрах, атрибутах и т.п. А потом берётся minimal API и... И из него делаются те же самые контроллеры, только всю обвязку надо повторно написать руками вместо готовой предоставленной Майкрософтом. А профит-то где? Пара процентов перформанса? Я лучше буду иметь меньше самописного кода и большую структурированность, это в костах больше сэкономит.
Обвязка для регистрации? Ну, это десяток строк кода, плюс по 1 строчке на эндпоинт/группу эндпоинтов.
А если остальное так у нас как были, так и есть много коробочных фильтров/middleware/поддержка OpenAPI от Microsoft. Просто, в большом проекте (а я в статье делаю акцент на такой), всегда появятся свои приколы, и тут уж, не важно какой язык, фреймворк или библиотека - за вас это никто не напишет.
Как пример, в крупном контроллере может быть одна зависимость, которая сама тянет конфиги через сеть. Сломается одна зависимость и сломаются все endpoint, которые находятся в контроллере.
Или, нужно вывести enum на фронт, а для этого вы тянете 10 дополнительных зависимостей при каждом запросе.
Каждый решает сам, стоит оно того или нет, но мне подход с контроллерами напоминает подход с тяжеловесными сервисами по 20 методов, которые врятли когда-либо будут отрефакторены и навсегда останутся подвержены росту. И поэтому я использую Minimal Api на крупном проекте и доволен удобством
Ну, справедливости ради
Сломается одна зависимость и сломаются все endpoint, которые находятся в контроллере.
Обычно зависимости идут не в контроллеры, а хэндлеры/сервисы (впрочем, в случае сервисов, проблема остается но просто в другом слое).
Но, если хочется в контроллер - DI там тоже работает не только в конструкторе контроллера, но и в его методах (эндпоинтах), и можно прокинуть сервис туда.
Я, поэтому, не упомянул в плюсах Minimal API то что мы тащим только нужные для конкретного эндпоинта сервисы: в контроллерах так тоже можно.
Minimal API: Избавляемся от устаревающих контроллеров в ASP.NET Core