<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. C#
  3. Master level
All lessons in this course

Lesson 59 of 60 17 minMaster

Source generators

Let the compiler write the boring code: using built-in generators (GeneratedRegex, JSON, LibraryImport, MVVM Toolkit) and building your own incremental generator.

Some code is pure boilerplate: INotifyPropertyChanged properties, ToString, mapping, serialisation. You could generate it at run time with reflection (slow, and it breaks trimming and AOT), or write it by hand (boring, and easy to get wrong). A source generator is a plugin for the C# compiler that reads your code while it compiles and adds new C# files to the build.

Interactive

Code that writes code

Members:
  1. you write Beam.cs
  2. compiler parses it
  3. generator runs
  4. adds Beam.ToString.g.cs
  5. both compile into one class
Beam.cs (you write)
[AutoToString]public partial class Beam{    public string Mark { get; set; } = "";    public string Profile { get; set; } = "";    public double LengthM { get; set; }}
Beam.ToString.g.cs (generated)
// <auto-generated/>partial class Beam{    public override string ToString() =>        "Beam { " + "Mark = " + Mark + ", " + "Profile = " + Profile + ", " + "LengthM = " + LengthM + " }";}
The generator read the class at compile time and wrote ToString() for 3 properties. Add a property and the method updates on the next build, no reflection at run time.

Generators you can use today

GeneratorYou writeIt generates
[GeneratedRegex]a partial method with a patterna compiled, fast regex
[JsonSerializable]a JsonSerializerContextserialisation code without reflection
[LibraryImport]a partial native methodP/Invoke marshalling code
[LoggerMessage]a partial logging methodfast, allocation-free logging
CommunityToolkit.Mvvm [ObservableProperty], [RelayCommand]fields and methodsbindable properties and ICommands for WPF
Builtins.cs
using System.Text.RegularExpressions; public static partial class Patterns{    [GeneratedRegex(@"^(IPE|HEA|HEB)(\d{2,4})$", RegexOptions.IgnoreCase)]    public static partial Regex ISection();} Match m = Patterns.ISection().Match("ipe300");Console.WriteLine($"{m.Groups[1].Value.ToUpper()} depth {m.Groups[2].Value} mm");
Output
IPE depth 300 mm
Json.cs
[JsonSerializable(typeof(List<Beam>))]internal partial class ModelJsonContext : JsonSerializerContext { } string json = JsonSerializer.Serialize(beams, ModelJsonContext.Default.ListBeam);   // no reflection

MVVM without boilerplate

BeamViewModel.cs
// dotnet add package CommunityToolkit.Mvvmusing CommunityToolkit.Mvvm.ComponentModel;using CommunityToolkit.Mvvm.Input; public partial class BeamViewModel : ObservableObject{    [ObservableProperty]    private string _mark = "";          // generates public string Mark { get; set; } with change notification     [ObservableProperty]    private double _lengthM;     [RelayCommand]    private void Check() => Status = LengthM > 24 ? "Too long" : "OK";   // generates CheckCommand     [ObservableProperty]    private string _status = "";}

In XAML you bind to {Binding Mark} and {Binding CheckCommand}. The toolkit wrote the properties and the command for you.

Writing your own generator

A generator lives in its own class library that targets netstandard2.0 (the compiler loads it) and references the Roslyn packages.

Gen.csproj
<Project Sdk="Microsoft.NET.Sdk">  <PropertyGroup>    <TargetFramework>netstandard2.0</TargetFramework>    <LangVersion>latest</LangVersion>    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>  </PropertyGroup>  <ItemGroup>    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />  </ItemGroup></Project>
ToStringGenerator.cs
using System.Linq;using Microsoft.CodeAnalysis;using Microsoft.CodeAnalysis.CSharp.Syntax; [Generator]public class ToStringGenerator : IIncrementalGenerator{    public void Initialize(IncrementalGeneratorInitializationContext context)    {        // 1. add the [AutoToString] attribute itself to the user's project        context.RegisterPostInitializationOutput(ctx => ctx.AddSource("AutoToStringAttribute.g.cs",            "namespace Gen { [System.AttributeUsage(System.AttributeTargets.Class)] public sealed class AutoToStringAttribute : System.Attribute { } }"));         // 2. find classes marked with it, and collect what we need        var classes = context.SyntaxProvider.ForAttributeWithMetadataName(            "Gen.AutoToStringAttribute",            predicate: (node, _) => node is ClassDeclarationSyntax,            transform: (ctx, _) =>            {                var type = (INamedTypeSymbol)ctx.TargetSymbol;                var props = type.GetMembers().OfType<IPropertySymbol>()                                .Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic && !p.IsIndexer)                                .Select(p => p.Name).ToArray();                string ns = type.ContainingNamespace.IsGlobalNamespace ? "" : type.ContainingNamespace.ToDisplayString();                return (Ns: ns, Name: type.Name, Props: new EquatableArray(props));            });         // 3. write one file per class        context.RegisterSourceOutput(classes, (spc, c) =>        {            string body = c.Props.Items.Length == 0                ? "\"\""                                                   // no properties: still valid C#                : string.Join(" + \", \" + ", c.Props.Items.Select(p => $"\"{p} = \" + {p}"));            string nsLine = c.Ns == "" ? "" : $"namespace {c.Ns};";      // classes without a namespace            spc.AddSource($"{c.Name}.ToString.g.cs", $$"""                {{nsLine}}                partial class {{c.Name}}                {                    public override string ToString() => "{{c.Name}} { " + {{body}} + " }";                }                """);        });    }}

EquatableArray is a small wrapper (not shown) that compares arrays by their contents. The generator is incremental: Roslyn caches each step and reruns it only when its input changes, so typing in the editor stays fast. That only works if the values passed between steps compare by value. That is why we use tuples of strings and an equatable array, never Roslyn symbols or syntax nodes.

Using it

SteelTools.csproj
<ItemGroup>  <ProjectReference Include="..\Gen\Gen.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /></ItemGroup>
Beam.cs
using Gen; namespace SteelTools; [AutoToString]public partial class Beam            // partial: the generator adds the other half{    public string Mark { get; set; } = "";    public double LengthM { get; set; }}
Program.cs
Console.WriteLine(new SteelTools.Beam { Mark = "B12", LengthM = 6.5 });   // calls the generated ToString()
Output
Beam { Mark = B12, LengthM = 6.5 }

Generator rules

  • Generators can only add code, never change or delete yours. That is why the target class must be partial.
  • Use IIncrementalGenerator. The older ISourceGenerator reruns on every keystroke and makes the IDE slow.
  • Report problems with diagnostics (spc.ReportDiagnostic) instead of throwing.
  • Test generators with snapshot tests: compile sample code and compare the generated output.
Quick check

When does a source generator run?

Quick check

Why must a class be partial for a generator to add a method to it?

Practice

Use [GeneratedRegex] to write Patterns.Grid() that matches grid labels like "A-3", "C-12" (one capital letter, a dash, 1–2 digits). Print whether "B-7", "b-7" and "AB-1" match.

Show the solution
Solution.cs
public static partial class Patterns{    [GeneratedRegex(@"^[A-Z]-\d{1,2}$")]    public static partial Regex Grid();} foreach (var s in new[] { "B-7", "b-7", "AB-1" })    Console.WriteLine($"{s}: {Patterns.Grid().IsMatch(s)}");
Output
B-7: True
b-7: False
AB-1: False

Remember

  • Source generators add C# code at compile time: fast, AOT-friendly, no reflection.
  • Use the built-ins: [GeneratedRegex], [JsonSerializable], [LibraryImport], [LoggerMessage], MVVM Toolkit.
  • Your own generator: netstandard2.0 library, IIncrementalGenerator, ForAttributeWithMetadataName, AddSource.
  • Generators only add code, so targets must be partial; keep pipeline values equatable.
0 of 60 lessons completed