Source Generators
Building Your First Generator — From Concept to Compile-Time Magic
What if your compiler could write the repetitive code so you don’t have to?
A Source Generator is a C# class that runs during compilation. It receives a view of your source code as a structured model (via the Roslyn API), and it can write additional C# files that get compiled together with the rest of your project.
“Think of it as a code-writing robot that lives inside the compiler. It reads what you wrote, decides what boilerplate should exist, and writes it for you — all before your app ever runs.”
- Pankaj Ikhar, Solution Architect
The generated code is just ordinary C#. It ships in your DLL. No Reflection.Emit, no IL weaving, no runtime surprises.
We’ll build a generator that automatically creates a ToString() override for any class marked with a custom attribute. Classic boilerplate, eliminated forever.
1 Create the analyzer project
A Source Generator must live in its own class library targeting netstandard2.0. It cannot be in the same project it generates code for.// MyGenerators.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Nullable>enable</Nullable>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp"
Version="4.8.0"
PrivateAssets="all" />
</ItemGroup>
</Project>2 Implement IIncrementalGenerator (the modern approach)
Prefer IIncrementalGenerator over the old ISourceGenerator. It only re-runs when relevant parts of the syntax tree actually change — essential for IDE performance.
// ToStringGenerator.cs
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
[Generator]
public class ToStringGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 1. Inject the marker attribute into every compilation
context.RegisterPostInitializationOutput(ctx =>
ctx.AddSource("AutoToStringAttribute.g.cs", AttributeSource));
// 2. Find all classes marked [AutoToString]
var classes = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: (node, _) => node is ClassDeclarationSyntax c
&& c.AttributeLists.Count > 0,
transform: GetClassOrNull)
.Where(m => m is not null);
// 3. Generate code for each found class
context.RegisterSourceOutput(classes, Generate!);
}
}3 Emit your generated source
Get Pankaj Ikhar’s stories in your inbox
Join Medium for free to get updates from this writer.
The Generate method receives the semantic model of the target class and calls context.AddSource() with the code you build.
// Inside ToStringGenerator.cs — Generate method
static void Generate(SourceProductionContext ctx,
ClassDeclarationSyntax cls)
{
var ns = cls.GetNamespace(); // extension helper
var name = cls.Identifier.Text;
var props = cls.Members
.OfType<PropertyDeclarationSyntax>()
.Select(p => p.Identifier.Text);
var parts = String.Join(", ",
props.Select(p => $"{p}={{{p}}}"));
var source = $$"""
namespace {{ns}};
partial class {{name}}
{
public override string ToString()
=> $"{{name}} { {{parts}} }";
}
""";
ctx.AddSource($"{name}.g.cs", source);
}4 Reference the generator from your app project
Analyzer references require OutputItemType=”Analyzer” and ReferenceOutputAssembly=”false”. This tells MSBuild the project is a plugin, not a runtime dependency.
// YourApp.csproj
<ItemGroup>
<ProjectReference
Include="..\MyGenerators\MyGenerators.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>5 Use it in your domain model
Mark the class, keep it partial, and the rest is generated for you on every build.
// Order.cs — your code
[AutoToString]
public partial class Order
{
public Guid Id { get; init; }
public string Customer { get; init; }
public decimal Total { get; init; }
}
// Generated automatically — Order.g.cs:
// public override string ToString()
// => $"Order { Id={Id}, Customer={Customer}, Total={Total} }";Verifying What You Generate
Testing generators is non-negotiable. The community standard is the Microsoft.CodeAnalysis.Testing package combined with CSharpSourceGeneratorTest<T>.
// Typical generator test skeleton
var test = new CSharpSourceGeneratorTest<ToStringGenerator, DefaultVerifier>
{
TestState =
{
Sources = { """
[AutoToString]
public partial class Order
{
public string Name { get; init; }
}
""" },
GeneratedSources =
{
(typeof(ToStringGenerator), "Order.g.cs",
SourceText.From(ExpectedOutput, Encoding.UTF8))
}
}
};
await test.RunAsync();To inspect generated files in Visual Studio or Rider, enable EmitCompilerGeneratedFiles in your csproj. Files appear under obj/generated/ — fully browsable and breakpointable.










