[.NET, C#] Кодогенерацию с использованием Roslyn можно использовать и без перехода на .Net 5 (перевод)
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Недавно, когда я просматривал новые возможности, которые будут включены в .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#] Транслируй меня полностью
- [Программирование, Java, Совершенный код, C#, Kotlin] Лучший язык программирования
- [Python, API, Программирование микроконтроллеров, Разработка для интернета вещей] Опыт написания IDL для embedded
- [Совершенный код, .NET, API, C#, Микросервисы] Паттерн CQRS: теория и практика в рамках ASP.Net Core 5
- [.NET, C#] Реализация Minecraft Query протокола в .Net Core
- [.NET, C#] IQueryable порождает сильную связанность (перевод)
- [.NET] First touch of Kafka
- [Python, .NET, История IT] Языку программирования Python исполнилось 30 лет
- [Программирование, .NET, ASP, C#] Реализуем глобальную обработку исключений в ASP.NET Core приложении (перевод)
- [.NET] Как изменить формат данных JSON на Snake Case в ASP.NET Core Web API
Теги для поиска: #_.net, #_c#, #_.net, #_c#, #_codegeneration, #_.net_5, #_.net, #_c#
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 18:49
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Недавно, когда я просматривал новые возможности, которые будут включены в .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; Теперь необходимо проанализировать каждый тип параметра и выяснить следующее:
Семантическая модель дает ответы на эти вопросы: 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 ===========Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 18:49
Часовой пояс: UTC + 5