Недавно, когда я рассматривал новые возможности, которые будут включены в .Net 5, я натолкнулся на интересный вариант - Генераторы исходного кода C #. Эта функция особенно заинтересовала меня, так как я использовал аналогичный подход в течение последних… 5 лет, и то, что предлагает Microsoft, - это просто более глубокая интеграция этого подхода в процесс сборки.
Далее я поделюсь своим опытом использования Roslyn при генерации кода, и я думаю, что это поможет вам лучше понять, что именно предлагает Microsoft и когда вы можете это использовать.
Во-первых, давайте посмотрим на типичный сценарий генерации кода. У вас есть внешний источник информации, такой как база данных, описание JSON некоторой службы REST, другая сборка (через отражение) и т. Д. И с помощью этой информации вы можете сгенерировать различные типы исходного кода, такие как DTO, классы моделей db или прокси для REST сервисы.
Однако иногда возникают ситуации, когда нет какого-либо внешнего источника информации и все, что вам нужно, содержится в исходном коде самого проекта, куда вы хотите добавить какой-то сгенерированный код.
По совпадению, я недавно опубликовал проект с открытым исходным кодом, в котором есть пример такой ситуации. В проекте есть более 100 классов, которые представляют узлы синтаксического дерева SQL, и мне нужно было создать посетителей, которые бы обходили и изменяли объекты дерева (более подробную информацию о проекте вы можете найти в моей предыдущей статье Синтаксическое дерево и альтернатива LINQ при взаимодействии с базами данных SQL »).
Причина, по которой генерация кода является здесь хорошим выбором, заключается в том, что каждый раз, когда я вношу даже небольшое изменение в классы, мне нужно помнить об изменении посетителей, и эти изменения должны выполняться очень осторожно. Однако я не могу использовать отражение для генерации кода, поскольку сборки, содержащей новые изменения, просто еще не существует, и если эти изменения несовместимы с предыдущей версией и приводят к ошибкам компиляции, то эта сборка никогда не появится, пока я не буду вручную исправить все ошибки.
На первый взгляд, у этой проблемы нет решения, но на самом деле я могу использовать компилятор Roslyn для ее решения. Я могу заранее скомпилировать классы модели и получить информацию, аналогичную той, которую я мог бы получить с помощью отражения.
Давайте создадим простое консольное приложение и добавим пакет Microsoft.CodeAnalysis.CSharp.
Примечание. Теоретически это можно сделать в t4, но я предпочитаю не бороться с зависимостями и странным синтаксисом t4.
Во-первых, нам нужно прочитать все файлы cs, содержащие классы модели, и извлечь из них синтаксические деревья:
Деревья содержат много информации об исходном коде с точки зрения текста (имена классов, имена методов и т. Д.), Но часто этой информации недостаточно, поскольку мы хотим знать, что означает текст, поэтому нам нужно попросить Roslyn проанализировать синтаксические деревья и получить некоторые семантические данные:
Используя семантические данные, мы можем получить объект типа INamedTypeSymbol:
который может предоставить информацию о конструкторах и свойствах классов:
Поскольку все классы модели неизменяемы, все значимые свойства должны быть установлены через их конструкторы, поэтому давайте переберем все параметры конструкторов и получим их типы:
Теперь необходимо проанализировать тип параметра и выяснить следующее:
- Тип списка?
- Является ли тип Nullable (в проекте используются ссылочные типы, допускающие значение NULL)?
- Наследуется ли тип от базового типа (в нашем случае интерфейса), для которого мы создаем «Посетители».
Семантическая модель дает ответы на эти вопросы:
Примечание. Метод AnalyzeSymbol извлекает фактический тип из коллекций и значений Nullables:
List<T> => T (list := true) T? => T (nullable := true) List<T>? => T (list := true, nullable := true)
Проверка базового типа в семантической модели сложнее, чем если бы вы использовали отражение, но это также возможно:
Теперь мы можем поместить всю информацию в простой контейнер:
и использовать его при генерации кода, чтобы создать что-то вроде этого.
В моем проекте я запускаю генерацию кода как консольную утилиту, но в .Net 5 вы сможете встроить это поколение в проект как класс, помеченный специальным атрибутом, который будет автоматически запускаться во время компиляции для добавления недостающих частей. кода. Это, конечно, удобнее, чем отдельная утилита, но идея аналогична.
Наконец, я хочу сказать, что вы не должны воспринимать эту новую функцию .Net 5 как невероятное нововведение, которое коренным образом изменит подход к генерации динамического кода, который используется в таких библиотеках, как AutoMapper, ASP.Net Core и т. Д. ( Слышал такие мнения) Не будет! Дело в том, что генерация кода работает в статическом контексте, где все заранее известно, но, например, AutoMapper не знает, с какими классами он будет работать, и ему все равно придется динамически генерировать код. Однако бывают ситуации, когда такая генерация кода очень полезна (одну из них я описал в этой статье). Поэтому стоит знать об этой функции и понимать ее принципы и ограничения.