I created a YouTube video on Source Generator in which I showcased one possible implementation. However, I feel that I didn’t fully highlight its capabilities. In this blog post, I aim to demonstrate how to use the Source Generator to automatically create builders for a given class.
Check the YouTube video and the source code here
For our Builder Generator implementation, let’s start from scratch. Create a new class library project, name it SourceGenerator
, add the necessary NuGet package references, and add a new class called AutoBuilderGenerator
as shown in the code snippet below
SourceGenerator.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
</ItemGroup>
</Project>
The EnforceExtendedAnalyzerRules
is required to remove the warning that appears when adding the Generator attribute.
AutoBuilderGenerator class
namespace SourceGenerator;
[Generator]
public class AutoBuilderGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// implementation goes here
}
}
You need to generate a provider instance. This instance uses the context to filter and return only the syntax related to the class definition that has an AutoBuilder
attribute:
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var provider = context.SyntaxProvider.CreateSyntaxProvider(
(node, _) => node is ClassDeclarationSyntax t && t.AttributeLists.Any(x => x.Attributes.Any(a => a.Name.ToString() == "AutoBuilder")),
(syntaxContext, _) => (ClassDeclarationSyntax)syntaxContext.Node
).Where(x => x is not null);
var compilation = context.CompilationProvider
.Combine(provider.Collect());
context.RegisterSourceOutput(compilation, Execute);
}
And now go ahead and create the Execute method:
private void Execute(SourceProductionContext context, (Compilation compilation, ImmutableArray<ClassDeclarationSyntax> classes) tuple)
{
var (compilation, classes) = tuple;
foreach (var syntax in classes)
{
// per class implementation
var symbol = compilation.GetSemanticModel(syntax.SyntaxTree)
.GetDeclaredSymbol(syntax) as INamedTypeSymbol;
}
}
Since we already know that the class has the AutoBuilder
attribute, we can start gathering all the necessary information to construct our generated code.
First, to get the class namespace we can use the following code:
// get the namespace of the current syntax
var syntaxParent = syntax.Parent;
string @namespace = string.Empty;
if (syntaxParent is BaseNamespaceDeclarationSyntax namespaceDeclaration)
{
@namespace = namespaceDeclaration.Name.ToString();
}
Now, let’s generate our boilerplate code. I’m going to use a StringBuilder
to build the class code string.
Note that I’m adding a static implicit operator here. This will be useful when I want to implicitly convert the builder into the desired object.
string prefixCode = $$"""
// <auto-generated />
namespace {{@namespace}};
public class {{symbol!.Name}}Builder
{
protected {{symbol!.Name}} {{symbol!.Name}} = new {{symbol!.Name}}();
public static implicit operator {{symbol!.Name}}({{symbol!.Name}}Builder builder)
{
return builder.{{symbol!.Name}};
}
""";
string suffixCode = """
}
""";
StringBuilder codeBuilder = new StringBuilder();
codeBuilder.AppendLine(prefixCode);
// append code for every properties
codeBuilder.AppendLine(suffixCode);
Now, let’s identify all the properties that have a setter so we can create a WithMethod
for each property:
replace // append code for every properties
with the following
var properties = symbol!.GetMembers()
.OfType<IPropertySymbol>()
.Where(x => x.SetMethod is not null);
foreach (var property in properties)
{
codeBuilder.AppendLine($@" public {symbol!.Name}Builder With{property.Name}({property.Type} {property.Name.ToLower()})");
codeBuilder.AppendLine(" {");
codeBuilder.AppendLine($@" {symbol!.Name}.{property.Name} = {property.Name.ToLower()};");
codeBuilder.AppendLine(" return this;");
codeBuilder.AppendLine(" }");
codeBuilder.AppendLine();
}
We can go a step further by ignoring any property with the AutoBuilderIgnore
attribute using the following code:
foreach (var property in properties)
{
// check if property has AutoBuilderIgnore as attribute
var ignoreAttribute = property.GetAttributes()
.Any(x => x.AttributeClass?.Name == "AutoBuilderIgnoreAttribute");
if (ignoreAttribute)
{
continue;
}
// rest of the properties code here
}
If we have a property with a List
, we might want to generate a method that adds one item instead of a list. I’m using params
here so we can have multiple items, if needed, as parameters.
if (property.Type.ToString().StartsWith("System.Collections.Generic.List<"))
{
var listType = property.Type.ToString().Replace("System.Collections.Generic.List<", "").Replace(">", "");
codeBuilder.AppendLine($@" public {symbol!.Name}Builder Add{property.Name}(params {listType}[] {property.Name.ToLower()})");
codeBuilder.AppendLine(" {");
codeBuilder.AppendLine($@" {symbol!.Name}.{property.Name}.AddRange({property.Name.ToLower()}.ToList());");
codeBuilder.AppendLine(" return this;");
codeBuilder.AppendLine(" }");
codeBuilder.AppendLine();
}
and now register the code in the context and let us try everything
// end of foreach loop
codeBuilder.AppendLine(suffixCode);
context.AddSource($"{symbol!.Name}Builder.g.cs", codeBuilder.ToString());
To test this out, create a new project, add the SourceGenerator
reference, and ensure to specify the OutputItemType
as Analyzer
.
Client.csproj
:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SourceGenerator\SourceGenerator.csproj" OutputItemType="Analyzer" />
</ItemGroup>
</Project>
Now create the following attributes and a class to test the generated builder
// Attributes
public class AutoBuilderAttribute : Attribute
{
}
public class AutoBuilderIgnoreAttribute : Attribute
{
}
[AutoBuilder]
public class Person
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Address { get; set; } = string.Empty;
public List<string> Orders { get; set; } = [];
[AutoBuilderIgnore]
public double Ignored { get; set; }
}
Now, in your Program.cs
, you can start building a Person
using the automatically generated PersonBuilder
class.
var builder = new PersonBuilder()
.WithId(1)
.WithName("John")
.WithAddress("123 Main St")
.AddOrders("My Order", "My Second Order");
Person john = builder; // implicit operator will convert the builder into Person.
Happy Coding!