C# 9 introduced a new feature that allows you to inspect user code as it is being compiled and generate new C# source files that are added to the compilation. This enables you to write code that runs during compilation and produces additional source code based on the analysis of your program. In this blog post, I will explain what C# source code generators are, why they are useful, and how to use them in your projects.
What are C# source code generators?
A C# source code generator is a piece of code that you can write to hook into the compilation pipeline and generate additional source files that will be compiled. The process is like this one:
(From https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/)
When the compilation runs, the source code is passed to our code generator, which analyzes it and generates new code that is added to the current code and compiled with it, generating an executable that has both the user code and the generated one.
This allows you to make your code more performant and robust: one scenario in which it could be used is to remove the need for reflection in your code: all code is generated at compile time and you don't need to use reflection to work with the class data. Another use is to remove boilerplate code: you can add a generator to add all the extra code that's needed for a specific situation. For example, in my MVVM Toolkit 8 article I've shown the use of removing all the boilerplate code for the INotifyPropertyChanged
interface implementations. In fact, this code:
would create the property with a getter and setter, which triggers the PropertyChanged
event in the setter.
Creating a C# source code generator
We will now create a simple Code Generator. The source code generator is a .NET Standard 2.1 dll that will be added to the main project. To create the dll, you should use these commands:
Add the packages Microsoft.CodeAnalysis.Analyzers
and Microsoft.CodeAnalysis.CSharp
:
The csproj file must be changed to set the new target version and be used as a generator:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.1</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <LangVersion>latest</LangVersion> <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.6.0" /> </ItemGroup> </Project>
In VS Code, create a new file and name it HelloGenerator.cs. The structure of the file is this:
We will create a HelloGenerator
class that implements the ISourceGenerator
interface and add the [Generator]
attribute. This interface has two methods:
Initialize
, to initialize the generator. This method is called once, when your generator is loaded. You can use this method to register callbacks for syntax or semantic changes in the user code.Execute
, where the source code will be generated. TheGeneratorExecutionContext
parameter will provide information about the code being compiled.
Our sample generator looks like this:
We don't need any initialization, and the execution will create a single file with a static class HelloWorld
that has a SayHello
method.
We can run dotnet build
to build the dll. Once you do it, we can create the code that uses it. Create a new console project and name it HelloTest
. In this project, we must reference the HelloGenerator
project:
In the HelloTest
project, we must change the reference for the generator project:
<Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> <ProjectReference Include="..\HelloGenerator\HelloGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/> </ItemGroup> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net7.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> </Project>
The program for this project is:
When you run it, you get something like this:
As you can see, the source generator created a new class called HelloWorld
in a namespace called HelloWorldGenerated
, and added a static method called SayHello
that prints a message to the console. The user code then referenced this class and called its method.
This is a very simple example, but it demonstrates the basic steps of creating and using a source generator. In the next section, we will see how to write a more complex source generator that implements a specific scenario.
Json serializer generator
Let's say we have a class like this:
And we want to implement a method ToJson
, that will take an instance of the Person
class and generate a json string with the data. We could create an extension method, like this one:
This approach works fine, but it requires reflection, which has some performance overhead.
Another approach is to use a source generator, where you could write some code that analyzes the Person
class at compile time and generates a new class that implements a ToJson
method especially written for it, without having to resort to reflection. This approach is more performant, because everything is generated at compile time and there is no need to do anything at runtime.
At the end, this generator will produce a new file with the following extension methods:
To write this generator, we need to do the same thing that we did in the previous project:
Create a new classlib project, add the packages Microsoft.CodeAnalysis.Analyzers
and Microsoft.CodeAnalysis.CSharp
and change the project file be used as a generator:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <LangVersion>latest</LangVersion> <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.6.0" /> </ItemGroup> </Project>
Create a new file JsonGenerator.cs and create a new class that implements the ISourceGenerator
interface and add the [Generator]
attribute to it.
Implement the Initialize
method to register a callback for syntax receiver creation. This callback will create an instance of a custom syntax receiver class that will collect all the classes with the [Serializable]
attribute in the user code and create this class, that implements the ISyntaxReceiver
interface and overrides the OnVisitSyntaxNode
method. This method will be called for every syntax node in the user code and will check if it is a class declaration with the [Serializable]
attribute. If this is the case, it will be added to the CandidateClasses
list, that will be used in the Execute
method.
Implement the Execute
method to generate the extension methods for each collected class using the syntax and semantic models provided by the context parameter.
This code will get the Compilation
object and loop through all the syntax trees, looking for classes that were selected in the Initialize
method. For each found class, it will call the GenerateSource
method and will write a new text file with the generated source:
This code will get the properties for the classes that have the [Serializable]
attribute and will generate a new static class with two methods, ToJson
and FromJson
, that will serialize and deserialize the class. For example, this code will generate this class for the Person class:
As you can see, the generated code is specific for the Person class and doesn't make use of reflection. To test this code, we can create a new project, JsonGeneratorTest and add the generator project as a reference:
And then change the csproj file to tell the referenced project is an analyzer:
<Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> <ProjectReference Include="..\JsonGenerator\JsonGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/> </ItemGroup> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net7.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> </Project>
The code for the tester program is:
Once you run the code, you will get something like this:
As you can see, the generator has created the class and compiled it with the code and we can use it the same way as if we had written it ourselves. This saves us from writing boilerplate code and avoids the use of reflection.
The full source code for the article is at https://github.com/bsonnino/SourceGenerators