Skip to main content
Back to Blog
Source Generators Are Not Free

Source Generators Are Not Free

4 min read762 views0 likes
#dotnet#roslyn#source-generators#build-performance#nuget

A source generator is a build-time dependency, and build time is a shared resource. What generators actually cost, the packaging trap, the debugging question, and when they're clearly worth it.

Andrew Lock wrote about splitting the NetEscapades.EnumGenerators packages on the road to a stable release — separating the generator from the attributes it consumes so that consumers aren't forced to take a compile-time dependency they don't need.

That packaging problem is worth understanding even if you never ship a source generator, because it's a symptom of something teams consistently underestimate: source generators are a build-time dependency, and build time is a shared resource.

I'm generally in favour of them. But the decision to adopt one deserves more thought than "this removes boilerplate", which is how it usually gets made.

What a Generator Actually Costs

When you add a source generator, you're adding code that runs inside the compiler, on every build, on every developer machine, and on every CI run.

A generator that takes 200ms sounds free. Multiply it by every build a team of twenty does in a day, plus every CI run, plus every incremental rebuild while somebody is iterating on a test, and it stops being free. Worse, it's a cost paid in the place developers are least tolerant of it — the gap between saving a file and seeing a result.

The modern incremental generator APIs exist precisely to make this tractable, and a well-written generator that caches properly is genuinely cheap. A poorly written one that recomputes the world on every keystroke is a tax the whole team pays and nobody attributes correctly, because "the build feels slow lately" doesn't point at anything.

The Packaging Trap

Andrew's post is about the specific shape of this problem: a generator typically ships with attributes that consumers apply to their code. Bundle them in one package and every consumer takes a dependency on the generator assembly to use an attribute. Split them and you have two packages whose versions must stay compatible.

Neither is obviously right, which is why it takes a few releases to settle, and it's the part of shipping a generator that surprises people who assumed the hard part was the Roslyn API.

If you're evaluating a generator to consume, this is a useful maturity signal. A project that has thought carefully about how its packages are split is a project that has thought about your build. One that ships a single assembly containing generator, attributes, and runtime helpers has pushed a decision onto you.

The Debugging Question

The question I'd ask before adopting any generator: when this produces something unexpected, how long until someone on my team understands why?

Generated code is real code that nobody wrote. It shows up in compiler errors referencing files that don't exist in the repository. Stepping through it requires emitting it to disk first, which most people haven't configured. If the generator's output depends on subtleties — partial declarations, nullable context, accessibility — the failure mode is a compiler error at one remove from its cause.

Good generators mitigate this with clear diagnostics. It's worth checking whether a generator produces helpful errors when you misuse it deliberately, before you adopt it. Try it wrong on purpose and see what it tells you.

When They're Clearly Worth It

Replacing reflection on a hot path. This is the strongest case. Moving work from runtime to compile time is a genuine win, and the generated code is usually faster than anything hand-written that stays maintainable.

Eliminating genuinely mechanical boilerplate. Enum helpers, logging boilerplate, serialisation glue — code that's identical every time and wrong occasionally when written by hand.

Enforcing a pattern at compile time. A generator that makes an invalid state fail to compile is worth more than documentation asking people not to do it.

When I'd Hesitate

When it's hiding a design problem. Boilerplate is sometimes a signal that an abstraction is missing. Generating the boilerplate faster doesn't address that, it entrenches it.

When only one person understands it. A custom in-house generator is a piece of compiler infrastructure your team now maintains. That's a real ongoing commitment, and it should be a deliberate one rather than something that happened because an engineer had an interesting week.

When the alternative is one small class. Not everything repetitive needs automating.

The Practical Advice

If you consume generators, prefer ones from maintainers who treat packaging and diagnostics as part of the product. That care is evidence of care elsewhere.

If you write one, measure it. Build a project with and without it, at realistic scale, and look at the difference. The compiler has diagnostics for generator timing — use them before shipping, not after somebody complains.

And whichever side you're on, remember what the split-package problem is really telling you: a source generator isn't a library your code calls. It's something your consumers' compilers run, which makes its cost everyone's problem and its design a matter of manners.

Source: Splitting the NetEscapades.EnumGenerators packages, Andrew Lock.

© 2026 Ahmed Shaltoot. All rights reserved.