No nonsense Source Generators (Part 1)
What is a source generator?
It is an Analyzer that generates code based on the current compiling code and adds it the overall compiling project for its use.
Why is it useful?
By inspecting compiling code and its metadata it is possible to emit new code back into the compiling code to avoid writing code that implements a particular functionality, example:
- Data Builder Generator a source generator that takes the properties of a decorated class and then creates the Builder Object with fluent API and validates its data.
- Value Object Generator is a source generator that takes a simple class to generate IEquatable objects.
It also eliminates the need for intensive use of Reflection on code making it closer to a total AOT compilation, this means that the less code is reflected, the more optimizations we get for better run time performance is archived.
How it can be used?
It is pretty simple, we need to create a simple library and implement the generator itself with the following steps:
Create Source Generator library
Create a C# Class Library project with the Net Standard 2.0 profile and add the dependencies:
- Microsoft.CodeAnalysis.CSharp
- Microsoft.CodeAnalysis.Analyzers
Once those are added the file project should look something like:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>preview</LangVersion>
</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>
Create the implementation
Every generato must be a class that is decorated by the Generator decorator and implement the interface ISourceGenerator
[Generator]
public class HelloWorld : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
}
public void Execute(GeneratorExecutionContext context)
{
var sourceBuilder = new StringBuilder(
@"
using System;
namespace HelloWorldSourceGenerated
{
public static class HelloWorld
{
public static void SayHello(string name)
{
Console.WriteLine(""Hello:"" +name);
}
}
}
");
// we add the generated code
context.AddSource("HelloWorldGenerated.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
}
}
The file is composed from 2 main methods Initialize and Execute, the first setups any required information to be used on by the process, Execute adds the files to the compilation. As you can se we can add source code as text to the generated code and it will be compiled.
Create the project that will use the Analyzer
Once the Analyzer is ready we create a project that references it and uses the code as if it were already there.
The project should look like:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference
Include="..\MyCustomAnalizer\MyCustomAnalizer.csproj" OutputItemType="Analyzer"
ReferenceOutputAssembly="false"/>
</ItemGroup>
</Project>
We call the generated code by simply
using System;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
HelloWorldSourceGenerated.HelloWorld.SayHello("John");
Console.ReadLine();
}
}
}
And that it, you have a Source Generator that generates code at compile time.
I should mention that there are few caveats when working with it currently such as:
- Visual Studio currently has some issues with detecting the code and may see it as Compile errors. it stops once the project is restarted.
- Debugging of the generated code is complex and but the best strategy is to create a Unit testing project to assert that everything is correct, I will show in the incoming parts how to do it easily.
- If you need to see the generated code you should be able to navigate to it but also its possible to emit the code a directory with the “EmitCompilerGeneratedFiles” and “CompilerGeneratedFilesOutputPath” properties at project level