
В 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) Это сломает совместимость