Roslyn Source Generators: Automating Code Generation in .NET
Source generators represent one of the most exciting frontiers in .NET development. These powerful tools allow you to automatically generate code during the build process, opening up endless possibilities for reducing boilerplate and creating more maintainable applications. While they can be complex to understand initially, the potential they unlock makes them worth the effort to master.
What Are Source Generators?
Source generators are a next-generation advancement over previous code generation approaches that relied on reflection. Instead of generating code at runtime, source generators work during the compilation phase, analyzing your source code and generating additional code that gets compiled alongside your original code.
The Roslyn compiler loads these source generators during the build phase, and they can analyze not just your source code, but anything that’s computable - files on disk, web APIs, databases, and more. The end goal is to automate the creation of additional source code for your application.
Imagine being able to automatically generate full classes with properties, fields, and values based on a spreadsheet. Or creating strongly-typed API clients from OpenAPI specifications. These are just a few examples of what source generators make possible.
Why Source Generators Matter
Source generators are preferred over reflection-based approaches because of their tight integration with the compiler. They work at compile time, which means better performance, better tooling support, and the ability to catch errors early in the development process.
The generated code becomes part of your compiled assembly, so there’s no runtime overhead. This makes source generators perfect for scenarios where you need to generate code based on compile-time information or external data sources.
Creating a Simple Source Generator
Let’s look at a practical example of a source generator that automatically creates ToString methods for classes. This demonstrates the basic concepts while being easy to understand.
To create source generators, you’ll need to add these NuGet packages to your project:
- Microsoft.CodeAnalysis.Common
- Microsoft.CodeAnalysis.CSharp
The project holding the generator is required to target netstandard2.0
<TargetFramework>netstandard2.0</TargetFramework>
Anywhere the generator is referenced it’s required to use a reference like this (note the OutputItemType attribute)
<ItemGroup>
<ProjectReference Include="..\generator\generator.csproj" OutputItemType="Analyzer" />
</ItemGroup>
First, we need a marker attribute that tells our generator which classes to process:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class GenerateToStringAttribute : Attribute { }
Here’s an example class that uses our attribute:
[GenerateToString]
public partial class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Email { get; set; }
}
Next, we create the source generator itself. This is where the magic happens:
[Generator]
public class ToStringGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Step 1: Find classes with our marker attribute
var classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 },
transform: (ctx, _) =>
{
var classSymbol = ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) as INamedTypeSymbol;
bool hasAttribute = classSymbol?.GetAttributes()
.Any(a => a.AttributeClass?.Name == "GenerateToStringAttribute") ?? false;
return hasAttribute ? (ctx.Node as ClassDeclarationSyntax, ctx.SemanticModel) : (null, null);
})
.Where(tuple => tuple.Item1 != null);
// Step 2: Generate code for each selected class
context.RegisterSourceOutput(classDeclarations, (spc, tuple) =>
{
var (classSyntax, semanticModel) = tuple;
var classSymbol = (INamedTypeSymbol) semanticModel.GetDeclaredSymbol(classSyntax);
// Generate the ToString method
string source = $$"""
// <auto-generated/>
namespace
{
partial class
{
public override string ToString() => "Generated ToString for ";
}
}
""";
spc.AddSource($"{classSymbol.Name}.g.cs", source);
});
}
}
How This Generator Works
The generator works in two main steps. First, it scans through all the class declarations in your code, looking for classes that have the GenerateToString
attribute. This is done using the syntax provider, which gives us access to the Roslyn syntax tree.
For each class that has our attribute, the generator creates a partial class with an overridden ToString method. The generated code is added to the compilation as a separate source file, which is why the class needs to be partial.
Testing the Generator
Here’s how you can test that your source generator is working correctly:
public class ToStringGenerator_Tests
{
[Fact]
public void Test1()
{
var p = new Person {
Name = "John",
Age = 30,
Email = "john@example.com"
};
Assert.Equal(p.ToString(), "Generated ToString for Person");
}
}
And here’s a simple console application that demonstrates the generator in action:
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
var p = new Person {
Name = "John",
Age = 30,
Email = "john@example.com"
};
Console.WriteLine(p.ToString());
}
}
When you run this program, you’ll see:
Hello, World!
Generated ToString for Person
The first line comes from our explicit Console.WriteLine
call, and the second line is the output from our automatically generated ToString
method. Notice that even though our Person class has properties like Name
, Age
, and Email
, the generated ToString method returns a simple string with the class name - this is exactly what our source generator creates.
GitHub Repo
Full source code at https://github.com/ryanrodemoyer/DotNetSourceGeneratorExample
The Power of Source Generators
This simple example barely scratches the surface of what’s possible with source generators. You could generate:
- Database access code from entity definitions
- API clients from OpenAPI specifications
- Validation logic from data annotations
- Serialization code from class definitions
- Unit tests from method signatures
- Configuration classes from JSON schemas
The key insight is that source generators can analyze any information available at compile time and generate code based on that analysis. This makes them incredibly powerful for reducing boilerplate and ensuring consistency across your codebase.
Best Practices for Source Generators
When creating source generators, there are several important considerations. Always mark generated code with the // <auto-generated/>
comment so that tools can recognize it as generated code. Use partial classes to allow the generated code to extend existing classes without conflicts.
Make sure your generators are incremental - they should only regenerate code when the relevant source code changes. This is crucial for maintaining good performance in large projects.
Consider the user experience. Source generators should fail gracefully with clear error messages when they encounter unexpected input. Provide good documentation and examples for users of your generator.
Download LINQPad Today
Get started with the most powerful .NET Rapid Progress Tool (RPT) available.
Get LINQPad PremiumPowerful .NET acceleration. Code at the speed of thought.
Testing with LINQPad
LINQPad is an invaluable tool when developing source generators because it lets you directly work with and debug the Roslyn API. Normally, creating a source generator requires setting up multiple projects, writing code, building, debugging, and going through slow iteration cycles. This process can be tedious and time-consuming.
With LINQPad, you can write code that directly interacts with the Roslyn IIncrementalGenerator
interface and related APIs. You can quickly experiment with syntax trees, semantic models, and code generation without the overhead of a full project setup. This allows for rapid prototyping and testing of your generator’s logic before implementing it in a proper source generator project.
For example, you can write code to parse syntax trees, analyze symbols, and generate code strings - all the core functionality your generator will need - and see the results immediately. While you’ll still need to create a proper source generator project eventually, LINQPad makes the development and debugging process much more efficient.
Conclusion
Roslyn source generators represent a powerful new paradigm in .NET development. While they can be complex to implement initially, the possibilities they unlock make them an essential tool for modern .NET developers. From reducing boilerplate code to creating domain-specific languages, source generators can transform how you approach code generation.
The tight integration with the compiler, compile-time execution, and ability to analyze any computable information make source generators a preferred approach over reflection-based solutions. As you explore this technology, you’ll discover new ways to automate repetitive tasks and create more maintainable, consistent code.
Shout Outs
This post was made possible only by the great work from Andrew Lock and his series on source generators.
Please checkout his work and support it by visiting his site at https://andrewlock.net/creating-a-source-generator-part-1-creating-an-incremental-source-generator/
Want to learn more about LINQPad and how it can supercharge your development workflow? Check out my comprehensive LINQPad course where I cover everything from basic queries to advanced scenarios like the ones shown here.