.NET 5 + Source Generator = Javascript

The task is to implement the generation of SPA (Vue / React) applications based on C # models and controllers.



.NET 5 introduces a source generator. We will do this with his help. This article will cover the main problems that I encountered when using the source generator and their solutions. Generating the UI itself is beyond the scope of this article. Used by Visual Studio 2019.



So, what is required for this:

1. Ability to generate js / vue / jsx files

2. Access to the main project directory

3. Access to the settings file

4. Using third-party libraries inside the generator, for example Newtonsoft.Json

5. Using my other assemblies inside the generator

6. Access to classes / types of controllers and models located in different assemblies

7. Debugging



A few words about T4



.NET 4.x has a T4 code generator. Initially, I tried to solve my problem with it. There were a number of problems, mainly related to the loading of system libraries, which were solved with varying success. But when it came to handling a .NET 5 assembly with controllers, which refers to an alien (for .NET 4.x runtime) AspNetCore library, my brain was at a dead end. T4 did not want to find and load it in any way.



Project structure



All new Microsoft technologies start with Hello World, where everything works cool. But when you start using them in a real project, you run into a lot of problems. One of these is the project structure. In Hello World, this is one assembly. And in a real project there are several of them.



My project includes four conditional assemblies:

1. NetGenerator5.Web - the main launched web application (net5.0), contains controllers, an assembly with models and the generator itself are connected to it.

2. NetGenerator5.Model - assembly with models (net5.0)

3. NetGenerator5.Generator - assembly with generator (netstandard2.0)

4. NetGenerator5.Generator.Dependency - conditional assembly that is used inside the generator (netstandard2.0)



Generator



The generator class implements the ISourceGenerator interface with two methods, Initialize and Execute. The Execute method will run directly during compilation of the project to which the generator is connected.



The generator project itself



<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>

      
      





How to connect it? It is necessary in the main project (NetGenerator5.Web), register the following:



<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>

      
      





Ability to generate js / vue / jsx files



Initially, the generator outputs cs files with C # code. To do this, inside the Execute method, the GeneratorExecutionContext.AddSource context method is used. As far as I understand, it is impossible to change their extension and these files are also compiled. Therefore, it is not possible to put code in any other language there. Visual Studio starts throwing compilation errors.



Therefore, we need a different approach to save js / vue / jsx files. The usual System.IO.File.WriteAllText helped me. But for this you need to know exactly where you need to save the generated files, i.e. know the directory of the main project.



Access to the main project directory



It can be obtained as follows:



In the main NetGenerator5.Web project, write the following:

<ItemGroup>
  <CompilerVisibleProperty Include="MSBuildProjectDirectory" />
</ItemGroup>

      
      





This will make the system variable for the source generator visible.



And in the generator itself, we will access it in the Execute method as follows:

context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.MSBuildProjectDirectory", out var projectDirectory)

      
      





In addition, we need to know exactly where to put the generated files inside the web project itself (for example, in wwwroot / js). It occurred to me to pass this through the generatorsettings.json configuration file in the main project. But now I somehow need to tell the generator about it.



Accessing the settings file



The generator has the ability to access files through the GeneratorExecutionContext.AdditionalFiles context collection inside the Execute method. For my configuration file to be there, you need to set the Build Action = C # analyzer additional file property on it, or like this:

<ItemGroup>
  <AdditionalFiles Include="generatorsettings.json" />
</ItemGroup>

      
      





After that, the contents of the file can be read as follows

var content = context.AdditionalFiles.First(e => e.Path.EndsWith("generatorsettings.json")).GetText(context.CancellationToken);
      
      





Next, a problem arises - it's json, but how can I actually parse it?



Using third party libraries inside the generator



Use an external library. For example Newtonsoft.Json. Something really went wrong here. I connected it through nuget, but the generator did not want to see this library in any way.



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.
      
      





and even though you crack.



The cookbook has a section dedicated to this.

There is even a little more information - how to design your generator in the form of a nuget package. For some reason it didn't help me.



As a result, at first I decided in a strange way. I stupidly added the library itself directly to the project as a file and specified Copy to Output Directory = Copy always / Copy if newer for it and it all worked. But later I got an answer to a question in the roslyn discussion section. The advice helped me. It is necessary to register in the generator project exactly like this:



<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>

      
      





Or alternatively use the built-in System.Text.Json.



Using my other assemblies inside the generator



Further, it would be nice to use my other assemblies inside the generator. For example, helper classes for Vue and React would be nice to be scattered in two different assemblies and connected to the generator as needed.



Oddly enough, everything went smoothly for me here. I just connected NetGenerator5.Generator.Dependency via Dependencies - Add Project Reference. Although some had problems.



Access to classes / types of controllers and models located in different assemblies



Now let's move on to the fun part. To generate files - I needed access to classes / types of controllers and models. Microsoft recommends using SyntaxReceiver

But it only has access to the classes of the currently compiled project (i.e. in my case NetGenerator5.Web), and the NetGenerator5.Model classes are not there.



In the same roslyn discussion section, a solution was found . Within the context of the GeneratorExecutionContext, there is Compilation.GlobalNamespace. You can go over it recursively and get descriptions of all types, including the current compiled assembly and assembly with models.



Debugging



For debugging, it is enough to write in the generator class in the Initialize method



#if DEBUG
  if (!Debugger.IsAttached)
  {
    Debugger.Launch();
  }
#endif

      
      





When starting the build of the main project, a window opens with a proposal to start the debugger. If you click OK, then another instance of Visual Studio will be launched and the debug mode of this generator will be in it. You can go inside all other classes and methods, even those that are in a separate assembly NetGenerator5.Generator.Dependency



Outcome



After compilation NetGenerator5.Web / wwwroot / js file will generated.js, and NetGenerator5.Web \ obj \ GeneratedFiles \ NetGenerator5.Generator \ NetGenerator5.Generator.SourceGenerator will Dummy generated.cs file



complete source code can be viewed here



Sources






All Articles