Source Generators
Overview
Source Generators are a compile-time code generation feature introduced in .NET 5. They are executed when the compiler analyzes source code and add new source files to the compilation process. This enables metaprogramming without relying on runtime reflection.
What are Source Generators?
Source Generators are special .NET analyzers executed by the Roslyn compiler at compile time.
Key Features
- Compile-time Execution: Generate code during build, resulting in zero runtime overhead
- Performance: Significantly better runtime performance compared to reflection
- AOT Compatible: Works with Native AOT (Ahead-of-Time) compilation and trimmed environments
- Type Safety: Generated code is validated at compile time
Comparison with Reflection
| Feature | Source Generators | Reflection |
|---|---|---|
| Execution Timing | Compile time | Runtime |
| Performance | Fast (no overhead) | Slow (dynamic resolution required) |
| AOT Support | ✅ Fully supported | ❌ Limited |
| Debugging | Generated code directly viewable | Only viewable at runtime |
| Flexibility | Compile-time information only | Access to runtime state |
When to Use Source Generators
Source Generators are suitable for:
- Automatic generation of boilerplate code
- Serialization/deserialization
- Logging code generation
- Performance-critical processing
Reflection is suitable for:
- Processing types determined only at runtime
- Plugin systems or dynamic DLL loading
- Configuration or data-driven dynamic behavior
Creating a Source Generator
Project Setup
First, create a class library project dedicated to the Source Generator.
dotnet new classlib -n MySourceGenerator
cd MySourceGenerator
Add the required packages to the project file (.csproj):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<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" PrivateAssets="all" />
</ItemGroup>
</Project>
Basic Source Generator Implementation
Create a simple Hello World generator:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;
namespace MySourceGenerator;
[Generator]
public class HelloWorldGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
// Initialization processing (if needed)
}
public void Execute(GeneratorExecutionContext context)
{
// Code to generate
var sourceBuilder = new StringBuilder(@"
namespace GeneratedCode
{
public static class HelloWorld
{
public static string SayHello() => ""Hello from Source Generator!"";
}
}
");
// Add source
context.AddSource("HelloWorld.g.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
}
}
Using the Source Generator
In the .csproj file of the project using the generated code:
<ItemGroup>
<ProjectReference Include="..\MySourceGenerator\MySourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
Usage example:
using GeneratedCode;
class Program
{
static void Main()
{
Console.WriteLine(HelloWorld.SayHello());
// Output: Hello from Source Generator!
}
}
Practical Example: Attribute-Based Code Generation
As a more practical example, let's create a generator that automatically generates a ToString method using attributes.
Define Marker Attribute
using System;
namespace SourceGeneratorAttributes;
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class GenerateToStringAttribute : Attribute
{
}
Implementing Incremental Source Generator
From .NET 6 onwards, using IIncrementalGenerator is recommended:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
namespace MySourceGenerator;
[Generator]
public class ToStringGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Filter classes with the attribute
var classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (node, _) => node is ClassDeclarationSyntax,
transform: static (ctx, _) => GetSemanticTargetForGeneration(ctx))
.Where(static m => m is not null);
// Execute generation
context.RegisterSourceOutput(classDeclarations,
static (spc, source) => Execute(source!, spc));
}
private static ClassDeclarationSyntax? GetSemanticTargetForGeneration(
GeneratorSyntaxContext context)
{
var classDeclaration = (ClassDeclarationSyntax)context.Node;
// Check for GenerateToString attribute
foreach (var attributeList in classDeclaration.AttributeLists)
{
foreach (var attribute in attributeList.Attributes)
{
var symbol = context.SemanticModel.GetSymbolInfo(attribute).Symbol;
if (symbol?.ContainingType?.Name == "GenerateToStringAttribute")
{
return classDeclaration;
}
}
}
return null;
}
private static void Execute(ClassDeclarationSyntax classDeclaration,
SourceProductionContext context)
{
var className = classDeclaration.Identifier.Text;
var namespaceName = GetNamespace(classDeclaration);
var source = GenerateToStringMethod(namespaceName, className);
context.AddSource($"{className}.g.cs", source);
}
private static string GetNamespace(ClassDeclarationSyntax classDeclaration)
{
var namespaceDeclaration = classDeclaration.Parent as NamespaceDeclarationSyntax;
return namespaceDeclaration?.Name.ToString() ?? "Global";
}
private static string GenerateToStringMethod(string namespaceName, string className)
{
return $@"
namespace {namespaceName}
{{
partial class {className}
{{
public override string ToString()
{{
var properties = GetType().GetProperties()
.Select(p => $""{{p.Name}}={{p.GetValue(this)}}"")
.ToArray();
return $""{className} {{ {{string.Join("", "", properties)}} }}"";
}}
}}
}}
";
}
}
Usage Example
using SourceGeneratorAttributes;
namespace MyApp;
[GenerateToString]
public partial class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
// Usage
var person = new Person { Name = "Alice", Age = 30 };
Console.WriteLine(person.ToString());
// Output: Person { Name=Alice, Age=30 }
Practical Examples in .NET
System.Text.Json Source Generator
In ASP.NET Core 6 and later, Source Generator can be used for JSON serialization:
using System.Text.Json.Serialization;
[JsonSerializable(typeof(Person))]
[JsonSerializable(typeof(List<Person>))]
internal partial class MyJsonContext : JsonSerializerContext
{
}
// Usage
var options = new JsonSerializerOptions
{
TypeInfoResolver = MyJsonContext.Default
};
var json = JsonSerializer.Serialize(person, options);
Benefits:
- No runtime reflection required
- AOT compatible
- Performance improvement (up to 40% faster)
LoggerMessage Source Generator
Generate high-performance logging methods:
public static partial class LoggerExtensions
{
[LoggerMessage(
EventId = 0,
Level = LogLevel.Information,
Message = "Processing request for {UserName} at {Timestamp}")]
public static partial void LogProcessingRequest(
this ILogger logger, string userName, DateTime timestamp);
}
// Usage
logger.LogProcessingRequest("Alice", DateTime.UtcNow);
Benefits:
- Reduced allocations
- Reduced string interpolation overhead
- Type-safe logging
Regex Source Generator
Compile-time optimization of regular expressions:
public partial class RegexPatterns
{
[GeneratedRegex(@"^\d{3}-\d{4}$")]
public static partial Regex PhoneNumberPattern();
}
// Usage
if (RegexPatterns.PhoneNumberPattern().IsMatch(input))
{
// Process if matched
}
Debugging and Troubleshooting
Viewing Generated Code
Generated code is saved in the project's obj folder:
obj/Debug/net8.0/generated/MySourceGenerator/MySourceGenerator.ToStringGenerator/
In Visual Studio, you can view it from "Dependencies > Analyzers" in Solution Explorer.
Debugging Methods
To debug Source Generators, add the following to .csproj:
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Generated/**/*.cs" />
</ItemGroup>
To attach debugger:
public void Execute(GeneratorExecutionContext context)
{
#if DEBUG
if (!System.Diagnostics.Debugger.IsAttached)
{
System.Diagnostics.Debugger.Launch();
}
#endif
// Generator code
}
Common Issues
-
Generated code not found
- Verify that
OutputItemType="Analyzer"is set - Clean and rebuild
- Verify that
-
Partial class errors
- Classes targeted for generation must be declared as
partial
- Classes targeted for generation must be declared as
-
Performance issues
- Use
IIncrementalGenerator(more efficient thanISourceGenerator) - Avoid unnecessary Syntax Tree traversal
- Use
Best Practices
-
Use Incremental Generator
[Generator]public class MyGenerator : IIncrementalGenerator{// More efficient than ISourceGenerator} -
Provide diagnostic messages
context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor(id: "MYGEN001",title: "Invalid usage",messageFormat: "Class {0} must be partial",category: "MyGenerator",DiagnosticSeverity.Error,isEnabledByDefault: true),classDeclaration.GetLocation(),className)); -
Use .g.cs suffix for generated file names
context.AddSource($"{className}.g.cs", source); -
Target netstandard2.0
- For maximum compatibility
-
Use appropriate namespaces
- Ensure generated code uses the same namespace as the original code
Performance Impact
Source Generators significantly improve performance, especially in the following scenarios:
- JSON Serialization: Up to 40% faster compared to reflection-based
- Logging: Reduced string allocations, up to 8x faster
- Regular Expressions: 10-20% faster with compile-time optimization
- ORM Mapping: 2-3x faster compared to dynamic mapping
Summary
Source Generators are the new standard for metaprogramming in .NET.
Key Benefits:
- ✅ Zero runtime overhead with compile-time code generation
- ✅ Native AOT compatible
- ✅ Type safety and compile-time validation
- ✅ Easy to debug and maintain
- ✅ Significant performance improvements compared to reflection
Consider using when:
- Reducing boilerplate code
- Serialization/deserialization
- High-performance logging
- Performance-critical applications
- Using Native AOT or trimming
By properly using Source Generators, you can optimize performance while maintaining code maintainability.