Modern C# source generator that automatically creates ToString() methods at compile time using Roslyn APIs.
- Roslyn Source Generators: IIncrementalGenerator pattern (C# 9+)
- Compile-Time Code Generation: Zero runtime overhead
- Syntax Analysis: Filtering with predicates for performance
- Semantic Analysis: Full type information with ISymbol
- Attribute-Based Generation: Marker attributes for code gen triggers
SourceGenerators/
├── SourceGenerators.sln
├── Generator/ # Source generator project
│ ├── Generator.csproj # netstandard2.0 (required for analyzers)
│ ├── ToStringGenerator.cs # IIncrementalGenerator implementation
│ └── ToStringAttribute.cs # Marker attribute
└── Consumer/ # Application using the generator
├── Consumer.csproj # References generator as analyzer
├── Person.cs # Classes with [GenerateToString]
└── Program.cs # Demonstration
cd samples/04-Expert/SourceGenerators
# Build both projects
dotnet build
# Run consumer application
dotnet run --project Consumer🔧 ROSLYN SOURCE GENERATORS - ToString Auto-Generation
═══ Example 1: Person ═══
Person { Name = John Doe, Age = 30, Email = john@example.com }
═══ Example 2: Product ═══
Product { Name = Laptop, Price = 999.99 }
═══ Example 3: Order ═══
Order { OrderId = 1001, CustomerName = Alice Smith, TotalAmount = 1500.50, OrderDate = ... }
[GenerateToString] // ← Triggers source generator
public partial class Person // ← Must be partial
{
public string Name { get; set; }
public int Age { get; set; }
}The source generator runs during compilation and creates Person.g.cs:
// <auto-generated/>
partial class Person
{
public override string ToString()
{
return $"Person { Name = {Name}, Age = {Age} }";
}
}var person = new Person { Name = "John", Age = 30 };
Console.WriteLine(person); // Uses generated ToString()
// Output: Person { Name = John, Age = 30 }Generated files are written to the build output:
# View generated code
cat Consumer/obj/Debug/net8.0/generated/Generator/SourceGenerators.ToStringGenerator/Person.g.cspublic void Initialize(IncrementalGeneratorInitializationContext context)
{
// STEP 1: Syntax filter (fast, no semantic analysis)
var classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: IsSyntaxTargetForGeneration, // Quick check
transform: GetSemanticTargetForGeneration) // Full analysis
.Where(m => m is not null);
// STEP 2: Combine with compilation
var compilationAndClasses = context.CompilationProvider
.Combine(classDeclarations.Collect());
// STEP 3: Generate source
context.RegisterSourceOutput(compilationAndClasses, Execute);
}Benefits:
- ✅ Only re-generates when relevant code changes
- ✅ IDE remains responsive during editing
- ✅ Faster build times with incremental compilation
| Approach | Compile Time | Runtime | Type Safe | IDE Support |
|---|---|---|---|---|
| Manual Code | - | Fast | ✅ | ✅ |
| Source Generator | +100-500ms | Fast | ✅ | ✅ |
| Reflection | - | Slow | ❌ | |
| T4 Templates | Slow | Fast |
Source generators extend existing classes, so they must be partial:
// ✅ GOOD
[GenerateToString]
public partial class Person { }
// ❌ BAD - Won't compile
[GenerateToString]
public class Person { } // Missing 'partial'[GenerateToString(
IncludePropertyNames = true, // "Name = John" vs "John"
IncludePrivateFields = false)] // Only public properties
public partial class Person { }<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <!-- Required -->
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsRoslynComponent>true</IsRoslynComponent>
</PropertyGroup><ProjectReference Include="..\Generator\Generator.csproj"
OutputItemType="Analyzer" <!-- Treat as analyzer -->
ReferenceOutputAssembly="true" /> <!-- Can use attributes -->-
View generated files:
# Enable EmitCompilerGeneratedFiles dotnet build /p:EmitCompilerGeneratedFiles=true -
Attach debugger:
[Generator] public class ToStringGenerator : IIncrementalGenerator { public void Initialize(...) { System.Diagnostics.Debugger.Launch(); // Breaks here } }
- Use predicates to filter early (syntax-only checks)
- Use transforms for expensive semantic analysis
- Cache compilation data between runs
- Only generate when source changes (incremental)
- JSON Serialization: Generate serializers at compile time
- Entity Framework: Generate DbContext from attributes
- gRPC/Protobuf: Generate clients from .proto files
- Regex: Compile regex patterns at build time (C# 11+)
- Logging: Generate structured logging code
- DTOs: Auto-generate mapping between models
# Clean build output
dotnet clean
rm -rf Consumer/obj Consumer/bin
# Rebuild
dotnet buildEnsure ReferenceOutputAssembly="true" in project reference:
<ProjectReference Include="..\Generator\Generator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="true" /> <!-- This! -->// ❌ Error CS0260: Missing partial modifier
[GenerateToString]
public class Person { }
// ✅ Fixed
[GenerateToString]
public partial class Person { }- Understand IIncrementalGenerator vs ISourceGenerator
- Write predicates for syntax filtering
- Use semantic analysis with ISymbol
- Generate source with AddSource()
- Debug generated code
- Reference generator as analyzer
- Use partial classes correctly
- View generated files in obj/ directory
Key Takeaway: Source generators eliminate boilerplate while maintaining type safety and zero runtime overhead. They're compile-time magic that makes your code cleaner!