The implementation of a source generator
As a software developer, I often have to do things I don’t really enjoy: monotonous routine tasks that are constantly repeated and leave no room for creativity. In my private life, this is unavoidable; after all, someone has to empty the dishwasher regularly. Fortunately, things are different in my job. Here, I can have these tasks taken off my hands, and it’s actually really fun.
One solution for this is source generators. These are extensions for the C# compiler Roslyn that you can write yourself or integrate into your project as NuGet packages. During compilation, they can automatically generate additional source code, regardless of the IDE used. So instead of writing the same classes or methods over and over again by hand, I simply let the compiler generate these building blocks.
At this point, I could go into detail about the technical background, the motivation and the possible use cases of source generators. However, that is not the focus here. Instead, I would like to show you how to build your own source generator, what stumbling blocks you may encounter and how best to overcome them. As a concrete example, I have decided to automatically generate the validation methods for properties of a class based on defined attributes. Join me as we create our first generator step by step.
Setting up the sample project
For our example, we will create a new solution called SourceGeneratorDemo in our IDE (I am using Visual Studio). Within the solution, we will create two projects:
- MyApplication – a console application for which we will generate our code later.
- SourceGenerators – a class library that will contain our actual source generator.
In the MyApplication project, we first create a class called Person. This must be declared as partial because its validation methods will later be generated in a separate file, but still be part of the Person class. The class contains two properties whose validation rules we define using the Required and MaxLength attributes:
- Name – a mandatory field that can contain a maximum of 50 characters.
- Occupation – an optional field limited to 100 characters.
This structure forms the basis on which our source generator will be built. The screenshot shows what the project structure and the Person class currently look like:
Figure: Project structure for implementing a source generator
Choosing the source generator
Before we start implementing our source generator, it is worth taking a quick look at the two available variants: the classic source generator and the incremental generator. [1]
1. Source generator
A source generator (ISourceGenerator) completely regenerates the entire code with each build. Regardless of which parts of the project have changed, everything is processed again. This approach is easy to implement, but can quickly lead to longer build times for larger projects.
2. Incremental generator
Incremental generators (IIncrementalGenerator) work in a differentiated way: they recognise which parts of the project have changed and only regenerate the corresponding code modules. This saves a considerable amount of time during builds, especially in larger projects.
For our example project, we use an incremental generator; this allows us to benefit in the long term from shorter build times and better performance in a scalable application.
Implementation of the source generator – basic structure
We now know that we want to generate validation functions and have decided to work with an incremental generator. So let’s start developing our generator right away and lay the foundation for our automatically generated code.
Before we begin with the actual implementation, we need to install the NuGet package Microsoft.CodeAnalysis.CSharp in the SourceGenerators project. It provides all the necessary tools and APIs we need to create our generator.
Now we create a class called ValidationGenerator in the SourceGenerators project, which is responsible for generating our validation methods. It must implement the IIncrementalGenerator interface from the Microsoft.CodeAnalysis namespace. The [Generator] attribute is also required so that the compiler actually treats the class as a source generator. Only the combination of interface and attribute ensures that our generator is executed during the build.
[Generator]
public class ValidationGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
}
}
- Finding the relevant code – for example, all properties of a class.
- Examine the parts found – for example, whether a property has attributes such as [Required] or [MaxLength].
- Register the new code – i.e. return the automatically generated validation methods to the compiler.
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var propertyDeclarations = context
.SyntaxProvider.CreateSyntaxProvider(
predicate: (node, _) => node is PropertyDeclarationSyntax, // Filter properties only
transform: (ctx, _) => (Property: (PropertyDeclarationSyntax)ctx.Node, Model: ctx.SemanticModel)
)
.Where(x => x.Property != null);
var allProperties = propertyDeclarations.Collect();
context.RegisterSourceOutput(allProperties, GenerateValidationMethods);
}
Let’s take a look at the individual steps:
1. Only consider properties
With CreateSyntaxProvider, we specify that we are only interested in property declarations (i.e. properties in classes). Everything else in the code – methods, fields or events – is ignored.
2. Examine properties more closely
Next, we note down the important meta information provided by the compiler via a SemanticModel for each property found. This includes attributes such as [Required] or [MaxLength], which we will need later to generate the validation methods.
3. Collect all properties
Using Collect(), we bundle all the properties found into a single collection. This allows us to pass them all at once to RegisterSourceOutput later and generate the validation methods from them without having to process each property individually.
4. Generate code
Finally, we tell the generator: ‘Please take this list of properties and write source code for them.’ To do this, we call our GenerateValidationMethods method, which will later generate the appropriate validation methods for each property.
This gives us the basic structure of our source generator: find properties in the code, extract the relevant information from them, and then automatically generate the appropriate source code.
Implementation of the source generator – code generation
Now that we have created the basic structure of our source generator, we can move on to the actual code generation. Essentially, this works as follows: our generator creates the new code directly as text, line by line. To do this, we first collect all relevant properties, check their attributes and then write the corresponding C# methods into a string.
Finally, we pass this text to the compiler, which generates a new file that is integrated into the project so that the methods can be used as if they were normal C# files. Alternatively, the code could also be generated entirely in memory without creating a physical file; the compiler would still treat it as normal C# code.
I would like to see the generated files in my solution, so we will create a file with formatted code for each class. The method responsible for this in our generator, GenerateValidationMethods, looks like this:
private void GenerateValidationMethods(SourceProductionContext spc, ImmutableArray<(PropertyDeclarationSyntax Property, SemanticModel Model)> properties)
{
var validationMethodsToGenerate = CollectValidationMethodsToGenerate(properties);
foreach (var className in validationMethodsToGenerate.Select(v => v.ClassName).Distinct())
{
var validationMethodsToGenerateForClass = validationMethodsToGenerate
.Where(v => v.ClassName == className)
.ToList();
GenerateValidationMethodsForClass(validationMethodsToGenerateForClass, spc);
}
}
The process is as follows:
1. First, all properties (with meta information) of all classes for which validation methods need to be generated are collected.
2. Then, all validation methods defined via attributes are created for each class.
This ensures that each class receives its own validations in a separate file and that the generated code is cleanly structured.
Let’s now take a closer look at the methods that are called.
private static List<ValidationMethodContext> CollectValidationMethodsToGenerate(ImmutableArray<(PropertyDeclarationSyntax Property, SemanticModel Model)> properties)
{
var validationMethodsToGenerate = new List<ValidationMethodContext>();
foreach (var (Property, Model) in properties)
{
if (!(Model.GetDeclaredSymbol(Property) is IPropertySymbol symbol))
continue;
foreach (var attribute in symbol.GetAttributes())
{
var validationType = GetValidationType(attribute.AttributeClass?.Name);
if (validationType == null)
continue;
var validationMethodContext = new ValidationMethodContext
{
Namespace = symbol.ContainingNamespace.ToDisplayString(),
ClassName = symbol.ContainingType.Name,
Attribute = attribute,
PropertyName = symbol.Name,
};
validationMethodsToGenerate.Add(validationMethodContext);
}
}
return validationMethodsToGenerate;
}
The CollectValidationMethodsToGenerate method searches through all previously collected properties, using the SemanticModel to obtain additional information from the compiler, i.e. the set attributes, the namespace, the class name and the name of the property. For each matching property, a ValidationMethodContext is created, which bundles this data and makes it available for later code generation.
Now, for each class that contains a property to be validated, the GenerateValidationMethodsForClass function is called.
private static void GenerateValidationMethodsForClass(List<ValidationMethodContext> validationMethodContexts, SourceProductionContext spc)
{
var className = validationMethodContexts.First().ClassName;
var indent = new string(' ', 4);
var sb = new StringBuilder();
sb.AppendLine($"namespace {validationMethodContexts.First().Namespace}");
sb.AppendLine("{");
sb.AppendLine($" public partial class {className}");
sb.AppendLine(indent + "{");
foreach (var validationMethodContext in validationMethodContexts)
{
switch (GetValidationType(validationMethodContext.Attribute.AttributeClass!.Name))
{
case ValidationType.Required:
AppendRequiredValidation(validationMethodContext.PropertyName, sb, indent + indent);
break;
case ValidationType.MaxLength:
AppendMaxLengthValidation(validationMethodContext.PropertyName, validationMethodContext.Attribute, sb, indent + indent);
break;
}
}
sb.AppendLine(indent + "}");
sb.AppendLine("}");
spc.AddSource(
$"{className}_Validation.g.cs",
SourceText.From(sb.ToString(), Encoding.UTF8)
);
}
private static void AppendRequiredValidation(string propertyName, StringBuilder sb, string indent)
{
sb.AppendLine(indent + $"public void Validate{propertyName}IsSet()");
sb.AppendLine(indent + "{");
sb.AppendLine(indent + $" if (string.IsNullOrWhiteSpace(this.{propertyName}))");
sb.AppendLine(indent + $" throw new System.Exception(\"{propertyName} is required.\");");
sb.AppendLine(indent + "}");
}
private static void AppendMaxLengthValidation(string propertyName, AttributeData attr, StringBuilder sb, string indent)
{
if (attr.ConstructorArguments.Length == 1)
{
var max = attr.ConstructorArguments[0].Value;
sb.AppendLine(indent + $"public void Validate{propertyName}HasValidLength()");
sb.AppendLine(indent + "{");
sb.AppendLine(indent + $" if (this.{propertyName} != null && this.{propertyName}.Length > {max})");
sb.AppendLine(indent + $" throw new System.Exception($\"{propertyName} may contain at most {max} characters.\");");
sb.AppendLine(indent + "}");
}
}
The GenerateValidationMethodsForClass method handles the actual code generation. It receives all previously collected ValidationMethodContext objects for a class and uses them to create a new file named ‘[class name]_Validation.g.cs’ containing the appropriate validation methods. To do this, a StringBuilder is used, into which the source code is written step by step, starting with the namespace and class declaration. The method then runs through all ValidationMethodContext entries and uses the attribute to decide which validation method should be generated.
For [Required], a method Validate<PropertyName>IsSet() is generated, which checks whether the value is set. For [MaxLength], a method Validate<PropertyName>HasValidLength() is created, which ensures that the maximum length is not exceeded. In this example, this only works for the String data type. For a final implementation, the data type of the property would of course have to be taken into account.
Finally, the generated text is fed into the build process as a new file using spc.AddSource(). The compiler treats this file as if it had been part of the project from the outset.
This completes our source generator: it collects the relevant properties, recognises their attributes and generates the appropriate validation methods.
Adjustments to the project files
Before we can run the generator for the first time, we first need to make a few settings in our application’s project files. When starting to develop source generators, stumbling blocks often arise, for example, the generator does not run at all or the generated files end up in unexpected locations. The following adjustments ensure that the generator is integrated correctly, that the files generated by the compiler are stored in a fixed location, and that we can also see them in the project.
Changes to MyApplication.csproj
1. Integrate generator as analyser
<ItemGroup>
<ProjectReference Include="..\SourceGenerators\SourceGenerators.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
- ProjectReference refers to our source generator project.
- OutputItemType=‘Analyzer’ ensures that the generator runs as a Roslyn analyser during the build.
- ReferenceOutputAssembly=‘false’ prevents the generator project’s DLL from being copied to the target project.
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
- EmitCompilerGeneratedFiles=true ensures that the generated files are made visible by the compiler.
- CompilerGeneratedFilesOutputPath specifies the path where the files are written; in our case, the Generated folder created earlier.
<ItemGroup>
<Compile Remove="Generated\**" />
</ItemGroup>
- Compile Remove=”Generated\**” ensures that the files stored in the Generated folder are not included in the compilation. This setting is necessary because the source generator already provides the code in memory at compile time. Without this exclusion, the code would be included twice – once from memory and once from the hard drive – and the project would not be able to compile. The files in the Generated folder are therefore only used for viewing and error analysis, but not for the actual compilation.
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
- TargetFramework = netstandard2.0 ensures that the source generator can be integrated into all projects – regardless of whether they use the .NET Framework or modern .NET versions such as .NET 8.
- LangVersion = 8.0: Since netstandard2.0 only comes with an older C# version by default (usually 7.3), we have to manually increase the language version. This allows us to use modern language features that we need to work with Roslyn and the source generator.
<PropertyGroup>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
- EnforceExtendedAnalyzerRules = true ensures that the compiler performs stricter checks for analysers and source generators. Specifically, this means that if we overlook something when accessing properties, attributes or symbols, the compiler will immediately point this out. This allows us to detect potential errors early on, before they later cause methods not to be generated or attributes to be overlooked.
Run the source generator
Now that we have implemented the code generation logic and made all the necessary adjustments to the project files, it is time to bring the generator to life. Source generators are always activated when the compiler translates the application code. This means that as soon as we build our project, all registered generators are executed and generate the additional source code.
We now run our generator for the first time by rebuilding our solution. We can check the results immediately:
1. Generator is executed
The generator is executed automatically during the build; no manual steps are required. We can see this, among other things, by the fact that a new file, Person_Validation.g.cs, has been created in the previously configured folder MyApplication\Generated\SourceGenerators\SourceGenerators.ValidationGenerator.
2. File contents
When we open the file, we see all the validation methods that were defined via attributes in our class. The generated methods are located in the correct namespace and are part of the Person class.
namespace MyApplication
{
public partial class Person
{
public void ValidateNameIsSet()
{
if (string.IsNullOrWhiteSpace(this.Name))
throw new System.Exception("Name is required.");
}
public void ValidateNameHasValidLength()
{
if (this.Name != null && this.Name.Length > 50)
throw new System.Exception($"Name may contain at most 50 characters.");
}
public void ValidateOccupationHasValidLength()
{
if (this.Occupation != null && this.Occupation.Length > 100)
throw new System.Exception($"Occupation may contain at most 100 characters.");
}
}
}
3. Compilability
The generated file can be compiled immediately and used straight away. Changes to the properties or attributes automatically result in updated code during the next build.
This confirms that our generator works as intended: it generates the code correctly, in the right place, and all validation methods are fully available.
Debugging the source generator
When developing source generators, errors can easily occur, such as code that is not generated at all or is generated incorrectly. Such problems are often frustrating and difficult to solve without debugging. Fortunately, source generators can also be debugged. Since they are executed during compilation, classic debugging does not work directly. However, a simple and proven option is to trigger debugging directly in the generator code. To do this, the initialise method or an auxiliary method of the generator is used.
System.Diagnostics.Debugger.Launch();
During the next build, Visual Studio will automatically ask whether the debugger should be attached. You can then set breakpoints and go through the generator code step by step to identify possible sources of error.
Conclusion
In this article, we have created a source generator in C# step by step: We set up a sample project with a Person class, implemented the basic generator structure, and finally automatically generated methods for validating properties. In doing so, we addressed typical stumbling blocks when getting started with generator development, from the necessary adjustments in the project files to the correct integration of the generator to the generation of files and debugging.
All validation methods defined via attributes were correctly generated in files that can be easily viewed and compiled in the project. If you wish, you can now extend the sample project: add more classes with string properties – both with and without attributes – and observe how the generator automatically creates additional validation files and methods.
I am convinced that source generators can be a useful aid in many projects and can significantly reduce repetitive work. Perhaps in your projects too?
Hinweise:
[1] Microsoft Dev Blogs: Introducing C# Source Generators
Would you like to use source generators? Then talk to us about your project.
Would you like to discuss source generators as a multiplier? Then share this article in your networks.
Glen Gelmroth has published another post here on the t2informatik Blog:

Glen Dimroth
Glen Gelmroth has been interested in software development since childhood and has turned his hobby into his profession. He has been a senior developer at t2informatik since 2012 and has worked on numerous projects, mainly in the .NET environment.
He firmly believes that good code should be so clear that it can still be understood, maintained and tested years later. He is also enthusiastic about Jira customisation and automation – because who likes monotonous tasks when you can script them away cleverly?
In the t2informatik Blog, we publish articles for people in organisations. For these people, we develop and modernise software. Pragmatic. ✔️ Personal. ✔️ Professional. ✔️ Click here to find out more.

