[.NET, C#] Кодогенерацию с использованием Roslyn можно использовать и без перехода на .Net 5 (перевод)

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
26-Фев-2021 02:31


Недавно, когда я просматривал новые возможности, которые будут включены в .Net 5, я натолкнулся на одну весьма интересную — генераторы исходного кода. Этот функционал меня особенно заинтересовал, так как я использую аналогичный подход в течение последних… 5 лет, и то, что предлагает Microsoft — это просто более глубокая интеграция этого подхода в процесс сборки проектов.
Примечание: Оригинал был написан в момент, когда релиз .Net 5 только-только собирался выйти, но актуальности этот текст, на мой взгляд, не потерял, поскольку переход на новую версию платформы занимает какое-то время, да и принципы работы с Roslyn никак не поменялись.
Далее я поделюсь своим опытом использования Roslyn при генерации кода, и надеюсь, что это поможет вам лучше понять, что именно предлагает Microsoft в .Net 5 и в каких случаях это можно использовать.
Для начала, давайте рассмотрим типичный сценарий генерации исходного кода. У вас есть некий внешний источник информации например такой как база данных или JSON описание какого-нибудь REST сервиса или другая .Net сборка (через рефлексию) или что-нибудь еще, и с помощью этой информации вы можете сгенерировать различные типы исходного кода, такие как DTO, классы моделей базы данных или прокси для REST сервисов.
Однако иногда возникают ситуации, когда нет какого-либо внешнего источника информации и все, что вам нужно, содержится в исходном коде самого проекта, куда вы хотите добавить какой-то сгенерированный код.
По совпадению, я недавно опубликовал проект с открытым исходным кодом, в котором есть пример такой ситуации. В проекте более 100 классов, которые представляют узлы синтаксического дерева SQL, и мне нужно было создать посетителей (visitors — реализации интерфейса IVisitor в советующем шаблоне проектирования), которые будут обходить и изменять объекты дерева (больше информации о проекте вы можете найти в моей предыдущей статье "Дерево синтаксиса и альтернатива LINQ при взаимодействии с базами данных SQL").
Причина, по которой генерация кода является здесь хорошим выбором, заключается в том, что каждый раз, когда я делаю даже небольшое изменение в классах, мне нужно помнить об изменении посетителей (visitors), и эти изменения должны выполняться очень осторожно. Однако я не могу использовать рефлексию для генерации кода, так как сборка (assembly), которая содержит эти новые изменения, просто еще не существует, и если эти изменения несовместимы с предыдущей версией и приводят к ошибкам компиляции, то эта сборка никогда и не появится до тех пор, пока я вручную не исправлю все ошибки.
На первый взгляд, у этой проблемы нет решения, но на самом деле, чтобы её решить, я могу использовать компилятор Roslyn и заранее пре-компилировать классы модели, получив таким образом информацию, аналогичную той, которую я мог бы получить через рефлексию.
Давайте создадим простое консольное приложение и добавим в него пакет Microsoft.CodeAnalysis.CSharp.
Примечание: теоретически это можно сделать и через t4 (без консольного приложения), но я предпочитаю не бороться с добавлением в него ссылок на dll и странным синтаксисом, при отсутствии нормального редактора.
Для начала, нам нужно прочитать все .cs файлы, содержащие классы модели, и извлечь из них синтаксические деревья:
var files = Directory.EnumerateFiles(
    Path.Combine(projectFolder, "Syntax"),
    "*.cs",
    SearchOption.AllDirectories);
files = files.Concat(Directory.EnumerateFiles(projectFolder, "IExpr*.cs"));
var trees = files
    .Select(f => CSharpSyntaxTree.ParseText(File.ReadAllText(f)))
    .ToList();

Синтаксические деревья содержат много информации об исходном коде с точки зрения текста (имена классов, имена методов и т. д.), но часто этой информации недостаточно, поскольку мы хотим знать, что же этот текст означает, и поэтому нам нужно попросить Roslyn проанализировать синтаксические деревья для того, чтобы получить семантические данные:
var cSharpCompilation = CSharpCompilation.Create("Syntax", trees);
foreach (var tree in trees)
{
    var semantic = cSharpCompilation.GetSemanticModel(tree);
    ...

Используя семантические данные, мы можем получить объект типа INamedTypeSymbol:
foreach (var classDeclarationSyntax in tree
    .GetRoot()
    .DescendantNodesAndSelf()
    .OfType<ClassDeclarationSyntax>())
{
    var classSymbol = semantic.GetDeclaredSymbol(classDeclarationSyntax);

который может предоставить информацию о конструкторах и свойствах классов:
//Properties
var properties = GetProperties(classSymbol);
List<ISymbol> GetProperties(INamedTypeSymbol symbol)
{
    List<ISymbol> result = new List<ISymbol>();
    while (symbol != null)
    {
        result.AddRange(symbol.GetMembers()
            .Where(m => m.Kind == SymbolKind.Property));
        symbol = symbol.BaseType;
    }
    return result;
}
//Constructors
foreach (var constructor in classSymbol.Constructors)
{
    ...
}

Поскольку все классы модели неизменяемы, то все значения свойств этих классов должны быть установлены через их конструкторы, поэтому переберем все параметры конструкторов и получим их типы:
foreach (var parameter in constructor.Parameters)
{
    ...
    INamedTypeSymbol pType = (INamedTypeSymbol)parameter.Type;

Теперь необходимо проанализировать каждый тип параметра и выяснить следующее:
  • Является ли этот тип списком?
  • Является ли тип Nullable (в проекте используются "Nullable reference types")?
  • Наследуется ли от этот тип от базового типа (в нашем случае интерфейса), для которого мы и создаем "Посетителей" (Visitors).

Семантическая модель дает ответы на эти вопросы:
var ta = AnalyzeSymbol(ref pType);
....
(bool IsNullable, bool IsList, bool Expr) AnalyzeSymbol(
    ref INamedTypeSymbol typeSymbol)
{
    bool isList = false;
    var nullable = typeSymbol.NullableAnnotation == NullableAnnotation.Annotated;
    if (nullable && typeSymbol.Name == "Nullable")
    {
        typeSymbol = (INamedTypeSymbol)typeSymbol.TypeArguments.Single();
    }
    if (typeSymbol.IsGenericType)
    {
        if (typeSymbol.Name.Contains("List"))
        {
            isList = true;
        }
        if (typeSymbol.Name == "Nullable")
        {
            nullable = true;
        }
        typeSymbol = (INamedTypeSymbol)typeSymbol.TypeArguments.Single();
    }
    return (nullable, isList, IsExpr(typeSymbol));
}

Примечание: метод AnalyzeSymbol извлекает фактический тип из коллекций и значений Nullables::
List<T> => T (list := true)
T? => T (nullable := true)
List<T>? => T (list := true, nullable := true)

Проверка базового типа в семантической модели Roslyn является более сложной задачей, чем такая же при использовании рефлексии, но это также возможно:
bool IsExpr(INamedTypeSymbol symbol)
{
    while (symbol != null)
    {
        if (symbol.Interfaces.Any(NameIsExpr))
        {
            return true;
        }
        symbol = symbol.BaseType;
    }
    return false;
    bool NameIsExpr(INamedTypeSymbol iSym)
    {
        if (iSym.Name == "IExpr")
        {
            return true;
        }
        return IsExpr(iSym);
    }
}

Теперь мы можем поместить всю эту информацию в простой контейнер:
public class NodeModel
{
    ...
    public string TypeName { get; }
    public bool IsSingleton { get; }
    public IReadOnlyList<SubNodeModel> SubNodes { get; }
    public IReadOnlyList<SubNodeModel> Properties { get; }
}
public class SubNodeModel
{
    ...
    public string PropertyName { get; }
    public string ConstructorArgumentName { get; }
    public string PropertyType { get; }
    public bool IsList { get; }
    public bool IsNullable { get; }
}

и использовать его при генерации кода, получая при этом что-то вроде этого (большой класс с кучей однотипных методов). Ссылка на сам генератор в конце статьи.
В моем проекте я запускаю генерацию кода как консольную утилиту, но в .Net 5 вы сможете встроить эту генерацию в класс реализующий интерфейс ISourceGenerator и помеченный специальным атрибутом Generator. Экземпляр этого класса будет автоматически создаваться и запускаться во время сборки проекта для добавления недостающих частей кода. Это, конечно, удобнее, чем отдельная утилита, но идея аналогична.
Примечание: Я здесь не буду описывать саму кодогенерацию в .Net 5 так как в интернете есть много информации об этом, например ссылка 1 или ссылка 2
В завершение, я хочу сказать, что вы не должны воспринимать эту новую возможность .Net 5 как невероятное нововведение, которое коренным образом изменит подход к генерации динамического кода, используемый в таких библиотеках, как AutoMapper, Dapper и т. д. (слышал и такие мнения) Не изменит! Дело в том, что описанная выше генерация кода работает в статическом контексте, где все заранее известно, но, например, AutoMapper не знает заранее, с какими классами он будет работать, и ему все равно придется динамически генерировать IL код "на лету". Однако бывают ситуации, когда такая генерация кода может быть весьма полезна (одну из них ситуаций я описал в этой статье). Поэтому стоит, как минимум, знать об этой возможности и понимать ее принципы и ограничения.
Ссылка на исходный код на github
===========
Источник:
habr.com
===========

===========
Автор оригинала: Dmitry Tikhonov
===========
Похожие новости: Теги для поиска: #_.net, #_c#, #_.net, #_c#, #_codegeneration, #_.net_5, #_.net, #_c#
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 18:49
Часовой пояс: UTC + 5