В данной статье будет рассмотрен подход автоматически сгенерированных контроллеров в одной из самых популярных технологий для написания веб-сайтов — ASP.NET Core MVC.
Ключевые слова: разработка ПО, C#, ASP.NET, MVC, контроллеры, метапрограммирование.
Информационные технологии имеют чрезвычайно тесную взаимосвязь с наукой. Десятилетия кропотливых исследований, идущих бок о бок с научными методами, являются результатом изобретения цифровых инструментов, которые предыдущие поколения назвали бы магией.
И в наши дни, в годы расцвета ИТ, можно встретить подходы, инструменты и концепты делающие такие вещи, которые лет десять или пятнадцать назад казались невозможными. Такую вещь можно встретить в мире объектно-ориентированного программирования под названием метапрограммирование.
Метапрограммирование — это процесс написания программ, которые могут создавать другие программы. Это один из самых недоиспользуемых и плохо изученных методов программирования. Положительная черта метапрограммирования заключается в том, что оно позволяет программистам минимизировать количество строк кода для выражения решения в декларативном стиле или дает программам большую гибкость для эффективной обработки новых ситуаций без перекомпиляции.
Несмотря на то, что метапрограммирование широко распространено в некоторых языках программирования, оно не получает должного внимания. Это, потенциально, очень мощный, и малоизученный концепт, который может быть использован как инструмент позволяющий абстрагироваться от большинства проблем абсолютно на любом проекте и минимизировать затраты на разработку.
MVC— контроллеры.
Одна из тех наиболее повторяющихся тем, которые, довольно часто обсуждается среди веб-разработчиков.NET и не только, — это использование универсальных контроллеров для определения конечных точек, маршрутов в их веб-API. Такое высокое внимание к этому оправдано, ведь почти любое веб-приложение, написанное с помощью MVC, использует в себе десятки и сотни различных контроллеров. Поэтому существует множество взглядов на то, как это должно быть реализовано.
Хотя потребность или выгоду в универсальных контроллерах не всегда оправдана ввиду некоторой сложности в реализации, можно себе представить, что особенно в контексте предприятия существуют сценарии, в которых раскрытие аналогично структурированных контроллеров, которые предлагают конечные HTTP маршруты для методов чтения и записи могут иметь некоторую ценность для бизнеса.
Давайте тогда посмотрим на универсальные контроллеры и на то, как мы могли бы также их динамически типизировать и использовать.
Тестовая доменная область ирепозиторий.
Итак, для ознакомительных целей придумаем пару типов. Во-первых, нам понадобятся некоторые фиктивные сущности, которые будут представлять наши объекты данных, а также будут в доступны из API нашего приложения.
Давайте рассмотрим две тестовые сущности, с которыми мы будем работать:
Рис. 1. Демонстрационные сущности
Помимо наших сущностей, давайте также придумаем очень простой и универсальный механизм хранения. Это полностью демонстрационный код, и его единственная цель состоит в том, чтобы мы могли заполнить наш контроллер каким-то более значимым кодом.
Универсальный сервис хранения данных, придуманный для данной статьи, очень прост и основан на словаре в памяти. Частично сервис представляет из себя реализацию паттерна “Репозиторий” и предоставляет только операции чтения и сохранения:
Рис. 2. Универсальный сервис хранения данных
Наконец, вооружившись всем этим, мы можем приступить к рассмотрению видов универсальных контроллеров.
Абстрактный контроллер
Абстрактные контроллеры не поддерживаются из коробки по ASP.NET ядро MVC. Тем не менее, нетрудно представить, как будет выглядеть такой контроллер:
Рис. 3. Реализация абстрактного контроллера
Здесь не так уж много нужно обсуждать, поскольку код говорит сам за себя — мы просто выставляем операции из нашего общего хранилища как операции GET/POST. Мы могли бы пойти дальше, добавив PUT, DELETE или что-то еще, но фактические детали реализации здесь имеют второстепенное значение.
Как уже упоминалось, ASP.NET ядро не будет рассматривать BaseController
Давайте теперь рассмотрим, как мы могли бы решить это проблему.
Подход 1. Наследование от абстрактного контроллера.
Самым простым решением было бы сделать дочерние контроллеры, которые наследуются от BaseController
Рис. 4. Дочерние контроллеры, реализующие GenericController
Код, показанный выше сразу же работает, и никакие дополнительные настройки или пользовательские расширения не требуются. Имена наших контроллеров Book и Album вставляются в шаблон маршрута из базового класса [Route(“api/ [controller]”)], и все определенные операции GET/POST доступны автоматически.
Конечно, этот подход не самый лучший, потому что он диктует что мы должны вручную создать тип контроллера для каждого типа сущности, которое мы хотели бы включить в наше приложение.
Подход 2. Динамические контроллеры
Мы могли бы избежать необходимости вручную создавать тип контроллера для каждой сущности, если мы создадим наш собственный пользовательский интерфейс IApplicationFeatureProvider
Этот интерфейс вызывается при запуске приложения и позволяет нам явно вводить определенные типы, которые должны рассматриваться MVC механизмом как контроллеры. Это означает, что мы могли бы использовать в качестве контроллеров определенные типы, которые обычно не были бы обнаружены механизмом обнаружения MVC контроллеров по умолчанию.
В нашем случае мы могли бы использовать этот подход, чтобы сопоставить наш BaseController
Чтобы это реализовать, нам нужно будет ввести дополнительный декоратор. Для этого введем атрибут GeneratedControllerAttribute.
Он будет использоваться нами для обозначения типов, которые мы хотели бы использовать совместно с BaseController
Очевидно, что мы могли бы сделать это и по-другому, то есть выделив определенную сборку для доменных моделей, и просто создав контроллеры для всех типов — это зависит от требований системы и потребностей заказчика.
Рис. 5. Атрибут GeneratedControllerAttribute
Как часть атрибута, мы также закладываем указание маршрута. Это позволит нам иметь пользовательский базовый путь для каждого из наших типов, а не полагаться на общий унаследованный маршрут.
Следующим шагом является введение вышеупомянутой реализации IApplicationFeatureProvider
Рис. 6. Реализация GenericTypeControllerFeatureProvider
Нам все еще нужно обрабатывать маршруты, которые мы определили как часть нашего GeneratedControllerAttribute. Мы можем сделать это с помощью настраиваемой конвенции MVC [2].
В нашей конвенции мы возьмем шаблон маршрута из атрибута и введем его в контроллер, как если бы это был встроенный маршрут атрибута (эквивалентно использованию атрибута [Route(...)] на контроллере).
Рис. 7. Настраиваемая конвенция MVC
Во время выполнения кода мы перебираем все зарегистрированные в сборке контроллеры, и если у кого-то из них есть аргументы универсального типа (например, BaseController
Обе пользовательские функции: GenericTypeControllerFeatureProvider и GenericControllerRouteConvention — должны быть добавлены в сборку MVC при запуске. Это делается как часть класса Startup, как только мы вызываем AddMvc() [1].
Рис. 8. Регистрация GenericTypeControllerFeatureProvider и GenericControllerRouteConvention
Наконец, мы должны добавить атрибут для сущностей:
Рис. 9. Пример использования реализованного функционала
Код показанный выше заставит работать эти классы как объекты контроллеров. Теперь мы можем добавить сколько угодно типов DTO, декорировать их атрибутом GeneratedControllerAttribute и использовать их как общедоступный вызываемый HTTP маршрут.
Литература:
- Рихтер CLR via C#. Программирование на платформе Microsoft.NET Framework 2.0 на языке C# / Рихтер, Джефри. — М.: Питер, 2012. — 656 c.
- Робинсон, С. C# для профессионалов / С. Робинсон, О. Корнес, Д. Глинн, и др. — М.: ЛОРИ, 2018. — 779 c.
- Альфред, В. Ахо Компиляторы. Принципы, технологии и инструментарий / Альфред В. Ахо и др. — М.: Вильямс, 2015. — 266 c.