Use Visual Studio and Source Generator to automatically generate code

Page update date :
Page creation date :

Operating environment

Visual Studio
  • Visual Studio 2022
.NET
  • .NET 8.0

Prerequisites

Visual Studio
  • It works even with a somewhat older version
.NET
  • It works even with a somewhat older version

At first

There are several techniques for automatically generating code with your own definitions, but in this article I'll show you how to use a Source Generator. One of the biggest advantages of a source generator is that it analyzes the structure of the current project's source code and generates new code based on it. For example, when you create a new class, you can make it so that the code is automatically added to match the class. You can program what kind of code you want to generate, so you can create any form of automatic code generation you like.

The code is essentially auto-generated in the background and incorporated into the project behind the scenes. Since it is not output as a file in a visible way, it is not used for the purpose of reusing the auto-generated code for general purposes (although it can be removed by copying it for the time being). However, since the code is automatically generated according to the structure of the project, it reduces the cost of manual input and reduces code writing errors accordingly, which is a huge advantage.

In this article, I will explain how to check that the code is automatically generated, so I will not go so far as to actually analyze the code deeply and perform advanced output. Please look it up yourself as an application.

setup

First, install Visual Studio. A brief explanation is summarized in the following Tips.

Basically, you can use it for any project, so it doesn't matter which workload you set up. However, this time, as an "individual component", ". NET Compiler Platform SDK. This is useful for debugging during Source Generator development. If you already have Visual Studio installed, you can add it from the Visual Studio menu under Tools > Get Tools and Features.

Creating and Preparing a Source Generator Project

Source Generator is created in a project separate from the main application project. It doesn't matter if you create them first or create additional ones later. In this case, I will create it from the Source Generator project.

On the Create New Project screen, select Class Library.

The project name can be anything, but for now CodeGenerator , we'll leave it as .

For class libraries, Source Generator currently supports . NET Standard 2.0.

Once you've created your project, get the package with Microsoft.CodeAnalysis.CSharp NuGet. The behavior may differ depending on the version, but there is no point in continuing to use the old version, so I will put the latest version.

Then open the project file as code.

When you open it, you will see the following.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
  </ItemGroup>

</Project>

Add it as follows: Feel free to add anything else you need.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>

    <!-- Roslyn によるコード解析を行う -->
    <IsRoslynComponent>true</IsRoslynComponent>
    <!-- 入れないと警告が表示されるので入れておく -->
    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
    <!-- 最新の C# の記述を使いたいので入れておく -->
    <LangVersion>Latest</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
  </ItemGroup>

</Project>

Next, write the code for the class that will automatically generate the code. First of all, we will only create a framework, so please rewrite the existing code from the beginning Class1.cs or add a new code.

The code should look like this: The class name can be anything, but it is better to have a name that shows what kind of code is automatically generated. SampleGenerator For now, leave it as . Initialize In this method, you will write the code analysis and code generation process.

using Microsoft.CodeAnalysis;

namespace CodeGenerator
{
  [Generator(LanguageNames.CSharp)]
  public partial class SampleGenerator : IIncrementalGenerator
  {
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
    }
  }
}

Next, create a project for the application for which you want to automatically generate code. It can be anything, but here we will simply use it as a console application. The type and version of the framework of the project to which the code is automatically generated corresponds to the general number.

Add a reference to the source generator project from the application-side project.

Some settings cannot be set in the properties, so open the project file in code.

I think it looks like this:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\CodeGenerator\CodeGenerator.csproj" />
  </ItemGroup>

</Project>

Add the settings for the referenced project as follows: Make sure that you do not make a mistake in the syntax of the XML.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\CodeGenerator\CodeGenerator.csproj">
      <OutputItemType>Analyzer</OutputItemType>
      <ReferenceOutputAssembly>False</ReferenceOutputAssembly>
    </ProjectReference>
  </ItemGroup>

</Project>

Next, open the project properties on the "Code Generator side".

Click the link to Open the Debug Launch Properties UI.

Delete the original profile because you don't want to use it.

Add a new profile.

Select Roslyn Component.

If you have made the settings so far, you should be able to select the application project, so select it.

This is the end of the preparation.

Check if you can debug

Open the source generator code and Initialize place a breakpoint at the end of the method.

Let's debug the source generator.

If the process stops at the breakpoint, you can confirm that you are debugging normally. This should make the development of your source generator reasonably easy.

For the time being, let's output a fixed code

First, let's try to output a fixed code easily. It's easy because you don't even need to analyze the code. Even if it is a fixed code, it is handled as a string, so it is possible to increase the production of code in a fixed form by building a program.

If you want to output a fixed code, you context.RegisterPostInitializationOutput can do so by using . The following is an example of the code output.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;

namespace CodeGenerator;

[Generator(LanguageNames.CSharp)]
public partial class SampleGenerator : IIncrementalGenerator
{
  public void Initialize(IncrementalGeneratorInitializationContext context)
  {
    // コードを解析せずそのままコードを出力するならこれを使用します
    context.RegisterPostInitializationOutput(static postInitializationContext =>
    {
      // コード生成処理が中断されることもあるので CancellationToken 処理は入れたほうがいいです
      System.Threading.CancellationToken token = postInitializationContext.CancellationToken;
      token.ThrowIfCancellationRequested();

      // 出力するコードを作ります
      var source = """
internal static class SampleClass
{
  public static void Hello() => Console.WriteLine("Hello Source Generator!!");
}
""";
      
      // コードファイルに出力します (実際に目に見えるどこかのフォルダに出力されるわけではありません)
      postInitializationContext.AddSource("SampleGeneratedFile.cs", SourceText.From(source, Encoding.UTF8));
    });
  }
}

The content of the code is as written in the comments, so I will omit the details. Build it and make sure there are no errors.

When the build is complete, you can expand "Analyzers" in the application project to see the code generated by the code generator.

If you don't see it, restart Visual Studio and check it. It seems that at this time, Visual Studio may not be updated unless you restart it. The Intellisence and syntax highlighting used when creating programs are similar. However, since the code itself is reflected at the time of build, it seems that the program is reflected on the application side.

Once the code is generated, try using it in your application. It should work fine.

Analyze and generate code

If you just process the code normally and output it, it is not much different from other automatic code generation, and you cannot take advantage of the benefits of the source generator. So now let's analyze the code of the application project and generate the code accordingly.

However, the analysis of the code is quite deep, so I can't explain everything here. For the time being, I will explain up to the point where you can analyze and output the code. If you want to go deeper, please do your own research.

For the time being, as an example, I would like to automatically create the code "add a method to Reset all created classes and reset the value default of all properties to ". In recent years, it seems that handling immutable instances has been preferred, but in game programs, it is not possible to create a new instance every frame, so I think that there is a use for the process of resetting the value. default You may want to set something other than that, but if you do that, it will be a long first example, so please apply it yourself.

Code Generator Side

Create a new class for the purpose of keeping the generator you created before. If the content to be automatically generated changes, it is better to create a new one in another class.

The Roslyn compiler takes care of the structuring of the code, so what we're going to do here is parse the structured data and write the code.

First, I'll post all the code. I've tried to have as little code as possible. This time it works, but it is only at a level that moves as a Mr./Ms., so when you actually proceed with development, there will be quite a few missing parts. Please improve there as needed.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Linq;
using System.Text;
using System.Threading;

namespace CodeGenerator;

[Generator(LanguageNames.CSharp)]
public partial class Sample2Generator : IIncrementalGenerator
{
  public void Initialize(IncrementalGeneratorInitializationContext context)
  {
    // 構文を解析し処理対象のノードをコード出力用に変換します
    var syntaxProvider = context.SyntaxProvider.CreateSyntaxProvider(
      // すべてのノードが処理対象となるため、predicate で処理対象とするノードを限定します
      predicate: static (node, cancelToken) =>
      {
        // 処理が中断される場面は多々あるのでいつでもキャンセルできるようにしておく
        cancelToken.ThrowIfCancellationRequested();

        // クラスのノードのみを対象とする (record とかつけると別な種類のノードになるので注意)
        // また partial class であること
        return node is ClassDeclarationSyntax nodeClass && nodeClass.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword));
      },
      // 対象のノードをコード出力に必要な形に変換します
      transform: static (contextSyntax, cancelToken) =>
      {
        // いつでもキャンセルできるようにしておく
        cancelToken.ThrowIfCancellationRequested();

        // 今回は ClassDeclarationSyntax 確定なのでそのままキャストします
        ClassDeclarationSyntax classDecl = (ClassDeclarationSyntax)contextSyntax.Node;

        // データと構文から宣言されたシンボルを取得します
        // 今回利用場面がほとんどないですが取得しておくといろんな情報を取得できるので便利です
        var symbol = Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetDeclaredSymbol(contextSyntax.SemanticModel, classDecl, cancelToken)!;

        // 出力に必要な値を返します。大抵の型を渡すことができます
        return (symbol, classDecl);
      }
    );

    // 解析し変換した情報をもとにコードを出力します
    context.RegisterSourceOutput(syntaxProvider, static (contextSource, input) =>
    {
      // いつでもキャンセルできるようにしておく
      CancellationToken cancelToken = contextSource.CancellationToken;
      cancelToken.ThrowIfCancellationRequested();

      // 解析後に渡された値を受け取ります
      var (symbol, classDecl) = input;

      StringBuilder sbInitValues = new();

      // プロパティを列挙して値を初期化するコードを生成
      foreach (var syntax in classDecl.Members.OfType<PropertyDeclarationSyntax>())
      {
        sbInitValues.AppendLine($"{syntax.Identifier.ValueText} = default;");
      }

      // 出力するコードを作成
      var source = @$"
        {string.Join(" ", classDecl.Modifiers.Select(x => x.ToString()))} class {symbol.Name}
        {{
          public void Reset()
          {{
            {sbInitValues}
          }}
        }}";

      // ファイル名はクラス名に合わせておきます。作成したコードを出力します
      contextSource.AddSource($"{symbol.Name}.g.cs", source);
    });
  }
}

I've written about it in the comments, but I'll explain it in some places.

// 構文を解析し処理対象のノードをコード出力用に変換します
var syntaxProvider = context.SyntaxProvider.CreateSyntaxProvider(
  // すべてのノードが処理対象となるため、predicate で処理対象とするノードを限定します
  predicate: ...,
  // 対象のノードをコード出力に必要な形に変換します
  transform: ...
);

context.SyntaxProvider.CreateSyntaxProvider to analyze the code in the application project and structure it as much as possible. Everything is divided into nodes, and we use to determine which of them we predicate want to process. Also, if necessary transform , convert the processed object with and pass it to the output side.

predicate In this case, we are doing the following.

// すべてのノードが処理対象となるため、predicate で処理対象とするノードを限定します
predicate: static (node, cancelToken) =>
{
  // 処理が中断される場面は多々あるのでいつでもキャンセルできるようにしておく
  cancelToken.ThrowIfCancellationRequested();

  // クラスのノードのみを対象とする (record とかつけると別な種類のノードになるので注意)
  // また partial class であること
  return node is ClassDeclarationSyntax nodeClass && nodeClass.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword));
},

First of all, as with any process, the process of automatically generating code can always be canceled prematurely. So use the cancellation token to ThrowIfCancellationRequested call so that you can interrupt at any time.

predicate Now that every node is called, we want to determine which one is the one we want to process. Since there are a huge number of them, it is better to narrow them down here to some extent.

Since we are going to add processing to the class this time, we ClassDeclarationSyntax will determine whether it is and determine whether only the class will be processed. partial class Also, since the code is attached with , it partial class is put as a judgment.

// 対象のノードをコード出力に必要な形に変換します
transform: static (contextSyntax, cancelToken) =>
{
  // いつでもキャンセルできるようにしておく
  cancelToken.ThrowIfCancellationRequested();

  // 今回は ClassDeclarationSyntax 確定なのでそのままキャストします
  ClassDeclarationSyntax classDecl = (ClassDeclarationSyntax)contextSyntax.Node;

  // データと構文から宣言されたシンボルを取得します
  // 今回利用場面がほとんどないですが取得しておくといろんな情報を取得できるので便利です
  var symbol = Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetDeclaredSymbol(contextSyntax.SemanticModel, classDecl, cancelToken)!;

  // 出力に必要な値を返します。大抵の型を渡すことができます
   return (symbol, classDecl);
}

transform allows you to convert the analysis into the required form and pass it to the code output. This time, we don't do much conversion, but pass the value to the output side as it is return . We are getting "declared symbols" along the way, butSemanticModel we can get a lot of additional information using and Syntac , so I think we can get it if necessary. return tuples are created and returned, but the configuration of the data to be passed to the output side can be anything. If you want to pass multiple data, you can create a tuple like this, or you can define your own class and pass it in.

// 解析し変換した情報をもとにコードを出力します
context.RegisterSourceOutput(syntaxProvider, static (contextSource, input) =>
{
  // いつでもキャンセルできるようにしておく
  CancellationToken cancelToken = contextSource.CancellationToken;
  cancelToken.ThrowIfCancellationRequested();

  // 解析後に渡された値を受け取ります
  var (symbol, classDecl) = input;

  StringBuilder sbInitValues = new();

  // プロパティを列挙して値を初期化するコードを生成
  foreach (var syntax in classDecl.Members.OfType<PropertyDeclarationSyntax>())
  {
    sbInitValues.AppendLine($"{syntax.Identifier.ValueText} = default;");
  }

  // 出力するコードを作成
  var source = @$"
    {string.Join(" ", classDecl.Modifiers.Select(x => x.ToString()))} class {symbol.Name}
    {{
      public void Reset()
      {{
        {sbInitValues}
      }}
    }}";

  // ファイル名はクラス名に合わせておきます。作成したコードを出力します
  contextSource.AddSource($"{symbol.Name}.g.cs", source);
});

context.RegisterSourceOutputcontext.SyntaxProvider.CreateSyntaxProvider Now, we will generate and output code based on the data to be processed by . context.SyntaxProvider.CreateSyntaxProvider return The value of can be received as the second argument of Action , so the code is generated based on that value.

In this case, we are creating code to get the syntax list of properties from the syntax of the class and set the default property to from each name.

After that, create a method based on Reset the class name, embed the code of the property list created earlier, and set the value default of all properties back Reset to The method is completed.

The code is contextSource.AddSource output separately for each class in the method. By the way, the reason why I put "g" in the file name is to make it easier to determine whether it is manually created code or automatically generated code error when there is a build error.

If you actually make it, you will receive requests such as "I want to reset the field", "I want to initialize it other than default", "I want to reset only the automatic properties". If you put them in, the cord will be long, so try to make it by yourself.

Application project side

This time, I created a code that extends the method of the class, so I will create a class appropriately.

The contents are as follows, but if there are properties, you can use the rest as appropriate.

Player.cs

public partial class Player
{
  public string Name { get; set; } = "";


  public float PositionX { get; set; }
  public float PositionY { get; set; }

  public int Level { get; set; } = 1;

  public override string ToString()
  {
    return $"{nameof(Player)} {{{nameof(Name)} = {Name}, {nameof(PositionX)} = {PositionX}, {nameof(PositionY)} = {PositionY}, {nameof(Level)} = {Level}}}";
  }
}

Item.cs

public partial class Item
{
  public string Name { get; set; } = "";

  public float PositionX { get; set; }
  public float PositionY { get; set; }

  public int Type { get; set; }

  public override string ToString()
  {
    return $"{nameof(Item)} {{{nameof(Name)} = {Name}, {nameof(PositionX)} = {PositionX}, {nameof(PositionY)} = {PositionY}, {nameof(Type)} = {Type}}}";
  }
}

The method is extended when Reset you create the code, but it may not be reflected in Visual Studio, so restart Visual Studio at that time. I think you can see that the code is automatically extended to the analyzer.

The indentation is strange, but you can ignore it because you basically don't touch the auto-generated code.

Program.cs Try writing code in to see Reset if you can call the method. I think the results of the execution are reflected as expected.

Program.cs

Console.WriteLine("Hello, World!");

SampleClass.Hello();
Console.WriteLine();

Player player = new()
{
  Name = "Mike",
  PositionX = 10,
  PositionY = 5.5f,
  Level = 3,
};
Console.WriteLine(player);
player.Reset();
Console.WriteLine(player);
Console.WriteLine();

Item item = new()
{
  Name = "Banana",
  PositionX = 50,
  PositionY = 53.5f,
  Type = 12,
};
Console.WriteLine(item);
item.Reset();
Console.WriteLine(item);
Console.WriteLine();