Exploring Source Generators in C#: Real-World Examples
Tired of writing the same class scaffolding over and over? Annoyed by how much time you spend on "glue code" instead of actual features? You're not alone. Imagine if the C# compiler does this on your behalf every time, perfectly. Let me introduce you to C# code source generators, a powerful feature that helps automate the generation of boilerplate code at compile time. If you don't know much about this feature, don't worry, we will dive into how it can ace your code.

What Are C# Source Generators?
Introduced in C# 9, the source generator feature allows developers to generate source code during the compilation process. You can reduce repetitive code by automating with source generators, achieving a faster pace and less error-prone code.
Unlike other metaprogramming techniques, such as reflection or IL weaving, which operate at runtime, Source Generators are injected into the compiler and generate code before the final output is built. C#12 introduced key changes to the source generator, including minimising boilerplate code while extending functionalities to support complex code generation.
What is Roslyn?
Roslyn is the compiler for C# and the Visual Basic .NET compiler, which converts source code into a low-level code called CIL (Common Intermediate Language). Beyond that, Roslyn provides tools (APIs) for developers to analyse, understand and generate the code during the compilation process.
How does the C# source generator work?

Source Generators are a Roslyn compiler component that integrates into the compilation pipeline. They analyse existing code and generate additional source code files with the following steps.
- Source generators inspect your existing code with syntax trees and understand the meaning of the code using semantic models.
- Generate new code programmatically and add it to the main code for compilation. The generated output is identical to what you would obtain if you were to do it manually.
Advantages of C# Source Generation
One of the key benefits you gain is reduced time and effort spent writing similar code. You can utilise the same energy on other crucial aspects of the project. The compiler identifies errors in the generated code, leading to more robust applications. Since code generation occurs within the application during build, it incurs no runtime cost.
Example 1: A Console greeting
I am choosing a simple example of a code generator that prints a console message.
dotnet new classlib -n ESourceGenerator
Generator project should be .netstandard 2.0
In the project’s .csproj
file, add the following line to the <PropertyGroup>
to ensure it runs as a generator
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
Just to confirm everything is on track, your Generator project's csproj should look like

Step 2: Install required packages
We need a couple of NuGet packages for the generator functionalities
dotnet add package Microsoft.CodeAnalysis.CSharp
dotnet add package Microsoft.CodeAnalysis.Analyzers
Step 3: Write the generator code
.NET library project creates a Class1.cs by default, we will rename it HelloSayenGenerator.cs and put our code
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;
namespace ESourcerGenerator;
[Generator]
public class HelloSayenGenerator: ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
// Register any callbacks here
// We specify here the code requirements from the compiler
}
public void Execute(GeneratorExecutionContext context)
{
// Create the source code to inject
string sourceCode = @" using System; namespace HelloSayenGenerated { public static class HelloSayen { public static void GreetSayen() => Console.WriteLine(""Hi from the Super Sayen!""); } }";
// Add the source code to the compilation
context.AddSource("HelloSayenGenerated", SourceText.From(sourceCode, Encoding.UTF8));
}
}
Let's disclose some code jargon here. We implemented the HelloSayenGenerator class using the ISourceGenerator interface, which is the base interface required for a source generator. You can see one [Generator] attribute hovering over, specifying that the following class is a source generator that provides C# sources. In the Execute method, context calls the AddSource method, accepting a name and the sourceCode string storing the code. The HelloSayenGenerated is the context that calls the AddSource method, accepting a name and the sourceCode string, which stores the name of the generated namespace that Roslyn will use internally during compilation.
Step 4: Build the generator project
dotnet build
Step 5: Add a consumer of the generator
Now we are adding one consumer project named SCConsumer
dotnet new console -n SCConsumer
Step 5: Reference the generator project in SCConsumer
dotnet add reference ..\ESourcerGenerator\bin\Debug\netstandard2.0\ESourcerGenerator.dll
However, doing this will add a project reference, just as it does for any library. However, we are working with a generator. So, change the
<ProjectReference Include="..\ESourcerGenerator\bin\Debug\netstandard2.0\ESourcerGenerator.dll" />
to
<Analyzer Include="..\ESourcerGenerator\bin\Debug\netstandard2.0\ESourcerGenerator.dll" />
Step 6: Use the generator in the Program.cs
using HelloSayenGenerated;
HelloSayen.GreetSayen();
Step 7: Test and run

We have successfully created a printing code by using a C# generator. However, we have made the entire source code available in the library. In actual scenarios, we will write less and generate more. That is the purpose of code generation. For that, move to our second example.
Example 2: Code generator for the serialisation of objects into JSON
In this example, we will work with a real-world scenario of converting the class objects into JSON.
Step 1: Define the generator class
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
namespace ESourcerGenerator;
[Generator]
public class CustomSerializationGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
// No initialization logic required for now
}
public void Execute(GeneratorExecutionContext context)
{
var serializableAttr = context.Compilation.GetTypeByMetadataName("System.SerializableAttribute");
if (serializableAttr == null) return;
foreach (var syntaxTree in context.Compilation.SyntaxTrees)
{
var semanticModel = context.Compilation.GetSemanticModel(syntaxTree);
var classDeclarations = syntaxTree.GetRoot().DescendantNodes()
.OfType<Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntax>();
foreach (var classDecl in classDeclarations)
{
var classSymbol = semanticModel.GetDeclaredSymbol(classDecl) as INamedTypeSymbol;
if (classSymbol == null) continue;
if (!classSymbol.GetAttributes().Any(attr =>
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, serializableAttr)))
continue;
var namespaceName = classSymbol.ContainingNamespace.ToDisplayString();
var className = classSymbol.Name;
var properties = classSymbol.GetMembers()
.OfType<IPropertySymbol>()
.Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic)
.ToList();
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated>");
sb.AppendLine($"namespace {namespaceName}.Generated");
sb.AppendLine("{");
sb.AppendLine($" public static class {className}Extensions");
sb.AppendLine(" {");
sb.AppendLine($" public static string ToJson(this {className} obj)");
sb.AppendLine(" {");
sb.AppendLine(" var sb = new System.Text.StringBuilder();");
sb.AppendLine(" sb.Append(\"{\");");
for (int i = 0; i < properties.Count; i++)
{
var prop = properties[i];
var comma = i < properties.Count - 1 ? "," : "";
sb.AppendLine($" sb.Append(\"\\\"{prop.Name}\\\": \\\"\" + obj.{prop.Name} + \"\\\"{comma}\");");
}
sb.AppendLine(" sb.Append(\"}\");");
sb.AppendLine(" return sb.ToString();");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine("}");
context.AddSource($"{className}Extensions.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8));
}
}
}
}
Step 2: Build the project
dotnet build
Step 3: Define classes for the consumer to test the functionality
Blog class
namespace SCConsumer.Models;
[Serializable]
public class Blog
{
public Guid Id { get; set; }
public string Title { get; set; }
}
Course class
namespace SCConsumer.Models;
[Serializable]
public class Course
{
public Guid Id { get; set; }
public string Name { get; set; }
}
Step 4: Call the generated methods in the Program.cs
using Guid = System.Guid;
using SCConsumer.Models;
using SCConsumer.Models.Generated;
var blog = new Blog
{
Id = Guid.NewGuid(),
Title = "Elmah.io Blog"
};
var course = new Course
{
Id = Guid.NewGuid(),
Name = "Intro to Source Generators"
};
Console.WriteLine("Serialized Blog:-");
Console.WriteLine(blog.ToJson());
Console.WriteLine("\nSerialized Course:-");
Console.WriteLine(course.ToJson());

With the C# generator, we have designed a model-to-JSON parser that can handle any type of class without requiring separate parsing logic for each type. You can notice how this feature significantly reduces the code.
Best Practices for Using Source Generators
Although source generators are a great tool to upscale maintainability and efficiency, you should consider their use in a meaningful way.
- Performance Considerations
While source generators downsize development time, they may incur compile-time overhead. Prefer manual code in projects where build performance matters more than automation.
- Code consistency and Maintainability
Ensure that the generated code is clear and maintainable. For better maintainability, the generated code should adhere to the same coding standards as the rest of your project. Introduce documentation or comments in the generated code to aid future maintenance.
- Debugging and Diagnostics
In C#12, source generators include enhanced diagnostic capabilities to provide clear error and warning messages. Consider using these features to ease debugging of the output code. Add meaningful logs during the code generation.
Conclusion
Writing repetitive code is a productivity killer. Whether it’s generating serializers, wrappers, or mapping logic, these tasks are ripe for automation. C# filled that pitfall with a remarkable feature of the source generator. They enable developers to produce boilerplate code at compile time based on existing code.
In this post, we showed the actual implementation of C# Source Generators. Utilise this powerful Roslyn component to enhance your productivity and performance.
elmah.io: Error logging and Uptime Monitoring for your web apps
This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.
See how we can help you monitor your website for crashes Monitor your website