Source Generators: Less Boilerplate, Faster Startup

Source Generators: cut boilerplate and speed up startup

Stop paying runtime costs for reflection. Learn how a tiny attribute plus generated code trims boilerplate and can improve app startup time.

.NET Nuggets·By amarozka · October 15, 2025

Source Generators: cut boilerplate and speed up startup

Still hand-writing ToString, factories, or mappers? You’re paying that cost at runtime or in code reviews. What if the compiler stamped out that boring code at build time instead?

What a Source Generator actually does

A Source Generator is a Roslyn plug‑in that runs during compilation and adds C# files to your project. No reflection at startup, no runtime emission. You write small hints (usually attributes); the generator inspects your syntax tree and produces code before your app runs.

Why you should care

  • Boilerplate reduction: let the compiler write the repetitive parts.
  • Startup gains: replace reflection-heavy scanning with generated code.
  • Safer refactors: generated code is typed, shows errors in the IDE.

Tiny example: one attribute → one method

Below is a minimal, compilable demo. The block marked generated would normally be produced by a generator; here it’s inlined so the snippet runs end‑to‑end.

using System;

[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class AutoToStringAttribute : Attribute { }

[AutoToString]
public partial class User
{
    public string Name { get; }
    public int Age { get; }
    public User(string name, int age) => (Name, Age) = (name, age);
}

// --- generated by a Source Generator ---
public partial class User
{
    public override string ToString()
        => $"User(Name={Name}, Age={Age})";
}

public static class Program
{
    public static void Main()
    {
        Console.WriteLine(new User("Ana", 42));
    }
}

How the generator emits that partial

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

[Generator]
public sealed class AutoToStringGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext ctx)
    {
        var classes = ctx.SyntaxProvider.CreateSyntaxProvider(
            static (s, _) => s is ClassDeclarationSyntax c && c.AttributeLists.Count > 0,
            static (c, _) => (ClassDeclarationSyntax)c.Node);

        ctx.RegisterSourceOutput(classes, static (sp, cls) =>
        {
            if (!cls.AttributeLists.ToString().Contains("AutoToString")) return;
            var name = cls.Identifier.Text;
            sp.AddSource($"{name}.AutoToString.g.cs",
@"// <auto-generated/>
public partial class " + name + @"
{
    public override string ToString() => \"" + name + "(Name={Name}, Age={Age})\";
}");
        });
    }
}

How it works in a real project

  1. You tag a class with [AutoToString].
  2. The generator finds that attribute and emits a matching partial with override ToString().
  3. The compiler merges both partials. No reflection needed when the app starts.

Compile-time cost vs runtime wins

Source Generators shift work left:

  • Build time increases a bit (the generator runs), especially if it scans the whole solution.
  • Startup time often drops because you remove reflection and dynamic code paths.
  • Incremental generators cache results between builds; when inputs don’t change, they don’t re-run.

Rules of thumb from my projects

  • Use a generator when the runtime alternative would scan assemblies or use heavy reflection.
  • Keep outputs small and focused; emit only what you’ll call.
  • Prefer IIncrementalGenerator over the classic ISourceGenerator for better build perf.

Common pitfalls (and quick fixes)

  • Full-compilation scans hurt builds. Fix: use SyntaxProvider to filter to the exact nodes (e.g., classes with your attribute) instead of walking every symbol.
  • Non-deterministic output (timestamps, GUIDs) causes rebuild loops. Fix: generated text must be stable for the same inputs.
  • Editing generated files by hand leads to lost changes. Fix: write to obj/ as the compiler does, and mark files as generated so IDE hints make that clear.

When it breaks

  • Big solutions + naive generators = minutes added to CI. Start with one focused feature, measure, then expand.

Quick detection

  • Turn on a binary log: dotnet build -bl. Check the generator’s contribution to build time. Trim input set if it’s high.

Where to use them

  • DTO mappers, ToString, Equals/GetHashCode, builders.
  • DI source for known registrations (instead of assembly scans).
  • API clients from contracts (attributes or .json schemas).

Checklist

  • Add small attributes to mark what should be generated.
  • Use IIncrementalGenerator + SyntaxProvider.
  • Keep outputs tiny and deterministic.
  • Measure with dotnet build -bl.
  • Remove runtime reflection where possible.

Try it now (5 minutes)

  1. dotnet new console -n GenDemo
  2. Add a class library GenDemo.Generators with Microsoft.CodeAnalysis.CSharp and set <IsRoslynComponent>true</IsRoslynComponent>.
  3. Implement an IIncrementalGenerator that picks [AutoToString] and emits the partial ToString.
  4. Reference the generator as an Analyzer in the app project.
  5. Tag a class with [AutoToString] and run. Compare startup vs a reflection-based version.

Conclusion

Let the compiler handle the boring bits and earn back startup time. Start with one tiny feature like ToString and grow from there. What piece of boilerplate will you delete first?

Leave a Reply

Your email address will not be published. Required fields are marked *