Недавно, когда я рассматривал новые возможности, которые будут включены в .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:

который может предоставить информацию о конструкторах и свойствах классов:

Поскольку все классы модели неизменяемы, все значимые свойства должны быть установлены через их конструкторы, поэтому давайте переберем все параметры конструкторов и получим их типы:

Теперь необходимо проанализировать тип параметра и выяснить следующее:

  1. Тип списка?
  2. Является ли тип Nullable (в проекте используются ссылочные типы, допускающие значение NULL)?
  3. Наследуется ли тип от базового типа (в нашем случае интерфейса), для которого мы создаем «Посетители».

Семантическая модель дает ответы на эти вопросы:

Примечание. Метод 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 не знает, с какими классами он будет работать, и ему все равно придется динамически генерировать код. Однако бывают ситуации, когда такая генерация кода очень полезна (одну из них я описал в этой статье). Поэтому стоит знать об этой функции и понимать ее принципы и ограничения.

(Ссылка на исходный код на github)