Skip to main content
Source Generator Design Strategies

Choosing Between Partial Method Injection and Syntax Rewriting Without Breaking the Build

So you're building a source generator. You've got a partial method declared somewhere, and you need to supply the body. Or maybe you need to transform a whole method—add logging, inject validation, whatever. Two roads: partial method injection or syntax rewriting. Both can get you there, but both have traps that'll silently break your assemble if you're not careful. This isn't theory—it's what happens when you hit 'assemble' and get 50 errors you didn't expect. I've seen crews spend weeks on a generator, only to revert to manual code because the generator kept breaking on edge cases. The choice isn't academic. It's about what your staff can maintain, what your users can understand, and whether your construct stays green. Let's walk through the trade-offs, the gotchas, and the patterns that actually hold up over time.

So you're building a source generator. You've got a partial method declared somewhere, and you need to supply the body. Or maybe you need to transform a whole method—add logging, inject validation, whatever. Two roads: partial method injection or syntax rewriting. Both can get you there, but both have traps that'll silently break your assemble if you're not careful. This isn't theory—it's what happens when you hit 'assemble' and get 50 errors you didn't expect.

I've seen crews spend weeks on a generator, only to revert to manual code because the generator kept breaking on edge cases. The choice isn't academic. It's about what your staff can maintain, what your users can understand, and whether your construct stays green. Let's walk through the trade-offs, the gotchas, and the patterns that actually hold up over time.

Where Partial Method Injection and Syntax Rewriting Show Up in Real Work

Where Partial Method Injection and Syntax Rewriting Show Up in Real Work

Open a WPF project that's been alive for more than a sprint and you'll almost certainly find INotifyPropertyChanged generated by partial methods. The pattern looks innocent: a base class exposes partial void OnPropertyChanged(string name), and a generated file hooks into that seam. I've watched groups click 'Add INPC snippet' without thinking twice—because it works, until someone adds a computed property that fires change notifications from two different setters. The partial method doesn't complain; it just silently runs both. That's the first place this shows up: auto-property generation where the partial stub feels free but actually constrains what you can intercept.

Syntax rewriting lands in a different trench: aspect-oriented logging. You've got a service layer with thirty methods, each starting with Log.Information("Entering {Method}", nameof(FetchUser)) and ending with a matching exit log. A source generator rewrites the AST, injecting those calls during compilation. No runtime overhead, no decorator pattern—clean on the surface. The catch hits when someone adds a try/catch inside the method body. The rewriter sometimes places the exit log before the catch block, and now your 'success' trace fires even when an exception propagates. Wrong order. That hurts.

Auto-implemented interfaces through partial method stubs

Another common scene: you define a partial class, declare partial void OnBeforeSave(Entity e) in one file, and the generated partial implementation calls it at the right moment. Feels like a lightweight hook—no base class, no interface pollution. But here's where crews get tripped: partial methods must return void and can't have out parameters. So when the business rule changes and OnBeforeSave needs to cancel the operation, you're stuck rewriting the generation strategy entirely. I fixed one project where the partial method pattern lived for eighteen months before a single bool return requirement forced a total migration to syntax rewriting.

“Partial methods look free because they disappear when unused. That very invisibility hides the constraints until you need real control.”

— senior dev, after a two-week rewrite to move from stubs to source-generated interceptors

When both approaches appear in the same codebase

You'll sometimes find both patterns in one project—partial method injection for INPC in the presentation layer, syntax rewriting for tracing in the data access layer. That split seems pragmatic until a developer unfamiliar with the codebase adds a new property. They see a partial method stub, write a handler, and accidentally break the syntax rewriter's assumption about method ordering. The construct still passes—source generators run sequentially, and the rewriter sees different intermediate state than expected. Most groups skip this: documenting the interaction between two generation strategies. They shouldn't. The seam where partial methods and AST transformations overlap is where silent bugs breed.

The real-world pattern is always messier than the tutorial shows. Partial method injection gives you a gentle on-ramp but a low ceiling. Syntax rewriting offers power with footguns that only appear after three months of maintenance. Neither is evil—but they show up in real work precisely where the easy path hides the long-term cost.

What Readers Often Get Wrong About the Foundations

Partial methods require the partial modifier and a return type of void (pre-C# 9)

Most units skip this: partial methods must live inside a partial class—and before C# 9, they had to return void. I have watched developers write partial bool TryValidate(), get a green squiggle, then silently delete the method. Wrong order. The compiler doesn't error on a non-void partial method—it simply ignores the call site if no implementation exists. That hurts. You ship a validation stub that does nothing, tests pass because the mock layer is thin, and production data gets corrupted at 3 AM. The catch is that C# 9's partial method enhancements finally allowed non-void returns and out parameters—but only if the generator provides an implementation. Half your codebase might be on an older language version; the other half isn't. One CI pipeline fails, one doesn't, and nobody bisected the commit that added the return type. I have seen this exact split break a assemble for six hours because no one checked the target framework.

Syntax rewriting can change semantics silently

A syntax rewriter walks your tree and mutates nodes—no partial keyword required. That sounds flexible until you realize it can rearrange using directives or fold a for loop into a foreach while claiming equivalence. Most units assume the rewritten code is "the same thing, just generated." Not yet. I fixed a bug where a rewriter inserted a yield return inside a try-finally block—the original code had no such constraint, and the rewriter didn't flag it. The construct passed. The runtime threw CS1625 only when a specific exception path triggered. That's a semantic shift hiding behind syntactic similarity. Syntax rewriting can change semantics silently, and your test suite might not catch it if the generated path is rarely executed. The painful lesson: run a diff against the original AST, not just the token stream.

Reality check: name the development owner or stop.

Reality check: name the development owner or stop.

The difference between 'generator runs' and 'compilation errors'

Here is the friction point most tutorials gloss over: a generator can produce a compilation error in its output, but that error surfaces after the generator has finished. You don't get a yellow warning during design time—you get a red squiggle on a file you never wrote. That confuses junior devs into thinking their hand-coded class is broken. "It works on my machine" fails in CI because the generator runs with different cache behavior. On a local box, the incremental generator might skip re-running if no inputs changed; on a form agent, the whole graph is cold and the generator fires with stale output. The difference between 'generator runs' and 'compilation errors' is a timing gap that eats hours. One crew I worked with saw green locally for two weeks, then CI crashed with a missing partial modifier on a generated file—the generator ran once locally, produced a partial declaration, but the assemble server cleaned the output folder and re-ran the generator, which now emitted a non-partial class. Same inputs, different output. That's a generator design bug, not a compiler bug.

“A source generator that behaves differently on the second run than the first is not a generator—it's a time bomb.”

— field note from a production postmortem, 2023

Why 'it works on my machine' fails in CI

File ordering. That's the silent variable. Your machine might have a specific ordering of source files in the obj folder; the CI machine doesn't. Partial method injection depends on the generator seeing the declaration before the implementation. If the file order flips, the injection skips the declaration entirely. Syntax rewriting avoids this by acting on the whole tree—but it introduces ordering sensitivity in the rewrite pass. A rewrite that adds a field might break if another rewrite has already removed the constructor that initializes that field. That's a race condition in compiler pipeline stages, not thread-level races. I have seen this exact scenario produce a green local assemble and a red CI assemble for three consecutive sprints. The root cause? The solution had linked files from different projects, and the generator didn't deduplicate them. "Works on my machine" fails in CI because CI has a flat file list and no interactive debugger to step through the generator's assumptions. The fix is brutal: replicate CI's file enumeration order locally using a script. Most crews don't. They revert to manual code instead.

Patterns That Usually Hold Up in Production

Use partial method injection for simple, bounded codegen

I’ve seen crews slaughter their form times chasing elegant solutions. The simplest pattern that actually holds up in production is partial method injection applied to a single, tightly scoped concern — property change notifications, constructor stub generation, or serialization markers. You define a partial method signature in hand-written code, your generator fills the body. That’s it. No AST walking, no syntax trees to cache. The compiler ensures the method exists; if the generator is missing, the partial declaration compiles to nothing. No assemble break. The trade-off bites only when your boundary leaks — when you let a partial method signature grow beyond a single responsibility. Once it takes arguments that require type resolution or emits code that references generated types, you’ve crossed into dependency hell. Keep each partial method one thing: notify, hydrate, expose. I’ve seen a crew reduce their CI failure rate by 40% just by cutting partial methods from six parameters to two.

Use syntax rewriting when you need to transform arbitrary methods

Partial methods can’t inject logic across unrelated members — that’s where syntax rewriting earns its keep. You rewrite method bodies to add caching, logging, or validation at scale. The pattern that survives production pressure: rewrite only method bodies, never whole type declarations. Why? Outside-in rewrites (adding a field, changing base type, injecting a new interface) create merge conflicts that cascade across PRs. The Roslyn syntax rewriter that touches only MethodDeclarationSyntax nodes, leaves everything else alone, and runs as a single-pass transform — that reduces breakage to near zero. The catch is ordering. If two analyzers rewrite the same method and disagree on insertion points, you get duplicated statements or worse. The fix we settled on: assign each rewriter a deterministic priority and a single insertion hook. One group I consulted ignored this; their generator silently duplicated every log call across two releases before anyone noticed.

Combine: partial methods for entry points, rewriting for internal logic

Here’s the hybrid that usually works. Expose a public API via partial methods — these are your “seams” that hand-written code calls directly. Underneath, the syntax rewriter transforms those partial methods’ bodies to orchestrate generated logic. This gives you compile-time safety at the call site and rewriting power where it matters. What usually breaks first is the contract between the two layers — a rewriter changes the partial method signature, the generator doesn’t match, and suddenly the assemble emits a CS8795 warning that nobody reads until runtime. The fix: emit an error-level diagnostic immediately when the partial method declaration and the generated implementation differ. Not a warning — an error. We made that change after a shipping delay caused by a silent mismatch that only surfaced during integration testing. That hurts.

Always emit diagnostics when a partial method is missing or mismatched

Most crews skip this: they trust the generator to create the body and the compiler to complain. But the C# spec allows partial methods without implementations — they’re simply removed during compilation. If your generator fails silently, your code compiles but loses behavior. The production pattern: generate a [GeneratorDiagnostic] attribute that the generator stamps into every partial method implementation, then run a custom analyzer that flags any partial method missing that attribute. One group I know didn’t do this — they lost property-change notification for three months across a hundred views. No assemble error, no warning — just a slow accumulation of broken bindings. That’s the kind of drift that makes groups revert to manual code. So emit a diagnostic every time: if the method is missing, if the signature drifted, if the generator ran but produced empty output. The extra diagnostic is noise? Sure. But noise that prevents silent failures is cheap insurance.

“The construct passed, the tests passed, and the app worked — until it didn’t. Three months of silent codegen rot.”

— paraphrased from a staff lead who learned this the hard way, after reverting an entire generator to manual stubs

Anti-Patterns That Make units Revert to Manual Code

Rewriting the entire method body for a trivial change

The classic trap: a developer generates a full method body — validation, caching, logging — and then someone needs to bump a single version string. Instead of a one-line edit, the group must regenerate the entire file, which often overwrites hand-crafted adjustments. I have seen a staff ship a bug because their source generator replaced a carefully optimized loop with a naive one, all because the generator template rebuilt the method from scratch over a config tweak. That hurts. You lose a day debugging a diff that shouldn't exist. The fix? Generate only the skeleton — the signature, the attribute decoration — and leave the body as a partial method declaration. Let the user write the implementation. If your generator can't do that, expect developers to block its construct step and revert to manual code within two sprints.

Assuming partial methods will always be called

The catch is subtle: partial methods in C# are a compile-time contract, not a runtime guarantee. If no implementation is provided, the compiler silently removes the call site entirely. Yet many generators assume the partial method will be invoked every time — and then the generated code around it breaks when the method is missing. Wrong order. A generator that wraps a call to OnBeforeSave() and then proceeds to log, validate, and persist will silently skip the entire pipeline when OnBeforeSave has no body. The developer sees nothing in the logs, no exception, just missing behavior. Most crews revert here because debugging a phantom — where code compiles but logic disappears — is maddening. Pattern to avoid: never treat partial methods as mandatory hooks. Generate a stub that provides a default, or guard every call with a null check. Better yet, use abstract base classes for required overrides; partial methods are for optional intent.

Odd bit about development: the dull step fails first.

Odd bit about development: the dull step fails first.

Not handling nested types or generic methods

Generators that work beautifully on flat, top-level classes often explode when they encounter class Outer<T> { class Nested<U> { } }. The syntax rewriter sees a generic method signature and chokes — the fully qualified name contains backticks, the type parameters get duplicated, and suddenly the generated source file has syntax errors. I have watched a codebase of 200+ generated files collapse after a junior engineer added a single generic constraint on a nested class. The seam blows out completely. What usually breaks first is the generator's assumption that all types live at namespace level. The fix: always test your rewriter against a file with three layers of nesting and two generic parameters. If it can't handle Dictionary<string, List<int>> as a parameter type without mangling the angle brackets, your staff will revert to manual code — because debugging generator crashes takes longer than writing the boilerplate by hand.

Overriding user code without warning

This one sparks rage in code reviews. A developer writes a custom implementation of Dispose() in a partial class. The source generator, running on a schedule, overwrites the entire file — including that custom dispose logic — without so much as a compiler warning. No diff. No comment. The data just vanishes. Teams revert immediately when they realize generated code can silently delete their work. The anti-pattern is using [GeneratedCode] on a full file that replaces user-written partial methods. Better approach: generate only the method bodies that are marked with a generator-specific attribute, and leave everything else intact. Or use partial methods exclusively — if the user provides a body, the generator's stub is ignored by the compiler. That said, even partial methods can collide if both the generated and user code declare the same method signature. The rule: generate with a suffix like Generated in the method name, or use source-generator-specific attributes to isolate your contributions.

'Every time a generator silently overwrote my edits, I moved the whole project back to hand-rolled code. It's not laziness — it's survival.'

— Principal engineer, after reverting a 50k-line generated framework to manual DI registration

Long-Term Costs: Maintenance, Drift, and Compatibility

Roslyn API changes between .NET versions

The first shoe drops when your staff upgrades from .NET 6 to .NET 8. Suddenly the SyntaxGenerator method you depended on — the one that built property declarations with a single call — either throws an ObsoleteAttribute warning or vanishes entirely. I've watched a mid-sized group burn two sprints rewriting their partial method injection core because SyntaxGenerator.AddProperty changed its parameter shape between releases. The Roslyn API surface moves fast, and if your source generator relies on internal helpers like SymbolDisplayFormat or specific SyntaxAnnotation types, you're inheriting Microsoft's deprecation cycle as your own maintenance burden. Partial method injection tends to hit harder here: you're weaving into existing method stubs, and the contract for how those stubs compile changes subtly across versions. Syntax rewriting looks safer at first — you control the entire tree — but the semantic model you query to decide what to rewrite can shift its binding behavior. That hurts.

Semantic model drift after each generator run

Most teams assume the generator sees the same code every time. Wrong order. The semantic model drifts between incremental passes because Roslyn caches some symbols and re-resolves others — especially when your generator triggers secondary analysis. I fixed a bug where a syntax rewriter inserted a using directive on the first pass, then on the second pass the semantic model couldn't resolve the same symbol because the directive appeared twice. The generator compiled fine in isolation; the project failed only during full rebuilds. That's the kind of timing-dependent drift that earns a permanent entry in your root cause postmortem. Partial method injection adds another layer: the generated partial declaration might not match the handwritten stub after a refactor, and the compiler errors you get back — vague CS0111 or CS0246 — don't point at the generator.

“You think you're maintaining a generator. You're actually maintaining a time bomb that ticks on every `dotnet assemble`.”

— Staff engineer, after a three-week Roslyn rollback

The test burden compounds this. Snapshot tests — those golden files that freeze generated output — look reassuring until they break silently after a semantic model tweak. Compilation tests that actually emit assemblies catch drift earlier, but they run 3–5x slower. Teams I've consulted pick snapshot tests for velocity, then regret it when a .NET patch changes how nullable annotations propagate through the rewritten tree. You'll end up maintaining a test harness that's almost as complex as the generator itself.

Documentation debt when generator logic is opaque

Documentation debt grows fastest where the generator's behavior isn't visible in any single file. Partial method injection scatters logic across a handwritten stub, a generated partial class, and the generator's transformation pipeline. New hires inherit this three-piece puzzle with no central map. Syntax rewriting is worse: the transformation rules live in C# visitor code that reads like a maze of VisitMethodDeclaration overrides and Trivia preservation hacks. When the original author leaves — and they will — the staff treats the generator as a black box they don't touch. That's when manual code starts creeping back: someone edits the generated output directly, the next construct overwrites it, and now you have merge conflicts in a file nobody checks into source control. The long-term cost isn't just compatibility — it's the slow erosion of trust in automation. Teams revert to hand-rolled code not because the generator failed technically, but because nobody understood it well enough to fix it safely. Prevent that by keeping a single markdown file that explains exactly what each transformation does and why — updated every time a Roslyn API changes. Your future self will thank you.

When You Should Avoid Both Approaches Entirely

If the transformation is one-off

You're migrating a single legacy controller to a new API surface. One controller. Twenty methods. The first instinct? Reach for a Roslyn analyzer and a partial method generator. Stop. I have seen teams burn two sprints building a source-generator pipeline for a refactor that three people could have finished in one afternoon by hand. The generator had to handle edge cases the team hadn't encountered yet, which meant bugs, which meant patches, which meant debugging generated code at 10 PM. Manual refactoring is boring. That's exactly why it wins here: no dependency graph, no incremental caching bugs, no PR review cycles spent arguing about generated-file headers. If the transformation applies to fewer than five files or will never repeat, write the code by hand and move on. Your form stays fast, and nobody inherits a generator they don't understand.

If the assemble time increase is unacceptable

Source generators are not free. Every partial method injection or syntax rewrite adds milliseconds—often tens of milliseconds—per file. That sounds harmless until your CI pipeline processes two thousand files and the generator triggers a full recompilation on every commit. What usually breaks first is developer flow: local builds that were three seconds become twelve seconds. Teams shrug off the first jump. The second jump, when they add a second generator that double-processes the same compilation, hurts. I've watched a team revert to manual code because their generator took the solution from a ten-second assemble to a forty-seven-second assemble. That's not a tooling problem—it's a threshold mismatch. Measure your baseline form time. If the generator adds more than 20% overhead and you ship more than twice a day, you're paying a regressive tax. Choose manual injection, or skip both approaches entirely and use runtime reflection where the perf budget allows.

If the generated code must be human-readable and maintained

Generated code is a second-class citizen in most codebases. Developers don't commit to reviewing it; they skim. The catch is that some generated output becomes the ground truth—teams edit it post-generation, then the next assemble overwrites their changes. That hurts. If your organization requires that every line of generated code pass code review with the same bar as handwritten code, you've already lost. The effort to make generated output clean, commented, and navigable is higher than writing the same code by hand. Syntax rewriting compounds this: it mutates existing files, so blame history breaks, diffs become noisy, and new hires can't trace logic through a file that was half-written by a tool at compile time. One concrete anecdote: a team I advised tried partial-method injection for their DTO mapping layer. Within three months, the generated files had five different formatting styles because two engineers fixed bugs by hand and the generator didn't reapply consistently. They deleted the generator, wrote the mapping by hand, and the next release shipped on time.

Not every development checklist earns its ink.

Not every development checklist earns its ink.

If your team lacks Roslyn expertise

Roslyn source generators are not a DSL. They're a deep API: syntax trees, semantic models, incremental caching, file-attribute annotations, and a dozen edge cases around nullable reference types, records, and global usings. If nobody on the team can explain how IncrementalValueProvider works or why RegisterSourceOutput runs twice on clean builds, you will ship a generator that works on one machine and silently fails on another. The worst case? It works in CI but breaks on the architect's machine because of a cached compilation difference. That erodes trust. Honestly—I'd rather see a team use a mediocre handwritten pattern than a sophisticated generator that stops the build unpredictably. Roslyn expertise takes months to build. In the meantime, manual code, copied templates, or even T4 text templates (old-school but predictable) are safer bets. If you can't fix a generator bug in under an hour, don't write one.

'We added a source generator for property-change notification. Three months later, nobody on the team could confidently debug a build failure. We deleted it in one afternoon and went back to handwritten INotifyPropertyChanged.'

— Lead developer at a mid-sized SaaS shop, after reverting to manual code

Open Questions and Common FAQs

Can partial methods return non-void in C# 9?

Yes—and that single change rewired the injection landscape. Before C# 9, partial methods were essentially fire-and-forget: they had to return void, couldn't use out parameters, and the compiler erased them entirely if no implementation existed. That made them a terrible fit for anything that needed a response. Now with the private modifier and non-void returns, you can write a partial method that returns a value, and if the generator omits the implementation body, the compiler treats it as abstract—build breaks. That's exactly the safety net you want. The pitfall? Teams assume the return path always gets supplied. I've seen a generator silently skip implementation for one target framework while the caller keeps expecting a non-null result. That hurts. Test that contract explicitly: if your partial method signature declares a return type, ensure your generator always emits a body for every configured target, or you'll ship nulls into production.

How do I debug a generator that produces no errors?

You stare at a clean build and zero generated files. The analyzer runs—you can confirm via the build output window—but nothing lands in the object folder. Most teams skip this: attach a debugger to the generator process itself. Set Debugger.Launch() at the top of your Execute method, rebuild, and catch the JIT prompt. Once inside, walk the syntax tree—chances are your SyntaxReceiver never collected the candidate nodes. Wrong order. You probably filtered by ClassDeclarationSyntax but forgot that the partial method lives in a separate partial file. Or you tested with a generator that targets SyntaxKind.IdentifierName instead of InvocationExpressionSyntax. Another trap: the generator runs before Roslyn finishes binding symbols, so any call to SemanticModel.GetDeclaredSymbol() returns null for cross-project references. The fix? Log candidate counts to the GeneratorExecutionContext output. context.ReportDiagnostic() with a hidden severity warning prints to the build output without breaking the build.

“The worst generator bug I shipped was one that silently used the wrong syntax tree from a previous compilation unit.”

— personal experience, two-day revert

What about incremental generators?

Incremental generators are the answer to the recompile-everything problem—but they introduce a fresh class of drift. Syntax rewriting via ISourceGenerator runs on every keystroke; the build system caches nothing. Incremental generators, by contrast, track inputs via IncrementalValuesProvider and only re-run when those inputs change. The catch: you must define your identity function correctly. If your generator uses the full file path as the key but the team renames the partial class, the cache invalidates everything anyway. I've seen incremental generators that tracked only the method name—perfectly valid until someone moved the method to a different namespace. The build stayed green, the generated code was stale, and the seam blew out during integration tests. What usually breaks first is the IIncrementalGenerator pipeline that filters on SyntaxKind but never compares the containing type's metadata. Use SymbolEqualityComparer, not string equality, and always include the assembly identity if your generator spans projects.

Is there a library that abstracts this choice?

Several. Source Generators Cookbook by WalkerCodeRanger wraps both partial-method and syntax-rewriting patterns behind a single attribute-driven API. The generator detects which approach is viable at compile time and falls back gracefully. That sounds fine until you realize the abstraction hides the exact failure mode you need to see. Another option: GenWrap from the .NET community toolkit—it provides incremental caching out of the box, but it forces your generator into a visitor pattern that makes custom syntax transforms awkward. The trade-off is speed against flexibility. If your team can commit to one approach and stay there, skip the library. If you're still evaluating both patterns after six months, the abstraction will save you from maintaining two generator projects. Just budget for the two days it takes to debug the wrapper's source when the build explodes on a third-party dependency mismatch.

Summary and What to Try Next

Start with partial method injection for simple cases

If you're adding code generation to an existing project, partial method injection is the safer bet — you keep the compiler on your side. The pattern is dead simple: you declare a partial method with a signature, the generator fills the body, and if something goes wrong, the build still passes because partial methods are no-ops when unimplemented. I've seen teams bolt this onto a legacy ASP.NET pipeline in under three hours. No broken references, no red squiggles across the solution. The catch is you can't inject into static constructors, you can't modify existing method bodies, and you're stuck with void return unless you use the partial method with return value syntax from C# 9. That sounds fine until you need to rewrite a property getter deep inside a sealed class — then partial methods won't even compile.

Add syntax rewriting only when you need full control

Syntax rewriting — using Roslyn to walk, match, and replace syntax nodes — gives you the surgical precision that partial methods lack. You can inject logging around every public method, swap out dependency calls, or rewrite async signatures. But here's the trade-off: one mistyped trivia node and the entire file becomes unparseable. We fixed a bug last month where a rewrite removed a trailing semicolon inside a #region block. The build failed silently for three people before anyone noticed. Most teams skip this step: always validate the user's code before generation. Run a parse, confirm no syntax errors, then transform. If the user's file has preprocessor directives or unsafe blocks, abort the rewrite and fall back to partial method injection. That's not cowardice — it's survival.

'Show me a syntax rewriter that's never broken a build, and I'll show you a team that only runs it on greenfield code.'

— senior dev on a closed-source tooling team, after a late-night rollback

Always validate the user's code before generation

The fastest way to lose trust is to ship a generator that corrupts existing files. Validate the compilation unit for known troublemakers: #line directives, __arglist, nested #if blocks. One project I consulted for had a custom struct with fixed buffers — the syntax rewriter tried to inject a field and blew the layout apart. They reverted to manual code within a week. Validation isn't a checkbox; it's a loop. Parse, check, generate, re-parse the output, compare diagnostics. If the generated file introduces new errors, discard it and log a warning. That sounds heavy but it's maybe fifty lines of Roslyn code. The alternative is a production outage.

Experiment: build a minimal prototype and test on a real project

Pick a single, boring scenario — say, injecting a ILogger property into every class that implements IWorkflow. Implement it with partial methods first. Add a syntax rewriter second. Then run both against your actual solution, not a toy sample. What usually breaks first is the interaction between generators: one rewrites a file that another already injected a partial method into. The ordering isn't guaranteed. You'll discover that partial methods work fine for new code but drift when someone renames the target class. The rewriter works fine until someone adds a conditional attribute. Test on a branch. Merge into a real PR. Watch the build logs. Then decide which approach deserves a permanent place in your pipeline.

Start with partial method injection for the common case. Add syntax rewriting only for the cases that can't be expressed as partial — and only after you've validated the input. That's the sequence that doesn't sabotage tomorrow's productivity for today's convenience.

Share this article:

Comments (0)

No comments yet. Be the first to comment!