[.NET] .NET 5 + Source Generator = Javascript

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

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

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

Задача реализовать генерацию 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. Отладка
Пара слов о 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
Полный исходный код можно посмотреть тут
Источники

===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_.net, #_#net5, #_#sourcegenerator, #_#javascript, #_#js, #_#react, #_#vue, #_.net
Профиль  ЛС 
Показать сообщения:     

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

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