Skip to main content

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

FeatureSource GeneratorsReflection
Execution TimingCompile timeRuntime
PerformanceFast (no overhead)Slow (dynamic resolution required)
AOT Support✅ Fully supported❌ Limited
DebuggingGenerated code directly viewableOnly viewable at runtime
FlexibilityCompile-time information onlyAccess 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

  1. Generated code not found

    • Verify that OutputItemType="Analyzer" is set
    • Clean and rebuild
  2. Partial class errors

    • Classes targeted for generation must be declared as partial
  3. Performance issues

    • Use IIncrementalGenerator (more efficient than ISourceGenerator)
    • Avoid unnecessary Syntax Tree traversal

Best Practices

  1. Use Incremental Generator

    [Generator]
    public class MyGenerator : IIncrementalGenerator
    {
    // More efficient than ISourceGenerator
    }
  2. 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));
  3. Use .g.cs suffix for generated file names

    context.AddSource($"{className}.g.cs", source);
  4. Target netstandard2.0

    • For maximum compatibility
  5. 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.