Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Задача реализовать генерацию SPA (Vue/React) приложения на основе моделей и контроллеров C#.
В .NET 5 появился source generator. С его помощью это и сделаем. В данной статье будут рассмотрены основные проблемы, с которыми я столкнулся при использовании source generator и их решение. Сама генерация UI выходит за рамки этой статьи. Используется Visual Studio 2019.
Итак, что для этого потребуется:
1. Возможность генерации js / vue / jsx файлов
2. Доступ к каталогу основного проекта
3. Доступ к файлу настроек
4. Использование сторонних библиотек внутри генератора, например Newtonsoft.Json
5. Использование других моих сборок внутри генератора
6. Доступ к классам/типам контроллеров и моделей, расположенных в разных сборках
7. Отладка
В .NET 4.x есть кодогенератор T4. Изначально я пробовал решить свою задачу с его помощью. Был ряд проблем, в основном связанных с подгрузкой системных библиотек, которые решались с переменным успехом. Но когда дело дошло до обработки сборки .NET 5 с контроллерами, которая ссылается на чуждую (для .NET 4.x рантайма) AspNetCore библиотеку — тут мой мозг зашел в тупик. T4 ни в какую не хотел ее находить и грузить.
Все новые технологии Microsoft начинаются с Hello World, в котором все круто работает. Но когда начинаешь использовать их в реальном проекте, то сталкиваешься с кучей проблем. Одной из таких как раз является структура проекта. В Hello World — это одна сборка. А в реальном проекте их несколько.
Мой проект включает в себя четыре условные сборки:
1. NetGenerator5.Web — основное запускаемое веб-приложение (net5.0), содержит контроллеры, к нему подключается сборка с моделями и сам генератор.
2. NetGenerator5.Model — cборка с моделями (net5.0)
3. NetGenerator5.Generator — cборка с генератором (netstandard2.0)
4. NetGenerator5.Generator.Dependency — условная сборка, которая используется внутри генератора (netstandard2.0)
Класс генератора реализует интерфейс ISourceGenerator с двумя методами — Initialize и Execute. Метод Execute будет запускаться непосредственно во время компиляции проекта, к которому подключен генератор.
Сам проект генератора
Как его подключать? Необходимо в основном проекте (NetGenerator5.Web), прописать следующее:
Изначально у генератора на выходе cs файлы с C# кодом. Для этого внутри метода Execute используется метод контекста GeneratorExecutionContext.AddSource. Поменять расширение у них, я так понял, нельзя и эти файлы так же компилируются. Поэтому поместить туда код на любом другом языке не представляется возможным. Visual Studio начинает выдавать ошибки компиляции.
Поэтому для сохранения js / vue / jsx файлов нам потребуется другой подход. Обычный System.IO.File.WriteAllText мне помог. Но для этого необходимо знать куда именно надо сохранить сгенерированные файлы, т.е. знать каталог основного проекта.
Его можно получить следующим образом:
Прописать в основном NetGenerator5.Web проекте следующее:
Этим мы сделаем видимой системную переменную для source generator.
А в самом генераторе получим к ней доступ в методе Execute следующим образом:
Помимо этого нам надо знать куда именно складывать сгенерированные файлы внутри самого веб проекта (например в wwwroot/js). Мне пришло в голову передать это через файл с настройками generatorsettings.json, который располагался бы в основном проекте. Но теперь мне как-то необходимо рассказать о нем генератору.
В генераторе есть возможность обратиться к файлам через коллекцию контекста GeneratorExecutionContext.AdditionalFiles внутри метода Execute. Чтобы мой файл с настройками оказался там, необходимо проставить у него свойство Build Action=C# analyzer additional file, или так:
После этого содержимое файла можно считать следующим образом
Далее возникает проблема — это же json, а как мне, собственно, его распарсить?
Использовать внешнюю библиотеку. Например Newtonsoft.Json. Вот тут действительно что-то пошло не так. Я ее подключил через nuget, но генератор ни в какую не хотел видеть эту библиотеку.
и хоть ты тресни.
В cookbook есть раздел, посвященный этому
Там даже немного больше информации — как свой генератор оформить в виде nuget пакета. Мне это почему-то не помогло.
В итоге сначала решил странным способом. Я тупо добавил саму библиотеку напрямую в проект как файл и указал для нее Copy to Output Directory = Copy always / Copy if newer и все заработало. Но позже мне ответили на вопрос в разделе дискуссий, посвящённому roslyn. Совет мне помог. Нужно прописать в проекте генератора именно так:
Или, как альтернатива, использовать встроенный System.Text.Json.
Далее, было бы неплохо использовать внутри генератора другие мои сборки. Например, вспомогательные классы для Vue и React хорошо бы разбросать по двум разным сборкам и подключать их к генератору по необходимости.
Как ни странно, здесь у меня все прошло гладко. Я просто подключил NetGenerator5.Generator.Dependency через Dependencies — Add Project Reference. Хотя у кого-то возникали проблемы.
Теперь перейдем к самому интересному. Чтобы сгенерировать файлы — мне нужен был доступ к классам/типам контроллеров и моделей. Microsoft рекомендует использовать SyntaxReceiver
Но он имеет доступ только к классам текущего компилируемого проекта (т.е. в моем случае NetGenerator5.Web), а классов NetGenerator5.Model там нет.
В том же разделе дискуссий roslyn было найдено решение. Внутри контекста GeneratorExecutionContext есть Compilation.GlobalNamespace. По нему можно пройтись рекурсивно и получить описания всех типов, в том числе и текущей компилируемой сборки и сборки с моделями.
Для отладки достаточно прописать в классе генератора в методе Initialize
При запуске билда основного проекта открывается окно с предложением запустить отладчик. Если нажать OK — то будет запущен еще один экземпляр Visual Studio и в нем будет режим отладки данного генератора. Можно заходить внутрь всех других классов и методов, даже в те, которые находятся в отдельной сборке NetGenerator5.Generator.Dependency
После компиляции в NetGenerator5.Web / wwwroot/js появится файл generated.js, а в NetGenerator5.Web\obj\GeneratedFiles\NetGenerator5.Generator\NetGenerator5.Generator.SourceGenerator появится файл пустышка generated.cs
Полный исходный код можно посмотреть тут
В .NET 5 появился source generator. С его помощью это и сделаем. В данной статье будут рассмотрены основные проблемы, с которыми я столкнулся при использовании source generator и их решение. Сама генерация UI выходит за рамки этой статьи. Используется Visual Studio 2019.
Итак, что для этого потребуется:
1. Возможность генерации js / vue / jsx файлов
2. Доступ к каталогу основного проекта
3. Доступ к файлу настроек
4. Использование сторонних библиотек внутри генератора, например Newtonsoft.Json
5. Использование других моих сборок внутри генератора
6. Доступ к классам/типам контроллеров и моделей, расположенных в разных сборках
7. Отладка
Пара слов о T4
В .NET 4.x есть кодогенератор T4. Изначально я пробовал решить свою задачу с его помощью. Был ряд проблем, в основном связанных с подгрузкой системных библиотек, которые решались с переменным успехом. Но когда дело дошло до обработки сборки .NET 5 с контроллерами, которая ссылается на чуждую (для .NET 4.x рантайма) AspNetCore библиотеку — тут мой мозг зашел в тупик. T4 ни в какую не хотел ее находить и грузить.
Структура проекта
Все новые технологии Microsoft начинаются с Hello World, в котором все круто работает. Но когда начинаешь использовать их в реальном проекте, то сталкиваешься с кучей проблем. Одной из таких как раз является структура проекта. В Hello World — это одна сборка. А в реальном проекте их несколько.
Мой проект включает в себя четыре условные сборки:
1. NetGenerator5.Web — основное запускаемое веб-приложение (net5.0), содержит контроллеры, к нему подключается сборка с моделями и сам генератор.
2. NetGenerator5.Model — cборка с моделями (net5.0)
3. NetGenerator5.Generator — cборка с генератором (netstandard2.0)
4. NetGenerator5.Generator.Dependency — условная сборка, которая используется внутри генератора (netstandard2.0)
Генератор
Класс генератора реализует интерфейс ISourceGenerator с двумя методами — Initialize и Execute. Метод Execute будет запускаться непосредственно во время компиляции проекта, к которому подключен генератор.
Сам проект генератора
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>preview</LangVersion>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<IncludeBuildOutput>false</IncludeBuildOutput>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2" PrivateAssets="all" />
</ItemGroup>
</Project>
Как его подключать? Необходимо в основном проекте (NetGenerator5.Web), прописать следующее:
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GeneratedFiles</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\NetGenerator5.Generator\NetGenerator5.Generator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
Возможность генерации js / vue / jsx файлов
Изначально у генератора на выходе cs файлы с C# кодом. Для этого внутри метода Execute используется метод контекста GeneratorExecutionContext.AddSource. Поменять расширение у них, я так понял, нельзя и эти файлы так же компилируются. Поэтому поместить туда код на любом другом языке не представляется возможным. Visual Studio начинает выдавать ошибки компиляции.
Поэтому для сохранения js / vue / jsx файлов нам потребуется другой подход. Обычный System.IO.File.WriteAllText мне помог. Но для этого необходимо знать куда именно надо сохранить сгенерированные файлы, т.е. знать каталог основного проекта.
Доступ к каталогу основного проекта
Его можно получить следующим образом:
Прописать в основном NetGenerator5.Web проекте следующее:
<ItemGroup>
<CompilerVisibleProperty Include="MSBuildProjectDirectory" />
</ItemGroup>
Этим мы сделаем видимой системную переменную для source generator.
А в самом генераторе получим к ней доступ в методе Execute следующим образом:
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.MSBuildProjectDirectory", out var projectDirectory)
Помимо этого нам надо знать куда именно складывать сгенерированные файлы внутри самого веб проекта (например в wwwroot/js). Мне пришло в голову передать это через файл с настройками generatorsettings.json, который располагался бы в основном проекте. Но теперь мне как-то необходимо рассказать о нем генератору.
Доступ к файлу настроек
В генераторе есть возможность обратиться к файлам через коллекцию контекста GeneratorExecutionContext.AdditionalFiles внутри метода Execute. Чтобы мой файл с настройками оказался там, необходимо проставить у него свойство Build Action=C# analyzer additional file, или так:
<ItemGroup>
<AdditionalFiles Include="generatorsettings.json" />
</ItemGroup>
После этого содержимое файла можно считать следующим образом
var content = context.AdditionalFiles.First(e => e.Path.EndsWith("generatorsettings.json")).GetText(context.CancellationToken);
Далее возникает проблема — это же json, а как мне, собственно, его распарсить?
Использование сторонних библиотек внутри генератора
Использовать внешнюю библиотеку. Например Newtonsoft.Json. Вот тут действительно что-то пошло не так. Я ее подключил через nuget, но генератор ни в какую не хотел видеть эту библиотеку.
Exception was of type 'FileNotFoundException' with message 'Could not load file or assembly 'Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed' or one of its dependencies.
и хоть ты тресни.
В cookbook есть раздел, посвященный этому
Там даже немного больше информации — как свой генератор оформить в виде nuget пакета. Мне это почему-то не помогло.
В итоге сначала решил странным способом. Я тупо добавил саму библиотеку напрямую в проект как файл и указал для нее Copy to Output Directory = Copy always / Copy if newer и все заработало. Но позже мне ответили на вопрос в разделе дискуссий, посвящённому roslyn. Совет мне помог. Нужно прописать в проекте генератора именно так:
<ItemGroup>
<!-- Generator dependencies -->
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" GeneratePathProperty="true" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NetGenerator5.Generator.Dependency\NetGenerator5.Generator.Dependency.csproj" />
</ItemGroup>
<PropertyGroup>
<GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
</PropertyGroup>
<Target Name="GetDependencyTargetPaths">
<ItemGroup>
<TargetPathWithTargetPlatformMoniker Include="$(PKGNewtonsoft_Json)\lib\netstandard2.0\Newtonsoft.Json.dll" IncludeRuntimeDependency="false" />
</ItemGroup>
</Target>
Или, как альтернатива, использовать встроенный System.Text.Json.
Использование других моих сборок внутри генератора
Далее, было бы неплохо использовать внутри генератора другие мои сборки. Например, вспомогательные классы для Vue и React хорошо бы разбросать по двум разным сборкам и подключать их к генератору по необходимости.
Как ни странно, здесь у меня все прошло гладко. Я просто подключил NetGenerator5.Generator.Dependency через Dependencies — Add Project Reference. Хотя у кого-то возникали проблемы.
Доступ к классам/типам контроллеров и моделей, расположенных в разных сборках
Теперь перейдем к самому интересному. Чтобы сгенерировать файлы — мне нужен был доступ к классам/типам контроллеров и моделей. Microsoft рекомендует использовать SyntaxReceiver
Но он имеет доступ только к классам текущего компилируемого проекта (т.е. в моем случае NetGenerator5.Web), а классов NetGenerator5.Model там нет.
В том же разделе дискуссий roslyn было найдено решение. Внутри контекста GeneratorExecutionContext есть Compilation.GlobalNamespace. По нему можно пройтись рекурсивно и получить описания всех типов, в том числе и текущей компилируемой сборки и сборки с моделями.
Отладка
Для отладки достаточно прописать в классе генератора в методе Initialize
#if DEBUG
if (!Debugger.IsAttached)
{
Debugger.Launch();
}
#endif
При запуске билда основного проекта открывается окно с предложением запустить отладчик. Если нажать OK — то будет запущен еще один экземпляр Visual Studio и в нем будет режим отладки данного генератора. Можно заходить внутрь всех других классов и методов, даже в те, которые находятся в отдельной сборке NetGenerator5.Generator.Dependency
Итоги
После компиляции в NetGenerator5.Web / wwwroot/js появится файл generated.js, а в NetGenerator5.Web\obj\GeneratedFiles\NetGenerator5.Generator\NetGenerator5.Generator.SourceGenerator появится файл пустышка generated.cs
Полный исходный код можно посмотреть тут
Источники
- github.com/amis92/csharp-source-generators
- github.com/dotnet/roslyn/blob/master/docs/features/source-generators.md
- github.com/dotnet/roslyn/blob/master/docs/features/source-generators.cookbook.md
- mihailromanov.wordpress.com/2021/01/31/net-code-generation-part-6-c-source-generators
- dominikjeske.github.io/source-generators
- github.com/dotnet/roslyn/discussions
- habr.com/ru/post/530454
- habr.com/ru/post/533128