Exploring C# Source Generators

Exploring C# Source Generators

A Guide to using C# Source Generators for Developers

Hello and Welcome back! I took a much needed break to focus on other parts of my life but I am back with a brand new article for this week. We will be exploring C# Source Generators.

C# Source Generators are a feature introduced in .NET 5 that allow you to generate C# code during the compilation process. Instead of manually writing repetitive or boilerplate code, Source Generators can automatically create this code for you, making your development faster and reducing errors.

Pre-requisites

To fully benefit from this article, readers should have the following prerequisites:

  • Basic Knowledge of C#
    You should be familiar with the fundamentals of the C# language, such as syntax, object-oriented programming principles, and common features like classes, methods, and properties.

  • Familiarity with .NET Development
    Understanding how to work with .NET projects (specifically .NET 5 or later), including creating and managing console applications or libraries in Visual Studio or any other IDE.

  • Understanding of Compilers
    A basic idea of what compilers do and how source code is compiled into executable programs will help in grasping the concepts of source generators and Roslyn.

  • Experience with NuGet Packages
    You should know how to install and manage NuGet packages in .NET projects, as source generators often require using or referencing specific packages.

  • Familiarity with Visual Studio or VS Code
    Knowledge of navigating an IDE like Visual Studio or Visual Studio Code, setting up projects, and running them is essential for implementing and testing source generators.

  • Introduction to Roslyn (Optional but Beneficial)
    While the guide will introduce Roslyn, having a basic understanding of what Roslyn is and how it serves as a compiler for C# will provide additional context.

Table of Contents

  • Introduction to C# Source Generators

  • Understanding Roslyn and the Compiler API

  • Setting Up Your First Source Generator

  • Deep Dive: Creating Custom Source Generators

  • Real-World Use Cases for Source Generators

  • Best Practices and Pitfalls

  • Performance Considerations

  • Testing and Validation of Source Generators

  • Conclusion and Further Learning

Introduction to C# Source Generators

What are Source Generators?
C# Source Generators are a feature introduced in .NET 5 that allow you to generate C# code during the compilation process. Instead of manually writing repetitive or boilerplate code, Source Generators can automatically create this code for you, making your development faster and reducing errors.

How do Source Generators work within C#?
Source Generators plug into the compiler (via the Roslyn compiler) and inspect your existing code. Based on what it finds, it can generate additional code and include it as part of your project when it compiles. For example, if you have a class that needs repetitive methods, a Source Generator can automatically create those methods when you build your project.

Benefits of using Source Generators

  • Eliminate Boilerplate Code: You don't have to repeat code for common patterns. The generator takes care of that.

  • Improved Performance: Since the generated code is part of the compilation process, you don’t suffer runtime performance hits for generating or using the code.

  • Increased Productivity: You spend less time on writing repetitive code and more time on the core logic of your application.

  • Compile-Time Validation: Because the code is generated at compile time, errors in the generated code are caught early during compilation rather than at runtime.

Understanding Roslyn and the Compiler API

Overview of the Roslyn Compiler

Roslyn is the name of the C# compiler, which is responsible for turning the code you write into something the computer can understand and run. But Roslyn isn’t just a typical compiler—it’s a "compiler as a service." This means that, along with compiling code, Roslyn also provides tools (APIs) that allow developers to interact with the code during the compilation process. It lets us analyze, understand, and even generate new code while the project is being built.

Role of the Roslyn API in Source Generators

The Roslyn API is what makes source generators possible. Through this API, we can access all the details about the code that’s being compiled—like class names, methods, and properties. This allows us to examine the structure of the code and even create new code based on what we find. For example, if we wanted to automatically generate code for repetitive tasks, the Roslyn API would help us figure out where and how to inject that generated code.

How Source Generators Leverage the Compiler

Source Generators plug into the Roslyn compiler. They run during the build process and have access to the project’s source code. Here’s what they do:

  1. Analyze the Code: The generator can look at your code structure. For example, it can find all classes marked with a specific attribute.

  2. Generate New Code: Based on what it finds, the generator can automatically create additional code. For instance, it could generate a ToString() method for every class, saving you time and effort.

  3. Add the Code to the Project: This generated code is treated as if you wrote it yourself, and it becomes part of your final application.

In short, Roslyn’s compiler and API provide the power behind source generators, allowing them to analyze and create new code during the build process, making development faster and more efficient.

Setting Up Your First Source Generator

Source generators are tools that allow you to generate code during compilation, which helps reduce boilerplate code and streamline development. Let's walk through the steps to set up and create a simple source generator in C#.

Project Setup and Prerequisites

  1. Install Visual Studio or Visual Studio Code
    You'll need an IDE, such as Visual Studio (recommended) or VS Code, with the .NET SDK installed.

  2. Create a New Solution
    Open Visual Studio and create a new solution with two projects:

    • Class Library for the source generator.

    • Console App to test the generated code.

Steps:

  1. Create a Class Library for the Source Generator
    In Visual Studio:

    • File > New > Project > Class Library (.NET Standard or .NET Core)

    • Name it something like MySourceGenerator.

  2. Add Required NuGet Package
    Add the Microsoft.CodeAnalysis.CSharp package to your class library:

    • Right-click on your project > Manage NuGet Packages > Search for Microsoft.CodeAnalysis.CSharp and install it.

Writing a Simple Source Generator

A source generator runs during the compile time, scanning your code and generating new source code. Let’s create a simple one that automatically generates a HelloWorld class.

  1. Implement the Source Generator

    Create a class called MyGenerator that implements the ISourceGenerator interface:

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

[Generator]
public class MyGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        // Optional initialization code
    }

    public void Execute(GeneratorExecutionContext context)
    {
        // The source code to be generated
        string generatedCode = @"
            namespace GeneratedNamespace
            {
                public static class HelloWorld
                {
                    public static void SayHello()
                    {
                        Console.WriteLine(""Hello from Source Generator!"");
                    }
                }
            }";

        // Add the generated code to the compilation
        context.AddSource("HelloWorldGenerator", SourceText.From(generatedCode, Encoding.UTF8));
    }
}
  • The Execute method is where the source generator creates code. In this example, it generates a HelloWorld class inside a GeneratedNamespace.
  1. Register the Source Generator

    In the project’s .csproj file, add the following line inside the <PropertyGroup> to ensure it runs as a generator:

<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>

Registering and Building Your Source Generator

  1. Add a Console App to Test the Source Generator

    Create a new console application in the same solution to test the source generator:

    • File > New > Project > Console App (.NET Core)
  2. Reference the Source Generator in the Console App

    Add a reference to the source generator class library:

    • Right-click on the Console App project > Add > Project Reference > Select MySourceGenerator.
  3. Use the Generated Code in the Console App

    Now, in the Program.cs file of your Console App, call the generated HelloWorld.SayHello method:

using System;
using GeneratedNamespace;

class Program
{
    static void Main(string[] args)
    {
        HelloWorld.SayHello(); // Calls the method generated by the Source Generator
    }
}
  1. Build and Run

    • Build and run the console app. You should see the output:
    Hello from Source Generator!

Deep Dive: Creating Custom Source Generators

Anatomy of a Source Generator Class

A Source Generator in C# is essentially a class that implements the ISourceGenerator interface. This interface has two main methods:

  1. Initialize: This is called once, at the start of the compilation process. You can use it to set up anything your generator might need.

  2. Execute: This is where the magic happens—it's called during the compilation process, and this is where you generate new code.

Here’s what a basic Source Generator class looks like:

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

[Generator]
public class HelloWorldGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        // Initialization logic if needed (e.g., registering syntax receivers)
    }

    public void Execute(GeneratorExecutionContext context)
    {
        // Code to generate new source files during compilation
        var sourceCode = @"
            namespace GeneratedCode
            {
                public class HelloWorld
                {
                    public static string SayHello() => ""Hello, World from Source Generator!"";
                }
            }
        ";

        // Add the generated code to the compilation
        context.AddSource("HelloWorldGenerator", SourceText.From(sourceCode, Encoding.UTF8));
    }
}

Emitting Source Code During Compilation

In the Execute method, you create new code in the form of a string, and then emit it into the project using context.AddSource. This generated code is compiled alongside the rest of your project.

In the example above, we are generating a new class called HelloWorld with a static method SayHello() that returns a string.

To emit this generated code, you:

  1. Write the source code as a string.

  2. Add it to the compilation with context.AddSource.

Practical Example and Code Walkthrough

  1. Create a New Project
    To get started, create a new Class Library project in Visual Studio or Visual Studio Code. Add the Microsoft.CodeAnalysis.CSharp NuGet package, which gives you access to the tools needed for source generation.

  2. Write the Source Generator
    Create a new class that implements the ISourceGenerator interface (like the HelloWorldGenerator example above). This class will generate a new file during compilation.

  3. Use the Generated Code
    Once the generator is set up, you can use the generated code in your project. For instance, you can call the HelloWorld.SayHello() method in your application:

using System;

class Program
{
    static void Main()
    {
        // Calling the method generated by the source generator
        Console.WriteLine(GeneratedCode.HelloWorld.SayHello());
    }
}

Explanation

  • GeneratorInitializationContext: In the Initialize method, you can register syntax receivers, which helps if you need to analyze existing code to generate new code. In simple generators, you might not need this.

  • GeneratorExecutionContext: This is the context in which you add your generated code. The AddSource method is used to inject new code during the build process.

Key Takeaways

  • Source Generators allow you to programmatically generate C# code at compile-time, reducing boilerplate and improving performance.

  • The basic structure involves a class that implements the ISourceGenerator interface.

  • You can generate classes, methods, or other types of C# code dynamically by emitting source code strings during the build process.

Real-World Use Cases for Source Generators

Source Generators are powerful tools that automatically generate code during the build process, helping developers avoid writing repetitive code, improving performance, and integrating with various libraries. Let’s look at three common use cases with easy-to-understand examples.

Auto-generating Boilerplate Code

Source Generators can automatically generate repetitive, boilerplate code that developers often need to write manually, such as properties, data classes, or DTOs (Data Transfer Objects).

Example: Imagine you have many classes where you manually implement ToString methods. Instead, you can use a source generator to automatically generate them for all your classes.

Here’s how it works:

  • Without source generator (manual implementation):

      public class Person
      {
          public string FirstName { get; set; }
          public string LastName { get; set; }
    
          public override string ToString()
          {
              return $"{FirstName} {LastName}";
          }
      }
    
  • With source generator (automatic generation):

      [AutoToString] // This attribute triggers the source generator
      public class Person
      {
          public string FirstName { get; set; }
          public string LastName { get; set; }
      }
    

    The generator automatically creates the ToString method during compile time, saving you from writing it yourself!

Performance Optimizations

Source Generators can also improve performance by generating optimized code at compile time, rather than relying on reflection or runtime code generation, which can be slow.

Example: If you need to create mappings between objects (like copying data from one object to another), source generators can generate this code more efficiently than relying on reflection or manual mapping.

  • Without a source generator:

      public void Map(Person source, PersonDto destination)
      {
          destination.FirstName = source.FirstName;
          destination.LastName = source.LastName;
      }
    
  • With a source generator: The source generator can automatically create this mapping method for all your classes, avoiding manual effort and improving performance since the code is generated at compile time.

Integrating with Libraries and Frameworks

Source Generators can enhance libraries and frameworks by generating code that helps integrate them smoothly into your projects.

Example: Consider a JSON serialization library that needs to serialize your classes. Instead of manually writing serializers, a source generator can automatically generate the necessary code for serialization.

[JsonSerializable] // This attribute triggers the source generator to create serialization code
public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

In this case, the source generator creates the Serialize and Deserialize methods, integrating seamlessly with the JSON library without you needing to write any additional code.

Conclusion

Source Generators help by:

  • Reducing the need for repetitive, boilerplate code.

  • Improving performance by generating code at compile time.

  • Integrating with libraries or frameworks to make your code cleaner and easier to manage.

Best Practices and Pitfalls

When to Use (and When Not to Use) Source Generators

When to use:

  • Eliminating repetitive code: If you're constantly writing the same code (like boilerplate code for logging, data models, or validation), source generators can automate that process.

  • Improving performance: Source generators can optimize the build process by generating code at compile-time instead of runtime, making your application more efficient.

  • Enhancing existing code: Source generators are useful for adding features like code analysis, code completion, or automatic updates without changing your actual source code.

When not to use:

  • For complex business logic: Source generators should not handle complex logic that can change often. They are better suited for tasks that stay consistent over time.

  • When it adds unnecessary complexity: If using a source generator adds too much complexity or makes the code harder to understand, it might be better to write the code manually.

Debugging and Troubleshooting Source Generators

Debugging source generators can be tricky, but here’s a simple approach:

  1. Log output: Source generators can log messages to the Visual Studio build output to help with debugging. Here’s a code sample for how to log in a source generator:

     using Microsoft.CodeAnalysis;
     using Microsoft.CodeAnalysis.Text;
     using System.Text;
    
     [Generator]
     public class SimpleGenerator : ISourceGenerator
     {
         public void Execute(GeneratorExecutionContext context)
         {
             // Logging a message to the build output
             context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor("SG001", "Info", "Source Generator Executed", "Usage", DiagnosticSeverity.Info, true), Location.None));
    
             var sourceCode = @"
             namespace HelloWorld
             {
                 public static class Program
                 {
                     public static void Main()
                     {
                         System.Console.WriteLine(""Hello from generated code!"");
                     }
                 }
             }";
    
             context.AddSource("HelloWorldGenerated", SourceText.From(sourceCode, Encoding.UTF8));
         }
    
         public void Initialize(GeneratorInitializationContext context)
         {
         }
     }
    
  2. Test the output: You can check the generated code by going to Visual Studio's Solution Explorer. Under the Analyzer node, you can inspect the generated source code and make sure it looks like what you intended.

Common Mistakes and How to Avoid Them

  • Not managing dependencies properly: If your generator depends on external libraries, make sure those libraries are properly referenced in your project. Generators should have minimal dependencies to keep things simple.

  • Generating too much code: Be careful not to generate unnecessary code, as it can slow down the build process. Stick to generating only what is required.

  • Forgetting about threading: Remember that source generators are executed at compile-time and may run in parallel. Avoid using stateful operations that can cause race conditions or conflicts.

  • Not handling edge cases: Ensure that your generator checks for edge cases like null values or invalid input to avoid generating faulty code.

    Example of handling an edge case:

      public void Execute(GeneratorExecutionContext context)
      {
          if (context.SyntaxReceiver is not MySyntaxReceiver receiver)
              return;
    
          // Generate code only if certain conditions are met
          if (receiver.NodesToProcess.Any())
          {
              // Add source generation logic here
          }
      }
    

Following these best practices will help you avoid common pitfalls and use source generators effectively in your projects.

Performance Considerations

When using C# source generators, it's essential to understand how they can impact your build process and how to optimize their performance. Below is a simple walkthrough to help you understand better:

Impact of Source Generators on Build Time

Source generators run during the compilation process to generate additional source code. This can affect the build time of your application. Here’s a brief overview:

  • Initial Build Time: When you add a source generator to your project, it can increase the time it takes to build the project for the first time. This is because the generator needs to create the additional code before the compilation is complete.

  • Subsequent Builds: For incremental builds, the impact might be less, but the generator will still run to check if any code needs to be regenerated based on changes.

Example: Imagine you have a source generator that creates boilerplate code for data models. During the first build, the generator processes all your models and generates code. This might take a bit longer, but subsequent builds should be faster if only a few models have changed.

Code Sample:

[Generator]
public class ExampleSourceGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        // Initialization logic (if needed)
    }

    public void Execute(GeneratorExecutionContext context)
    {
        // Code generation logic here
        // This method is called each time you build your project
    }
}

Optimizing Source Generators for Performance

To ensure your source generators don’t negatively impact build performance, consider the following tips:

  1. Minimize Work During Generation:

    • Perform only necessary operations in the Execute method. Avoid complex computations and keep the logic simple.
  2. Use Caching:

    • Cache results of expensive operations or reuse data to avoid recalculating it multiple times.
  3. Generate Only When Needed:

    • Ensure your generator runs only when there are changes that necessitate code regeneration. This helps avoid unnecessary work.
  4. Optimize Data Access:

    • If your generator needs to access external data, make sure this access is efficient. For example, use in-memory data structures rather than querying external services repeatedly.

Example: Here’s a refined version of the previous generator that includes caching to reduce repeated work:

Code Sample:

[Generator]
public class OptimizedSourceGenerator : ISourceGenerator
{
    private static readonly Dictionary<string, string> CachedResults = new();

    public void Initialize(GeneratorInitializationContext context)
    {
        // Initialization logic (if needed)
    }

    public void Execute(GeneratorExecutionContext context)
    {
        // Check if the result is already cached
        if (CachedResults.TryGetValue("key", out var cachedCode))
        {
            context.AddSource("GeneratedCode", cachedCode);
            return;
        }

        // Perform code generation logic
        var generatedCode = GenerateCode();
        CachedResults["key"] = generatedCode;

        context.AddSource("GeneratedCode", generatedCode);
    }

    private string GenerateCode()
    {
        // Example code generation logic
        return "public class GeneratedClass { }";
    }
}

By understanding and applying these performance considerations, you can make sure your source generators enhance your development process without causing significant delays in build times.

Testing and Validation of Source Generators

When creating source generators, it's crucial to test them to ensure they work correctly and perform efficiently. Here’s a simple walkthrough to help you test and validate your source generators, along with some code samples.

Unit Testing Your Source Generator Code

Unit testing ensures that your source generator behaves as expected. You can use the xUnit testing framework to create tests for your source generator. Here’s a simple example:

  1. Create a Test Project: In your solution, add a new test project (e.g., SourceGeneratorTests).

  2. Add Required Packages: Install the Microsoft.CodeAnalysis.CSharp and Microsoft.CodeAnalysis.CSharp.Workspaces NuGet packages, which are needed to work with Roslyn and test source generators.

  3. Write a Test Case:

     using Microsoft.CodeAnalysis;
     using Microsoft.CodeAnalysis.CSharp;
     using Microsoft.CodeAnalysis.CSharp.Syntax;
     using Xunit;
    
     public class MySourceGeneratorTests
     {
         [Fact]
         public void TestMySourceGenerator()
         {
             // Arrange
             var syntaxTree = CSharpSyntaxTree.ParseText(@"
                 public class MyClass
                 {
                     public void MyMethod() { }
                 }");
    
             var compilation = CSharpCompilation.Create("TestCompilation", new[] { syntaxTree });
             var generator = new MySourceGenerator();
    
             // Act
             var context = new GeneratorExecutionContext(); // Mock or real context
             generator.Initialize(context);
             generator.Execute(context);
    
             // Assert
             // Check if the generated code is correct
         }
     }
    

Ensuring Accuracy and Performance

  • Accuracy: Verify that your source generator produces the expected code. Compare the generated code against predefined expectations or known outputs.

  • Performance: Measure the impact of your source generator on build times. Ensure it doesn’t significantly slow down the compilation process.

Tools for Testing and Verifying Generated Code

  • Roslyn: Use Roslyn APIs to inspect the syntax trees and semantic models of the generated code. This helps verify that the generated code meets your expectations.

  • Code Analysis Tools: Tools like Roslyn Analyzers can help analyze the generated code for any issues.

  • Benchmarking: Use benchmarking tools to measure the performance of your source generator. For example, the BenchmarkDotNet library can help you evaluate the impact on build time.

Example of Verifying Generated Code

Here’s a simple example of how you might verify the output of your source generator:

public void VerifyGeneratedCode(string expectedCode)
{
    // Get the generated code as a string
    string generatedCode = GetGeneratedCode();

    // Compare with expected code
    Assert.Equal(expectedCode, generatedCode);
}

private string GetGeneratedCode()
{
    // Method to retrieve or generate code for comparison
    return "";
}

By following these steps, you’ll ensure that your source generator produces accurate and efficient code, and you’ll be better equipped to handle any issues that arise.

Conclusion and Further Learning

Recap of Key Concepts

We’ve explored the basics of C# Source Generators, including what they are, how they work, and how to create your own. We discussed setting up your first source generator, how to make custom ones, and when to use them effectively. We also touched on performance considerations and best practices for working with source generators.

Resources for Further Learning

To continue building your knowledge, check out these resources:

  • Articles: Look for tutorials and articles on C# source generators for practical examples and advanced tips.

  • Documentation: Review the official Microsoft documentation on Roslyn to get detailed technical insights.

  • Tutorials: Find step-by-step tutorials and videos online that walk you through creating and using source generators in real projects.

These resources will help you deepen your understanding and stay updated on new developments in C# and source generation. I hope you found this guide helpful and learned something new. Stay tuned for the next article in the Mastering C# series: Building High-Performance Applications with C#

Happy learning!