Comments 24
А статья точно не запоздала?
![](https://habrastorage.org/getpro/habr/upload_files/9e2/fc9/387/9e2fc93875c43c11f58b982fc9c33240.png)
И .NET 7 на мой взгляд расширил эту концепцию с помощью тех же Route groups (и общего развития этого типа приложений). Но в разговорах о применимости minimal APIs я всё ещё довольно часто встречаю подход "Minimal APIs для экспериментов / микропроектов, для всего крупнее используйте контроллеры".
Идея изначальной статьи, кажется, в том, что стоит посмотреть на Minimal APIs как полноценную замену контроллеров. И это ещё и облегчит реализацию domain-driven подхода. В общем случае это касается в разной степени всех MVC-фреймворков.
Я прочитал и вспомнил, что есть у нас микро(нано)сервис, в котором один контролер с одним методом и подумал "и правда, а зачем там контроллер?". Переписал.
Зачем контроллеры когда есть прекрасный FastEndpoints :)
На самом деле безконтроллерность же у нас была и раньше в разных проявлениях, а не только в "Minimal APIs"-like виде.
Уже, кажется, не поддерживаемый Nancy, который мне кажется довольно похожим на FastEndpoints.
Тот же Carter, на который есть ссылка в статье.
Довольно необычный ServiceStack, в котором
[Route]
вообще вешается на модели.
FastEndpoints после Minimal APIs выглядит самым привычным из всех. При этом он всё ещё смешивает маршрутизацию и обратку в одном месте, а в каком-то идеализированном варианте я вижу их разделенными. Условно, это app.MapGet<TRequest, TResponse>("myRoute")
, который умеет находить хендлер по переданным generic-параметрам. Но в таком случае мы опять возвращаемся к проблемам навигации, как с MediatR.
.NET 6 - LTS версия.
Здравствуйте, отличная статья. Подскажите, как лучше реализовать работу с разделяемыми между модулями моделями, классами с функционалом и расширениями. А так же со слоем данных. Если у нас entity framework используется.
Мне не кажется, что тут есть однозначный ответ.
Можно смотреть на то, что говорит DDD по этому поводу. Между несколькими bounded context могут быть "одинаковые" сущности, которые отражают разные данные и разное представление об одном объекте реального мира, но могут иметь одинаковые свойства. В доменах они будут представлены разными типами и даже храниться в разных таблицах на уровне БД. В этом случае ответ понятный — разным доменам могут соответствовать разные контексты.
При хранении в одной таблице на самом деле тоже можно разделять контексты (EF Core умеет маппить в двух контекстах одну таблицу с разным набором свойств). Другое дело, что при последующем возможном разделении сервисов, когда один из модулей будет переноситься в отдельный сервис, мы получим не микросервисы, а распределенный монолит, который ковыряется в одной базе, ещё и в одной таблице разными сервисами.
Наверное, самый подходящий ответ — зависит. Потому что вопрос тут в сценарии развития. В самой статье есть фраза "Настройка общей инфраструктуры (например, логирование, аутентификация, мидлвары, swagger, …) приложения также остается в Program.cs
, потому что она используется во всех модулях". В этом смысле DbContext может быть общим в рамках всего приложения (и это даже не будет противоречить DDD потому что контекст БД это не слой домена).
И, собственно, тоже самое можно сказать и про другие разделяемые между модулями зависимостями - если это не протекание доменного слоя, то вполне нормально шарить такие вещи.
Я честно говоря пришёл к почти такому же (ещё в эпоху .NET 5), только у меня один модуль - это одна Assembly/соотв. отдельный проект. И резолвит рефлексия у меня зависимости через зависимости между Assembly - вычитывает от каких Ассембли текущий модуль (или сам энтри-поинт) зависит, и в них уже ищет IModule-и (да, я даже назвал их точно так же). Соответственно IModule дальше рекурсивно ищет уже свои зависимости таким же способом (в дефолтной имплементации) и тоже их подвязывает. Только у меня дефолтная имплементация ещё и сами сервисы внутри модулей регистрирует через рефлексию через поиск классов с выставленными атрибутами, в которых прописывается время жизни сервисов.
Самая сложная часть - это не структура проекта, а разделение на модули.
Пройдёт совсем немного времени, прежде чем появится папка Common. А потом юзинги на типы из других модулей и мы получим всё тот же ком грязи.
Можно сказать "а вы следите за изоляций модулей, проектируйте домен и всё будет хорошо". Но на практике почему-то так не получается, т.к. требуется дисциплина и небольшие "срезания углов" будут постепенно накапливаться. Изоляция предполагает дублирование, которое сложно обосновать, когда всё рядом и решарпер сам подключает юзинги.
Кроме дисциплины, единственный известный мне путь - это физический запрет смешения кода модулей: тесты архитектуры, чтобы не выкладывалось в случае нарушения архитектуных правил, разделение модулей на сборки, чтобы физически нельзя было подключить лишнее или микросервисы.
Вообще, я за разделение на модули, но без фанатизма, не слишком мелко, т.к. изоляция тоже увеличивает сложность.
Эта же проблема всё равно была в случае контроллеров — доменный слой в разных bounded context не должен перемешиваться. Просто контроллеры не подсвечивали явно разные контексты и могли наоборот запутать. Роут /users/{id}/orders/
же просится подумать, что User тут тот же самый, что и в /users/{id}
, хотя в первом случае у нас статус покупателя, размер скидки и прочие атрибуты клиента, а во втором учетные данные и всякие статусы рассылки.
Но я согласен, что держать рамку DDD без дополнительных ограничений и/или проверок не всегда очень просто при развитии проекта.
private static IEnumerable<IModule> DiscoverModules()
{
return typeof(IModule).Assembly
.GetTypes()
.Where(p => p.IsClass && p.IsAssignableTo(typeof(IModule)))
.Select(Activator.CreateInstance)
.Cast<IModule>();
}
Может я что-то не понимаю, но данный код подключает модули только из сборки, где объявлен интерфейс IModule, доступный по контексту в данной процедуре. Не глупо ли так ограничивать область выбора подключаемых модулей?
Мне кажется, что это вопрос организации кода и реализации. А код из статьи это просто пример.
Можно держать модули в проектах сервисов или внешних проектах, а сервисы в одном репозитории/солюшене.
В зависимости от способа публикации (будут ли попадать "лишние" сборки в папку приложения) тут может быть разный подход, но при реализации нужно будет помнить, что есть шанс выстрелить себе в ногу.
А при локальном дебаге поведение может отличаться, если в этом случае все сборки складываются в одну папку.Если делить модули на проекты и сервисы на репозитории, то искать по всем сборкам логичнее и этот код авторегистрации нужно переписать.
Оно то верно. Ну мне показалось, что код текущая реализация совсем уж наивная - мне показалось автор хотел не показать какое-то красивое "авторское" решение для задачи модульности (а не просто описать Minimal APIs) - но это я так, к слову, скорее на заметку тем, кто будет читать и брать на вооружение не особо вникая в детали
Очень напоминает nest.js, подходом с модулями
Спасибо за хороший перевод!
Не очень новый подход на самом деле. См. например https://habr.com/ru/companies/jugru/articles/447308 раздел "Организация по модулям, а не слоям" (2019). И это наверняка не самый ранний источник. Только меняем ненавистный контроллер на минимал апи
Согласен, концепция встречалась раньше много раз. Тут как будто сложно найти самый ранний источник. Потому что не совсем понятно, что именно считать началом.
Про feature folders в asp.net mvc писали ещё в 2013ом.
Статья "Vertical slices in ASP.NET MVC" из 2016. И тоже без изоляции фич/модулей.
Сложная настройка поиска вьюх по папкам в статье Feature Slices for ASP.NET Core MVC из MSDN Magazine. Тоже 2016 год.
В "Организации по модулям, а не слоям" на мой взгляд не хватает изоляции и самодостаточности модулей, которая бы включала отделении регистрации доменнозависимых зависимостей внутри модуля.
Мне кажется, что идея как раз в том, чтобы применить идею модулей к Mininal APIs и минималистичному подходу к конфигурации приложения, который пришёл на смену Startup.cs
. Если раньше модули приходилось реализовывать, прикладывая дополнительные усилия, то в современном .net это делать легче, чем соблюдать "традиционный" подход.
Допустим у нас есть 2 микросервиса, один сервис дергает запросы другого, как используя такой подход вынести общие контракты в nuget, выделить отдельный проект Core для DTO`s?
Интересный подход. Вопрос. У контроллера я мог повесить атрибуты для авторизации, оформления методов в сваггере и т.п. Как это реализовать в модулях?
Я как-то очень по-разному могу понять этот вопрос.
Использовать готовые методы для авторизации или документации не должно быть проблемой.
Если вопрос к общим регистрациям, то исходная статья говорит об этом вот так: "Настройка общей инфраструктуры (например, логирование, аутентификация, мидлвары, swagger, …) приложения также остается в
Program.cs
, потому что она используется во всех модулях."
Но я при этом понимаю, что общая инфра как бы усложнит большие рефакторинги типа вынесения модулей в отдельные сервисы, это как-то необходимое зло, кажется.Если речь о кастомных атрибутах (методах, если мы говорим о Minimal APIs и смотреть на статью, то как раз об этом автор оригинала в конце говорит "Следуя этой архитектуре, вы по-прежнему можете выделить слой Core/Domain и слой Infrastructure в разные проекты. Будете вы это делать или нет? Зависит от размера проекта, и, как мне кажется, было бы неплохо поговорить об этом, чтобы быть одной волне. Лично у меня нет однозначного мнения на этот счет."
В моём представлении это всё — не доменный слой, а значит не обязательно думать об изоляции этого кода внутри модуля.
Пришло время пересмотреть структуру проектов на .NET