Немножко кодгена

от автора

В C# давно уже добавили возможность использовать кодогенерацию. Но покопавшись в интернетах не было найдено общинного количество гайдов. Спасибо сайту мс, за наличие информации по данной теме. Но, увы, там она достаточно поверхностна, а подробности можно найти только экспериментальным путем или изучением различных готовых примеров.

В данной статье хочется показать подробный пример решения задачи с использованием кодогенерации, а так же победа над некоторыми трудностями встреченными в процессе разработки.

Условия

Есть широко известная в узких кругах библиотека VkNet содержащая в своем коде огромное количество моделей. Для некоторых из этих моделей реализован метод FromJson со следующей сигнатурой:

public static Model FromJson(VkResponse response)

Данный метод примитивно парсит модель VkResponse и заполняет его свойства. И написание данного метода хочется автоматизировать.
И того:

  • Настроить кодогенерацию и убедиться что она работает

  • Найти все partial классы среди моделей

  • Убедиться, что у соответствующего класса уже не реализован необходимый метод

  • Найти все свойства помеченные атрибутомJsonProperty

  • На основании типа свойства и параметра атрибута сформировать искомый метод

  • Добавить в partial класс сгенерированный метод

  • Убедиться, что все работает

Для разработки использована IDE Rider

Настройка

Выкачиваем себе библиотеку VKNet и добавляем в решение новый проект VkNet.Generators типа Class Library

Устанавливаем TargetFramework у новой библиотеки как netstandard2.0и добавляем в нее следующие зависимости: Microsoft.CodeAnalysis.Analyzers, Microsoft.CodeAnalysis.CSharp.

А в проект, для которого мы хотим генерировать код (в нашем случае VkNet) добавляем ссылку на проект генератор следующего вида:

<ItemGroup> <ProjectReference Include="..\VkNet.Generators\VkNet.Generators.csproj" OutputItemType="Analyzer"   ReferenceOutputAssembly="false"  /> </ItemGroup>

Отлично, зависимости настроены, переходим к настройке нашего генератора.
Создаем новый класс в проекте VkNet.Generators добавляем ему атрибут [Generator] и реализуем в нем интерфейс ISourceGenerator.

Теперь проверим, что все это работает.
В методе Execute добавим следующий код:

System.Diagnostics.Debugger.Launch(); Debug.WriteLine("generator start");

Теперь необходимо убедить райдер, что ему нужно использовать дебаггер во время билда.
Для этого лезем в настройки и тыкаем соответствующую кнопку Set Rider as the default debugger.

Поиск нужных классов

В методе Execute мы получаем Context из которого мы можем извлекать синтаксические сущности, как было обозначено выше, нас интересуют только классы с определенными условиями:

var models = context.Compilation .SyntaxTrees .SelectMany(syntaxTree => syntaxTree.GetRoot().DescendantNodes()) .Where(x => x is ClassDeclarationSyntax) .Cast<ClassDeclarationSyntax>() .Where(GetPartialModels) .Where(GetSerializableModels) .Where(NotHaveMethodFromJson) .ToImmutableList();

Заметим, что в 6й строке, мы уже получили синтаксические объекты классов и дальше продолжаем работать уже с ними. Рассмотрим примененные к ним условия:

Получение только partial классов:

private static bool GetPartialModels(ClassDeclarationSyntax x) { return x.Modifiers.Any(m => m.ValueText == "partial"); }

Получаем только сериализуемые классы:

classDeclarationSyntax.AttributeLists.First().Attributes.Any(x => x.Name.ToString() == "Serializable");

Проверяем наличие FromJson метода:

classDeclarationSyntax.Members .Any(x =>   (x.Kind() == SyntaxKind.MethodDeclaration  && ((MethodDeclarationSyntax) x).Identifier.ValueText != "FromJSON"));

Извлечение свойств

Получив все необходимые классы на предыдущем этапе необходимо получить все свойства, которые мы в процессе заполним. Для этого нужно обработать каждый класс и извлечь из него все свойства, отмеченные атритбутом JsonProperty и сохранить его тип, имя, а так же аргумент атрибута.

Для начала получим все свойства класса:

var properties = model.Members.OfType<PropertyDeclarationSyntax>();

И для каждого свойства получим соответствующие параметры:

Имя:

var propertyName = property.Identifier.ValueText;

Тип:

var propertyType = property.Type.ToString();

Аргумент атрибута JsonProperty:

var attributeArgument = property.AttributeLists.First() .Attributes.First(x => x.Name.ToString() == "JsonProperty") .ArgumentList?.Arguments.First() .Expression.DescendantTokens() .First() .Text.Replace("\"", string.Empty);

Формирование тела метода

В общем виде, тело метода достаточно простое

Имя свойства = Ответ Вк [Ключ]
Для такого простого выражения подготовим шаблон:

const string PropertyDeclaration = "{0} = response[\"{1}\"],";

К сожалению коллекции таким образом не сериализуются, и нам потребуется подготовить еще пару шаблонов:

const string PropertyReadonlyCollectionWithLambda = "{0} = response[\"{1}\"].ToReadOnlyCollectionOf<{2}>(x => x),";  const string PropertyVkCollection = "{0} = response[\"{1}\"].ToVkCollectionOf<{2}>(x => x),";

Теперь необходимо пройтись по полученной на предыдущем этапе коллекции свойств и опираясь на их тип сформировать строку.

Count = response["count"], Items = response["items"].ToReadOnlyCollectionOf<Conversation>(), Profiles = response["profiles"].ToReadOnlyCollectionOf<User>(), Groups = response["groups"].ToReadOnlyCollectionOf<Group>(),

Формирование тела класса

Для тела класса подготовим шаблон следующего вида:

// Auto-generated code using System; using VkNet.Utils;  namespace {0} {{     public  partial class {1}     {{         public static {2} FromJson(VkResponse response) {{ return new {3} {{ {4} }}; }}     }} }}

Из имеющегося ClassDeclarationSyntax получим необходимые описания класса, а именно нам потребуется namespace, а так же 3 раза имя класса и тело метода полученное на третьем этапе.

string.Format(ClassDefinition, namespaceName, className, className, className, fieldDeclaration)

И соберем тело класса:

 // Auto-generated code using System; using VkNet.Utils;  namespace VkNet.Model {     public  partial class ConversationResult     {         public static ConversationResult FromJson(VkResponse response) {           return new ConversationResult           {             Count = response["count"],             Items = response["items"].ToReadOnlyCollectionOf<Conversation>(),             Profiles = response["profiles"].ToReadOnlyCollectionOf<User>(),             Groups = response["groups"].ToReadOnlyCollectionOf<Group>(),           }; }     } }

Теперь полученную строку необходимо добавить в основной контекст, дополнительно задав имя файла для нового класса:

context.AddSource(model.Identifier.ValueText + ".g.cs",classDeclaration);

Тест

Проверим, что после компиляции в рантайме тестов нашего приложения у нас есть 10 классов со статическим методом FromJson.

string nspace = "Model";  var assembly = Assembly.GetAssembly(typeof(VkApi)); var types = assembly.GetTypes(); var classes = types.Where(x => x.IsClass  && x.Namespace != null  && x.Namespace.Contains(nspace));  var count = classes .Select(@class => @class.GetMethods(BindingFlags.Public|BindingFlags.Static) .Where(x => x.Name.StartsWith("FromJson"))) .Count(methods => methods.Any());  count.Should().BeEqualTo(10);

Итоги

Кодогенерация на C# очень мощный, но достаточно запутанный инструмент. Очевидно, что синтаксические деревья это огромные сложные структуры и разработчики из ms постарались максимально упростить пользователям работу, но это не отменяет обширности кодовой базы, с которой впервые достаточно неудобно взаимодействовать.

Хотелось бы сказать спасибо @ForNeVeR.

И отметить, что поддержку по С# можно найти здесь.

А исходники проекта тут.

Очевидный спойлер

Обсуждая с коллегой он задал очевидный вопрос:

Вот эта вот вся шняга зачем тогда нужна, если там уже ньютонсовт?

Нельзя просто JsonConvert.Deserialize(response)?

Ответ на это прост, грустен и примитивен:
1) Так сложилось исторически
2) Рефактор и избавление от VkResponse требует много сил и времени
3) Это сломает совместимость


ссылка на оригинал статьи https://habr.com/ru/post/678690/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *