Skip to content

Source Generators in C# — Complete Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Source Generators in C#. We cover key concepts, practical examples, and best practices to help you master this topic.

Hook

Source generators are one of the most powerful features in modern C#. They allow you to generate code at compile time based on your existing code structure. This eliminates boilerplate, improves performance by removing runtime reflection, and enables patterns that were previously impossible. The .NET compiler platform (Roslyn) makes this accessible to every developer.

Learning Path

graph LR
  A[Source Generators] --> B[Incremental Generators]
  A --> C[Roslyn APIs]
  B --> D[Syntax Trees]
  B --> E[Semantic Model]
  C --> F[Code Generation]
  style A fill:#4a90d9,color:#fff
  style B fill:#4a90d9,color:#fff
  style C fill:#4a90d9,color:#fff
  style D fill:#4a90d9,color:#fff
  style E fill:#4a90d9,color:#fff
  style F fill:#4a90d9,color:#fff

What Source Generators Do

Source generators analyze your code during compilation and emit additional C# source files.

// Before compilation
[AutoNotify]
public partial class Person
{
    private string _name = "";
    private int _age;
}

// After source generator runs
public partial class Person
{
    public string Name
    {
        get => _name;
        set
        {
            _name = value;
            OnPropertyChanged(nameof(Name));
        }
    }

    public int Age
    {
        get => _age;
        set
        {
            _age = value;
            OnPropertyChanged(nameof(Age));
        }
    }
}

Building a Source Generator

Create a source generator project.

// Generator.csproj
/*
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>12</LangVersion>
    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
  </ItemGroup>
</Project>
*/

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Text;

[Generator(LanguageNames.CSharp)]
public class AutoNotifyGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        var declarations = context.SyntaxProvider
            .CreateSyntaxProvider(
                predicate: (node, _) => IsTargetAttribute(node),
                transform: (ctx, _) => GetTargetClass(ctx))
            .Where(t => t is not null);

        context.RegisterSourceOutput(declarations,
            (spc, source) => Execute(source!, spc));
    }

    private static bool IsTargetAttribute(SyntaxNode node)
    {
        return node is AttributeSyntax attr &&
               attr.Name.ToString() is "AutoNotify" or "AutoNotifyAttribute";
    }

    private static ClassDeclarationSyntax? GetTargetClass(GeneratorSyntaxContext context)
    {
        var attribute = (AttributeSyntax)context.Node;
        return attribute.Parent?.Parent as ClassDeclarationSyntax;
    }

    private void Execute(ClassDeclarationSyntax classDecl,
        SourceProductionContext context)
    {
        var className = classDecl.Identifier.Text;
        var ns = GetNamespace(classDecl);

        var sb = new StringBuilder();
        sb.AppendLine($$"""
using System.ComponentModel;

namespace {{ns}}
{
    public partial class {{className}} : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler? PropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
""");

        foreach (var field in classDecl.Members.OfType<FieldDeclarationSyntax>())
        {
            var variable = field.Declaration.Variables.First();
            var fieldName = variable.Identifier.Text;
            var propertyName = char.ToUpper(fieldName[0]) + fieldName[1..];
            var fieldType = field.Declaration.Type.ToString();

            sb.AppendLine($$"""
        public {{fieldType}} {{propertyName}}
        {
            get => {{fieldName}};
            set
            {
                if (!EqualityComparer<{{fieldType}}>.Default.Equals({{fieldName}}, value))
                {
                    {{fieldName}} = value;
                    OnPropertyChanged(nameof({{propertyName}}));
                }
            }
        }
""");
        }

        sb.AppendLine("    }");
        sb.AppendLine("}");

        context.AddSource($"{className}.g.cs", sb.ToString());
    }

    private static string GetNamespace(ClassDeclarationSyntax classDecl)
    {
        var ns = classDecl.Ancestors()
            .OfType<NamespaceDeclarationSyntax>()
            .FirstOrDefault()?.Name.ToString()
            ?? classDecl.Ancestors()
            .OfType<FileScopedNamespaceDeclarationSyntax>()
            .FirstOrDefault()?.Name.ToString()
            ?? "Global";
        return ns;
    }
}

Using the Generator

// In your main project, reference the generator
/*
<ItemGroup>
  <ProjectReference Include="..\AutoNotifyGenerator\AutoNotifyGenerator.csproj"
                    OutputItemType="Analyzer"
                    ReferenceOutputAssembly="false" />
</ItemGroup>
*/

[AutoNotify]
public partial class ViewModel
{
    private string _title = "";
    private string _description = "";
    private bool _isVisible;
}

// Usage
var vm = new ViewModel();
vm.PropertyChanged += (s, e) =>
    Console.WriteLine($"Property changed: {e.PropertyName}");

vm.Title = "Hello Source Generators!";
vm.IsVisible = true;

System.Text.Json Source Generator

The built-in JSON source generator eliminates reflection.

using System.Text.Json.Serialization;

[JsonSerializable(typeof(Person))]
[JsonSerializable(typeof(List<Person>))]
internal partial class AppJsonContext : JsonSerializerContext { }

// Usage - no reflection at runtime
var person = new Person { Name = "Alice", Age = 30 };
string json = JsonSerializer.Serialize(person, AppJsonContext.Default.Person);
Console.WriteLine(json);

Regex Source Generators (.NET 7+)

Compile regex patterns at build time.

using System.Text.RegularExpressions;

public partial class EmailValidator
{
    [GeneratedRegex(@"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$")]
    private static partial Regex EmailRegex();

    public bool IsValidEmail(string email) => EmailRegex().IsMatch(email);
}

// Benefits: zero startup cost, no runtime compilation, trim-safe

Logging Source Generators (Microsoft.Extensions)

High-performance logging with source generators.

using Microsoft.Extensions.Logging;

public static partial class LogMessages
{
    [LoggerMessage(
        EventId = 1001,
        Level = LogLevel.Information,
        Message = "User {UserId} logged in from {IpAddress}")]
    public static partial void UserLoggedIn(this ILogger logger,
        int userId, string ipAddress);

    [LoggerMessage(
        EventId = 1002,
        Level = LogLevel.Error,
        Message = "Payment failed for order {OrderId}: {Error}")]
    public static partial void PaymentFailed(this ILogger logger,
        int orderId, string error);
}

// Usage
_logger.UserLoggedIn(42, "192.168.1.1");
// No boxing, no string allocation at the call site

Common Mistakes

  1. Not using incremental generators: ISourceGenerator is deprecated. Always implement IIncrementalGenerator for performance and caching.

  2. Generating code that does not compile: Your generated code must be valid C#. Test it thoroughly with unit tests.

  3. Forgetting to set OutputItemType: The generator project must be referenced as an Analyzer, not a regular project reference.

  4. Targeting wrong framework: Source generators must target netstandard2.0 to run in the compiler Process.

  5. Not handling errors gracefully: Use SourceProductionContext.ReportDiagnostic to provide meaningful error messages to users.

Practice Questions

  1. Create a source generator that automatically implements IEquatable<T> for any class marked with [AutoEquatable].

  2. Build a generator that creates a strongly-typed configuration class from a JSON schema file.

  3. Implement a generator that creates DTO mapping methods (like AutoMapper) at compile time.

  4. Challenge: Write a source generator that parses SQL files and generates type-safe query methods.

FAQ

Can source generators read files from disk?

Yes, but use AdditionalFiles in the project file and access them through the AnalyzerConfigOptions or AdditionalTexts.

What is the difference between source generators and code analyzers?

Analyzers detect issues in code. Generators emit new code. You can combine both in a single analyzer project.

Are source generators available in all .NET versions?

Source generators require .NET 5+ with Roslyn 3.8+. The incremental generator API requires .NET 6+.

How do I debug a source generator?

Add Debugger.Launch() in your generator code. The IDE will prompt you to attach a debugger during compilation.

Can source generators modify existing files?

No. Source generators can only add new files. They cannot modify or delete existing source files.

Mini Project: Auto-Dependency Injection Generator

Build a source generator that automatically registers services in the DI container.

// Generator input
[RegisterService(ServiceLifetime.Scoped)]
public class UserService : IUserService
{
    public void DoWork() => Console.WriteLine("User service working");
}

[RegisterService(ServiceLifetime.Singleton)]
public class CacheService { }

// Generated code
public static class GeneratedServiceRegistration
{
    public static IServiceCollection AddGeneratedServices(
        this IServiceCollection services)
    {
        services.AddScoped<IUserService, UserService>();
        services.AddSingleton<CacheService>();
        return services;
    }
}

// Usage (no manual registration needed)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGeneratedServices();

// Generator implementation (simplified)
[Generator(LanguageNames.CSharp)]
public class DiRegistrationGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        var classes = context.SyntaxProvider
            .CreateSyntaxProvider(
                predicate: (node, _) => IsClassWithAttribute(node),
                transform: (ctx, _) => GetRegistrationInfo(ctx))
            .Where(info => info is not null);

        context.RegisterSourceOutput(classes, GenerateCode);
    }

    private void GenerateCode(SourceProductionContext context,
        List<RegistrationInfo> registrations)
    {
        var sb = new StringBuilder();
        sb.AppendLine("""
using Microsoft.Extensions.DependencyInjection;

public static class GeneratedServiceRegistration
{
    public static IServiceCollection AddGeneratedServices(
        this IServiceCollection services)
    {
""");
        foreach (var reg in registrations)
        {
            sb.AppendLine(
                $"        services.Add{reg.Lifetime}<{reg.Interface}, {reg.Class}>();");
        }
        sb.AppendLine("""
        return services;
    }
}
""");
        context.AddSource("GeneratedServiceRegistration.g.cs", sb.ToString());
    }
}

Source generators represent a paradigm shift in C# development. By moving Code Generation to compile time, they eliminate runtime overhead, enable new patterns, and reduce boilerplate. The .NET compiler platform makes source generators accessible, and built-in generators for JSON, logging, and regex demonstrate their power.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro