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:
Analyze the Code: The generator can look at your code structure. For example, it can find all classes marked with a specific attribute.
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.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
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.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:
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
.
Add Required NuGet Package
Add theMicrosoft.CodeAnalysis.CSharp
package to your class library:- Right-click on your project > Manage NuGet Packages > Search for
Microsoft.CodeAnalysis.CSharp
and install it.
- Right-click on your project > Manage NuGet Packages > Search for
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.
Implement the Source Generator
Create a class called
MyGenerator
that implements theISourceGenerator
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 aHelloWorld
class inside aGeneratedNamespace
.
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
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)
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
.
- Right-click on the Console App project > Add > Project Reference > Select
Use the Generated Code in the Console App
Now, in the
Program.cs
file of your Console App, call the generatedHelloWorld.SayHello
method:
using System;
using GeneratedNamespace;
class Program
{
static void Main(string[] args)
{
HelloWorld.SayHello(); // Calls the method generated by the Source Generator
}
}
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:
Initialize: This is called once, at the start of the compilation process. You can use it to set up anything your generator might need.
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:
Write the source code as a string.
Add it to the compilation with
context.AddSource
.
Practical Example and Code Walkthrough
Create a New Project
To get started, create a new Class Library project in Visual Studio or Visual Studio Code. Add theMicrosoft.CodeAnalysis.CSharp
NuGet package, which gives you access to the tools needed for source generation.Write the Source Generator
Create a new class that implements theISourceGenerator
interface (like theHelloWorldGenerator
example above). This class will generate a new file during compilation.Use the Generated Code
Once the generator is set up, you can use the generated code in your project. For instance, you can call theHelloWorld.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:
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) { } }
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:
Minimize Work During Generation:
- Perform only necessary operations in the
Execute
method. Avoid complex computations and keep the logic simple.
- Perform only necessary operations in the
Use Caching:
- Cache results of expensive operations or reuse data to avoid recalculating it multiple times.
Generate Only When Needed:
- Ensure your generator runs only when there are changes that necessitate code regeneration. This helps avoid unnecessary work.
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:
Create a Test Project: In your solution, add a new test project (e.g.,
SourceGeneratorTests
).Add Required Packages: Install the
Microsoft.CodeAnalysis.CSharp
andMicrosoft.CodeAnalysis.CSharp.Workspaces
NuGet packages, which are needed to work with Roslyn and test source generators.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!