Introduction
Code generation is an essential technique in software development that can help you automate repetitive tasks, optimize performance, and reduce errors. However, writing and maintaining code generators can be challenging and time-consuming. That's why C# 9 introduced a powerful feature called Source Generators that can simplify and enhance your code generation experience.
In this article, We will understand what Source Generators are and how you can write your own Source Generator to generate code based on attributes or other markers in your source code.
What Are Source Generators?
Source Generators are a new kind of component that you can write using the .NET Compiler Platform (Roslyn) SDK. They let you do two major things.
- Retrieve a Compilation object that represents all user code that is being compiled. This object can be inspected, and you can write code that works with the syntax and semantic models for the code being compiled, just like with analyzers today.
- Generate C# source files that can be added to a Compilation object during compilation. In other words, you can provide additional source code as input to a compilation while the code is being compiled.
When combined, these two things are what make Source Generators so useful. You can inspect user code with all of the rich metadata that the compiler builds up during compilation, then emit C# code back into the same compilation that is based on the data you've analyzed.
If you're familiar with Roslyn Analyzers, you can think of Source Generators as analyzers that can emit C# source code. A Source Generator is a .NET Standard 2.0 assembly that is loaded by the compiler along with any analyzers. It is usable in environments where .NET Standard components can be loaded and run.
Source Generators are different from other code generation techniques, such as T4 templates or Reflection, in several ways.
- Source Generators operate at compile time, not at design time or run time. This means they have no impact on the performance of your application at run time, and they can access compile-time information that is not available at design time or run time.
- Source Generators do not modify existing source files or generate new files on disk. They only add new source files to the compilation in memory. This means they do not interfere with your source control or file system, and they do not require any manual steps to invoke them or include their output in your project.
- Source Generators are fully integrated with the C# language and tooling. They support all C# language features and constructs, and they work seamlessly with Visual Studio, MSBuild, dotnet CLI, and any other tools that use Roslyn.
Writing Your First Source Generator
To get started with Source Generators, you'll need to install the latest .NET SDK and the latest Visual Studio.
Create projects with the following structure.
The MyGenerator.csproj
file is a standard C# class library project that references the Microsoft.CodeAnalysis.CSharp
and Microsoft.CodeAnalysis.Analyzers
packages. These packages provide the APIs for working with C# syntax and semantic models.
The MyGeneratorClass.cs
file contains a class that implements the ISourceGenerator
interface. This interface defines two methods: Initialize
and Execute
. The Initialize
method is called once when the generator is created, and it can be used to register callbacks for various events in the compilation process. The Execute
method is called for each generation pass, and it receives a GeneratorExecutionContext
object that provides access to the compilation object and other useful information.
The MyGeneratorTests.csproj
file is an exe project that references the MyGenerator.csproj
project.
Let's write a simple Source Generator that generates a class with a static property that returns the current date and time as a string. To do this, we need to do three things:
- Define an attribute that we can use to mark our target classes.
- Write some code to find all classes marked with our attribute in the user code.
- Write some code to generate a new class for each target class with our property.
In MyGenerator
project, create a file called GenerateDateTimeAttribute.cs
with the following code.
This attribute is very simple and has no parameters. We only use it as a marker to identify our target classes.
Next, we write some code to find all classes marked with our attribute in the user code. We can do this in the Initialize
method of our generator class by registering a callback for the SyntaxReceiver
event. This event is fired for each syntax node in the user code, and we can use it to collect the nodes that we are interested in. We can define a nested class that implements the ISyntaxReceiver
interface and stores the class declaration nodes that have our attribute.
We use the [Generator]
attribute to mark our generator class so that the compiler can discover it. We also use the ToString
method to get the name of the attribute since it may be qualified or unqualified.
Finally, we write some code to generate a new class for each target class with our property. We can do this in the Execute
method of our generator class by using the Compilation
object and the CSharpSyntaxTree
class. We also need to add a reference to our attribute project so that we can use its namespace and type name.
We use a helper method to generate the source code of the new class using a StringBuilder
. We also use string interpolation to insert the class name and namespace name into the code. The generated property simply returns the current date and time as a string using DateTime.Now.ToString()
.
To test our generator, we can create a console application project that references both our generator project and our attribute project. We can then write some user code that uses our attribute and calls our generated property.
When we build and run this project, we should see something like this.
Conclusion
In this article, we learned what Source Generators are, how they differ from other code-generation techniques, and how we can write our own Source Generator to generate code based on attributes or other markers in our source code.
I hope you enjoyed this article and learned something new. Thank you for reading! 😊