Unleashing the power of Source Generators in .NET

Unleashing the power of Source Generators in .NET

In the evolving world of .NET, a new feature was introduced in .NET 5.0 that might change the way we write code - the Source Generators. They are an addition to the C# compiler that lets developers generate code during the compilation process. Let's explore how to create and use them in our projects.

What are Source Generators?

Source Generators provide a way to inject our own code-generation logic into the compilation process, enabling us to automate repetitive tasks, reduce human error and improve code readability. One could use them to generate boilerplate code that can be derived from the codebase or to analyze the codebase for certain attributes or patterns and then generate code accordingly.

The magic sauce here is the ISourceGenerator interface, which can be implemented in the following simple example:

using Microsoft.CodeAnalysis;

namespace SourceGenerator
{
    [Generator]
    public class HelloSourceGenerator : ISourceGenerator
    {
        public void Execute(GeneratorExecutionContext context)
        {
            // Code generation goes here
        }

        public void Initialize(GeneratorInitializationContext context)
        {
            // No initialization required for this one
        }
    }
}
💡
Note the [Generator] attribute decoration on the HelloSourceGenerator class.

Creating a Simple Source Generator

Let's create a simple source generator that will add a "Hello, World!" method to each public class in our project. Given the following code:

using Example;

var foo = new Foo();
var bar = new Bar();

//classes
namespace Example
{
    public partial class Foo { }

    public partial class Bar { }
}

Now let's say we want to dynamically inject an HelloWorld() method during compilation, here's how our source generator might look like:

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

[Generator]
public class HelloWorldGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context) { }

    public void Execute(GeneratorExecutionContext context)
    {
        var compilation = context.Compilation;
        var syntaxTrees = compilation.SyntaxTrees;

        foreach (var syntaxTree in syntaxTrees)
        {
            var model = compilation.GetSemanticModel(syntaxTree);
            var classes = syntaxTree.GetRoot()
               .DescendantNodes()
               .OfType<ClassDeclarationSyntax>();

            foreach (var @class in classes)
            {
                var classSymbol = model.GetDeclaredSymbol(@class);

                context.AddSource(
                    $"{classSymbol.Name}_HelloWorld.cs",
                    SourceText.From($$"""
    
                    namespace {{classSymbol.ContainingNamespace}}
                    {
                        public partial class {{@class.Identifier}}
                        {
                            public void HelloWorld()
                            {
                                System.Console.WriteLine("Hello, World!");
                            }
                        }
                    }
                
                    """, Encoding.UTF8));
            }
        }
    }
}

Our HelloWorldGenerator scans each class in your project and generates a new HelloWorld method that prints "Hello, World!" to the console.

Be sure to include the Nuget package references to Microsoft.CodeAnalysis.Common and Microsoft.CodeAnalysis.CSharp:

<ItemGroup>
   <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.6.0" PrivateAssets="all" />
   <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>
💡
The source generator project needs to target the netstandard2.0 TFM, otherwise it will not work.

Packaging and Using Your Source Generator

Once the source generator is implemented, it should be packaged as a NuGet package. This can be achieved by creating a .nuspec file for your project and then using the nuget pack command.

Once the NuGet package is ready, it can be used in other projects. To reference it in your project, you can add it as an Analyzer. This can be done by adding the following to your .csproj file:

<ItemGroup>
  <Analyzer Include="path\to\nuget\package\HelloWorldGenerator.1.0.0.nupkg" />
</ItemGroup>

Just replace "path\to\nuget\package\HelloWorldGenerator.1.0.0.nupkg" with the path to your NuGet package file.

However, there might be instances where you want to reference the source generator project directly, rather than packaging it as a NuGet package. In such cases, the <ProjectReference> tag can be used. Assuming the source generator project and the target project are in the same solution, you can add a reference like so:

<ItemGroup>
  <ProjectReference Include="path\to\source\generator\project\HelloWorldGenerator.csproj"
  OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

Again, replace the path with the relative path to your source generator project.

When you compile your project, the source generator will run, and it will generate code that gets included in the compilation process.

Now with everything in place, we can use the generated code accordingly:

using Example;

var foo = new Foo();
var bar = new Bar();

foo.HelloWorld();
bar.HelloWorld();


namespace Example
{
    public partial class Foo { }

    public partial class Bar { }
}
💡
There seems to be a small glitch with Visual Studio that breaks the intellisense after compilation, but a quick restart of Visual Studio fixes the problem.

Where is the magic?

The Voodoo of Source Generators happens behind the scenes without emitting any visible source code, however ther are cases where you would like to make the generated code visible, or control where it is generated to. To do this, just add the EmitCompilerGeneratedFiles property as true in the project file of the consumer application.

<PropertyGroup>
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>

After compilation you will notice a new folder that contains the generated artifacts:

Real-world Applications of Source Generators

Now that we understand how to create and use Source Generators, let's explore how they are applied in real-world scenarios:

Use Case 1: Code Serialization

One of the prime use cases for source generators is serialization. Normally, serializers use reflection to understand the structure of the objects they are serializing, which can be quite slow. Source Generators can help by generating the serialization code at compile time, which eliminates the need for reflection and significantly improves performance. The System.Text.Json library in .NET 5.0 and onwards uses this approach.

Use Case 2: INotifyPropertyChanged

For WPF developers, implementing INotifyPropertyChanged interface can be a tedious and error-prone task. With Source Generators, developers can simply annotate properties with an attribute, and the generator would create the required boilerplate code.

Use Case 3: Auto-generating API Clients

If you have an OpenAPI/Swagger specification for your web API, you can use a Source Generator to generate the client-side code for calling the API. This ensures that your API client is always up to date with your API specification, which can help to eliminate runtime errors due to mismatches between the client and the server.

Use Case 4: ORM Mapping Code

In many Object-Relational Mapping (ORM) systems, there's a lot of repetitive code mapping database tables to objects or vice versa. A Source Generator could automate this process, generating the required mapping code based on your database schema or your objects.

Use Case 5: Blazor and Razor Components

Source Generators can also be used in Blazor or Razor components for generating component parameters, route attributes, or even entire components based on certain conditions, thereby reducing the amount of boilerplate code and potential errors.

Wrapping Up

Source Generators are a powerful tool in the .NET ecosystem, allowing us to generate code during the compilation process. This can be a great way to reduce boilerplate and ensure consistent coding practices across your project. But like any powerful tool, they should be used judiciously, as they can make the codebase more difficult to understand for those unfamiliar with the generators in use.

Start experimenting with this feature today and unlock the potential of automating your code-generation tasks! For more information about Source Generators, check out the official documentation or the Roslyn SDK GitHub page.