Source generators are a new feature of .NET 5. You may be interested in writing source generators to help remove boilerplate, improve application performance, or generally perform tasks at compile time rather than at runtime.
This post will show how we can retrieve class declarations from a project with our source generator referenced.
General Overview of Source Generators
A source generator allows developers to “write” code during the compilation process. Using the existing project, we can derive additional assets which we can add to our final artifacts. A source generator has two defining characteristics:
- The
Microsoft.CodeAnalysis .Generator
attribute decorating the class. This attribute causes a project to consider aclass
as source generator. - The
Microsoft.CodeAnalysis.ISourceGenerator
interface, which expects implementations forInitialize
andExecute
.
Let’s look at a simple source generator implementation:
[Generator]
public class HelloWorldGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context) { }
public void Execute(GeneratorExecutionContext context)
{
context.AddSource(
"hello.world",
SourceText.From(@"public class HelloWorld { }", Encoding.UTF8)
);
}
}
The source generator writes a HelloWorld
class into our final assembly, which we can use. As we can see, the type doesn’t do much, but we can reference it.
To utilize generators in your projects, I recommend reading my previous Jumpstart guide on Source Generators.
Using ISyntaxReceiver To Find Class Declarations
When within the Execute
method of our source generator, we can access the context
and traverse the syntax tree of our referencing project. The context has a Compilation
property that holds all the syntax trees in our current project.
var controllers =
context.Compilation
.SyntaxTrees
.SelectMany(syntaxTree => syntaxTree.GetRoot().DescendantNodes())
.Where(x => x is ClassDeclarationSyntax)
.Cast<ClassDeclarationSyntax>()
.Where(c => c.Identifier.ValueText.EndsWith("Controller", StringComparison.OrdinalIgnoreCase))
.ToImmutableList();
That’s “works” but is clunky and gets difficult to manage quickly. We can instead use the ISyntaxReceiver
interface.
public class ControllerFinder : ISyntaxReceiver
{
public List<ClassDeclarationSyntax> Controllers { get; }
= new();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is ClassDeclarationSyntax controller)
{
if (controller.Identifier.ValueText.EndsWith("Controller"))
{
Controllers.Add(controller);
}
}
}
}
To register our ControllerFinder
syntax receiver, we need to update our source generator’s Initialize
method.
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new ControllerFinder());
}
Finally, we can retrieve the class declarations that matched the criteria in our source generator’s Execute
method.
public void Execute(GeneratorExecutionContext context)
{
var controllers =
((ControllerFinder) context.SyntaxReceiver)?.Controllers;
// use controllers to do work...
}
The implementation of ISyntaxReceiver
can store any values, including custom classes and types. By the time we reach the Execute
method, we could have all the metadata required to generate the additional assets.
Here’s the full implementation of our source generator with ISyntaxReceiver
.
[Generator]
public class ControllerGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new ControllerFinder());
}
public void Execute(GeneratorExecutionContext context)
{
var controllers =
((ControllerFinder) context.SyntaxReceiver)?.Controllers;
// use controllers to do work...
}
public class ControllerFinder : ISyntaxReceiver
{
public List<ClassDeclarationSyntax> Controllers { get; }
= new();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is ClassDeclarationSyntax controller)
{
if (controller.Identifier.ValueText.EndsWith("Controller"))
{
Controllers.Add(controller);
}
}
}
}
}
SourceGeneratorsKit NuGet Package
While we could write our own ISyntaxReceiver
implementations, .NET developer Andrii Kurdiumov has been gracious enough to write some great starter implementations in his SourceGeneratorsKit project. The package includes receivers that can discover classes according to the following:
- Finding methods that have attributes with the
MethodsWithAttributeReceiver
. - Finding all classes which implement an interface with
ClassesWithInterfacesReceiver
. - Finding all classes which derive from a well-known type with
DerivedClassesReceiver
.
I recommend looking at this package, SourceGeneratorsKit, as a starting point for your source generator receivers. You might find that these ISyntaxReceiver
implementations are enough for your specific use case.
Conclusion
Dealing with syntax trees can be tricky and confusing. Querying for explicit values in the source generator’s Execute
method can also add bloat to our logic. Using ISyntaxReceiver
can help reduce the noise in our generators, and help us transform SyntaxNode
instances into friendly metadata values, ultimately making a source generator’s Execute
method better.