·C#·1 min read·Senior developers

Source Generators in C# — Stop Writing Boilerplate, Start Generating It

Hand-writing DTO mappers and tool descriptors is a tax. Incremental source generators pay it for you, compile-time, with no reflection cost at runtime.

I've written the same DTO-to-entity mapper maybe 200 times. Different shapes, same boredom. Source generators are the way out, and the incremental ones in C# 13/14 are finally fast enough to not ruin your IDE.

Here's a tiny generator that turns [Tool]-attributed methods into MCP tool descriptors at compile time:

[Generator]
public class ToolDescriptorGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext ctx)
    {
        var methods = ctx.SyntaxProvider
            .ForAttributeWithMetadataName(
                "MyApp.ToolAttribute",
                predicate: static (s, _) => s is MethodDeclarationSyntax,
                transform: static (g, _) => (IMethodSymbol)g.TargetSymbol)
            .Collect();

        ctx.RegisterSourceOutput(methods, (spc, syms) =>
        {
            var sb = new StringBuilder("namespace MyApp.Generated;\n");
            sb.AppendLine("public static class Tools {");
            foreach (var m in syms)
                sb.AppendLine($"  public const string {m.Name} = \"{m.Name}\";");
            sb.AppendLine("}");
            spc.AddSource("Tools.g.cs", sb.ToString());
        });
    }
}

Three things bite people the first time:

Don't capture symbols across compilations. ISymbol references are not safe to cache. Project to a record of primitives in your transform step, then operate on the record. Skip this and your IDE will start crawling after the second file.

Look in obj/generated/ when something's off. You'll see the actual emitted code, not what you think you wrote. Half my source-gen bugs were "oh, that's not actually emitting".

Profile your generator. A slow generator means a slow IDE, all day, for everyone on the team. The dotnet build --binarylogger trick + the SourceGeneratorAnalyzer let you spot the offender before it ships.

Worth the effort for: DTO mappers, OpenAPI descriptors, MCP tool registration, EF compiled queries, anything where you'd otherwise reach for runtime reflection. Skip it for: things you'll only write once, anything where the codegen is more code than the boilerplate it replaces.

That last one is the real test. If the generator is bigger than the duplication, you've just moved the boilerplate somewhere worse.