Source Generators in C# — Complete Guide
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
Not using incremental generators:
ISourceGeneratoris deprecated. Always implementIIncrementalGeneratorfor performance and caching.Generating code that does not compile: Your generated code must be valid C#. Test it thoroughly with unit tests.
Forgetting to set OutputItemType: The generator project must be referenced as an Analyzer, not a regular project reference.
Targeting wrong framework: Source generators must target
netstandard2.0to run in the compiler Process.Not handling errors gracefully: Use
SourceProductionContext.ReportDiagnosticto provide meaningful error messages to users.
Practice Questions
Create a source generator that automatically implements
IEquatable<T>for any class marked with[AutoEquatable].Build a generator that creates a strongly-typed configuration class from a JSON schema file.
Implement a generator that creates DTO mapping methods (like AutoMapper) at compile time.
Challenge: Write a source generator that parses SQL files and generates type-safe query methods.
FAQ
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