You’ve just added a Roslyn analyzer to your C# project. Clean code, better catches—until it flags a generated file. Suddenly your assemble is red for code you never wrote. The temptation is to silence the analyzer entirely. But that’s like ripping out the smoke detector because it chirps at burnt toast. You need a scalpel, not a sledgehammer.
This article walks through the decision: what to fix first when your analyzer cries wolf on generated code. We’ll compare real options, not vendor lists. Each section builds toward a practical answer—no fluff, no guaranteed results, just hard trade-offs.
Who Must Decide — and by When
The developer who owns the assemble pipeline
You — the person who wakes up to a red CI badge and a Slack channel full of grumpy teammates. This isn't abstract architecture. It's your Monday. The false-positive Roslyn analyzer fires on a file nobody wrote by hand — auto-generated from a T4 template, maybe, or spit out by a source generator. The construct fails. Someone says "just suppress it globally." That's poison. Once you suppress one generated file, you lose the ability to tell real violations from noise. I have seen crews paint themselves into that corner inside two sprints.
Your decision window is tight. If you wait until the next sprint planning, the suppression pile grows. Developers get used to ignoring analyzer warnings — they stop trusting the tool entirely. The catch is that your fix isn't purely technical; it's political. You need authority to say "we fix this at the generator level, not by muting the analyzer." Without that, the rest of this article is a nice theory you'll never execute.
Project lead facing a broken CI
What usually breaks first isn't the analyzer logic — it's the lack of a clear owner. The code is generated by a third-party package. The analyzer is from an internal NuGet. The CI pipeline is maintained by DevOps. When the construct fails, everyone points at someone else. A team I worked with wasted three days in this loop before someone finally mapped the generated file paths and found the culprit: a source generator that emitted nullable annotations the analyzer didn't expect.
The decision owner must be the person who can merge changes to both the analyzer configuration and the code-generation pipeline. That's usually a lead developer or a senior engineer — someone who doesn't need a ticket to edit a `.targets` file. Wrong order: assigning it to a junior who can only add suppressors. That hurts. You'll get a bandage, not a fix.
Honestly, the most common failure mode isn't technical ignorance — it's hesitation. groups know what to do but stall because nobody wants to touch the generated code boundary. "It's not our code," they say. Yet the assemble fails on your CI. That's ownership by default.
Deadline pressure: before next sprint review
Your deadline is not next quarter. It's before the sprint review, because that's when stakeholders ask why the construct turned red and what you did about it. A false-positive flood that takes down the pipeline once becomes a recurring incident if you postpone the decision. The fix might be simple — maybe you need to add `// ` at the top of the file, or configure the analyzer to skip files matching `**/*.g.cs`. But simple doesn't mean fast when you're still arguing about who decides.
'We'll handle it in the next sprint' translates to 'we'll let it rot until someone screams louder.'
— Principal engineer, after the third failed PR review
That sounds fine until the fourth false positive surfaces on a generated API client two hours before a major release. Then the decision gets made in a hallway, under pressure, without considering the trade-offs. You want structure before that hallway conversation. The three options in the next section give you that structure — but only if you've already decided who pulls the trigger.
Start here: map your generated file sources today. Figure out which team or person controls each pipeline. If you don't know, that's your first concrete action — and you need it done before your next CI run. Not next week. Now.
Three Ways to Tame False Positives
.editorconfig rule suppression — the zero‑code lever
You can silence a false positive without touching a single C# file. Drop an entry into your .editorconfig that targets the generated file pattern, and the analyzer vanishes. For example, if your source generator writes files into obj/ or *.g.cs:
[*.g.cs]dotnet_diagnostic.CA2000.severity = none
That hides the violation for every file ending with .g.cs. Cheap, obvious—and dangerously blunt. I once saw a team suppress CA2000 across all generated code only to discover three weeks later that a real, hand-written file also matched the pattern because someone had renamed it OrderService.g.cs by accident. The catch: .editorconfig rules apply to file paths, not the logical origin of the code. You can narrow the glob, but you're still painting with a wide brush.
'We killed the noise, but we also killed the signal for any file that happened to end with .g.cs.'
— Lead dev, three weeks after the blanket suppress
Worth it for quick triage — not for long‑term hygiene. Best paired with a comment in the file that explains why the rule is off.
Source generator #pragma injection — surgical, if you own the gen
When you control the source generator, you can emit #pragma warning disable directly into the generated output. Right before the offending line, your generator writes:
#pragma warning disable CA2000 // Dispose objects before losing scope
That targets one diagnostic in one location. No global side effects. The trade‑off? It lives inside generated text, which means the next maintainer might never find it unless they crack open the generator source. Worse — if the generator is from a third‑party NuGet package you don't control, you're stuck. You'd have to fork it or post‑process the file, and that's a rabbit hole nobody enjoys. I have seen one team embed a custom MSBuild target that patched generated files after compile — fragile, slow, and blew up on the form server twice before they gave up.
This approach works best when you are the generator author and the false positive is stable across builds. Otherwise you're maintaining a phantom workaround that breaks silently on the next generator update.
Custom DiagnosticSuppressor — the heavy lifter
Write a DiagnosticSuppressor that runs inside the analyzer pipeline. You register it in your project, implement SuppressDiagnostic, and inspect the syntax tree to decide: "Is this node inside a generated region? If yes, suppress." Real code looks like:
if (context.Compilation.SyntaxTrees.Any(t => t.FilePath.Contains(".g.cs") ) )context.ReportSuppression(Suppression.Create(descriptor, diagnostic));
Precise, programmable — and it requires a dedicated assembly, a reference in every project that needs it, and a dev who understands the Roslyn symbol API. The setup cost is real. However, once it's working, you suppress nothing you didn't intend to. And you can add heuristics: only suppress for types decorated with [GeneratedCode], or only for partial classes that live in auto‑generated namespaces. Most crews skip this because it feels like overkill — until the false positives hit double digits per assemble.
One rhetorical question worth asking: would you rather debug a misplaced .editorconfig glob for an hour, or debug a suppressor that accidentally swallows a legitimate warning? Both hurt. The suppressor at least gives you a single code path to test.
How to Compare These Options
Maintainability cost — the hidden tax nobody budgets for
The simplest fix often feels like the right one. You add a GeneratedCodeAttribute suppressor, the red squiggles vanish, and your PR barely changes. What usually breaks first is your definition of "simple" six months later. I have seen groups decorate every generated partial class with that attribute — then discover that a SDK update stopped emitting the attribute on one type. Suddenly, fifteen analyzers fire on a file you thought was silenced. Maintainability cost isn't about writing the suppression; it's about auditing it every time the generator version bumps. If your team rotates members often, ask: can a junior dev trace why a rule doesn't fire, or will they cargo-cult the <auto-generated> comment because "that's what we do here"? That hurts.
Rule-level suppressions in .editorconfig scale better — but only if you name the reason in the comment. Otherwise you're building a graveyard of pragmas nobody dares touch.
Performance overhead per assemble — or why your CI doubles in time
Most units skip this: measuring what suppressing doesn't run. When you silence a rule globally, the analyzer still executes — it just doesn't report. That's wasted CPU cycles on every assemble. The catch is insidious: generated code often constitutes 30–60% of your compilation units in a modern .NET project (source generators for JSON serialization, RegEx, or EFCore models). You're paying the full analysis cost for code you'll never read. A concrete anecdote: we fixed a false-positive spike by excluding the entire /obj/ output from one analyzer, and form time dropped 14% on a mid-size solution. That's free performance — no caching, no hardware upgrade — just honesty about what you're analyzing.
But file-level exclusions (GeneratedCodeAttribute or path-based skip) short-circuit the analyzer entirely. They skip both analysis and reporting. If your assemble server is already gasping, this matters more than any elegance argument.
Scope control: file-level vs rule-level — pick your battlefield
File-level suppression is a blunt hammer. You hit all rules against one file. Rule-level suppression is a scalpel — one rule, across many files. The tricky bit is that generated code seldom breaks just one rule. A typical T4 template output might violate naming conventions, missing documentation, and async usage patterns simultaneously. Silencing one rule per file bloats your config. Silencing all rules per file hides real bugs that happen to live in the same auto-generated region. Wrong order: if you apply file-level exclusion first, you lose visibility into genuine issues your source generator introduces. Not yet proven, but I've debugged a production crash caused by a generated Equals() override that the file-level exclusion hid for two sprints.
The right scope is the one you can re-evaluate after every generator update — not the one that requires three approval cycles to change.
— lead engineer, reflecting on a post-mortem after false negatives shipped to production
Start with a three-month probation period: apply rule-specific suppressions only to the Generated directory via editorconfig pattern, then review which rules actually fire. Most units over-suppress in week one and walk it back by week four. The real decision isn't which method — it's how quickly you can correct when you've guessed wrong.
Trade-Offs at a Glance
Short-term fix vs long-term debt
The quickest path — turning off the analyzer for generated files entirely — feels like a win at 4 pm on a Friday. You suppress the noise, your CI goes green, and you ship. I have seen crews do exactly that, then wake up six months later to a generated file that silently introduced a null-ref bug. That suppression attribute? It’s now buried in a globalconfig that nobody remembers editing. The catch is that every minute you save today compounds into an hour of head-scratching later. A targeted filter — say, excluding only *.g.cs files from one specific tool — keeps the guardrails up without silencing the entire highway. But targeted filters take time to write and test; that's the upfront cost most managers hate approving.
Wrong order here hurts. You apply a blanket suppression, the false alarms stop, and your team moves on. Then a new code generator arrives, its output lands in a different folder, and suddenly the suppression rule doesn't match. The seam blows out. Not because the approach is wrong — but because you never documented the decision. One concrete anecdote: I once traced a three-day debugging spiral back to a single line of generated code that dereferenced a null this. Our blanket exclusion had masked it for fourteen releases. That hurts.
Team size matters
A solo developer or a two-person shop can probably eyeball every generated file and spot what's real vs noise. You don't need elaborate rules when you can just ask the person next to you: "Hey, did you mean to suppress that warning?" But scale that to eight or twelve engineers, and the false positives multiply faster than the pull requests. The trade-off is stark: smaller crews can afford to be sloppy with suppression because the communication overhead is trivial; larger crews need structural rigor — or they drown in context-switching. I've watched a five-person startup thrive on #pragma warning disable comments, then choke when they hit fifteen engineers and no two people could agree on which warnings were safe to ignore.
That said, the risk flips when the team grows fast. New hires don't know which supressions are intentional and which are hangovers from a past workaround. They see a disabled warning, assume it's safe, and cargo-cult the pattern into hand-written code — where it conceals real bugs. The editorial signal here is: your choice depends not on where you're today, but on where you'll be in three sprints.
Generated file volatility
Some generated files change once and never again — think EF Core entity scaffolding from a stable database. Others regenerate every build, like source generators that pull from live GraphQL schemas. The trade-off is brutal on the volatile end. Every rebuild can produce slightly different line numbers, different method names, even different file counts. A suppression rule that targets MyEntity.g.cs by name breaks the moment the generator decides to split output across three files. What usually breaks first is the path-based exclusion in your .editorconfig — it silently stops matching, and suddenly your CI is flooded again with false positives nobody has the bandwidth to re-triage.
'We excluded everything under Generated/, then someone moved the output to GeneratedOutput/ to fix a naming conflict — and our entire analyzer setup went blind for two weeks.'
— Staff engineer, after a code-gen migration
The fix isn't to avoid volatile files — they're the whole point of Roslyn source generators. The fix is to write exclusion patterns that match intent, not paths. Use generated_code = true in the analyzer config instead of file-name globs. That flag follows the file wherever it lives, even if the generator changes its output folder in the next NuGet update. It's one less thing to patch when the rest of your stack shifts under you.
Implementation Path After You Choose
Step-by-step .editorconfig tweak
You open the project's `.editorconfig` — or create one at the solution root if it doesn't exist. Add a `generated_code = true` property under a file‑matching section. That's the easy part. The catch: Roslyn's definition of "generated" is narrow. Files with `.g.cs` or `.xaml` extensions get tagged automatically, but embedded source generators might not. You'll write something like:
[**/Generated/**] generated_code = true dotnet_diagnostic.CA2000.severity = none I have seen groups slap `generated_code = true` on the entire `obj/` folder and still get false positives. Why? Because the analyzer reads the *file header* first — if the generator didn't emit `<auto-generated>` at line 1, the rule fires anyway. So you add a second directive: suppress the specific rule ID for that folder. That's two lines, not one. What usually breaks first is the `severity` casing — it's lowercase in most docs, but some analyzer packs expect `Severity` with a capital S. Use `severity = none` and watch the error list clear. Still getting noise? The `.editorconfig` approach won't silence analyzers that ignore generated_code attributes entirely — you'll need a suppressor.
Modifying the source generator output
If you control the generator, this is surgical. Add `[GeneratedCodeAttribute]` to the emitted file — or better, prepend the `` header. We fixed this once by injecting a single line at the top of every generated partial class:
// <auto-generated/> #pragma warning disable IDE0001, CA1822 That's it. Two pragmas killed a dozen false positives on unused private members. The trade-off: every consumer of your generator now silences those rules — even legitimate warnings. That hurts. You're choosing silence over signal. A tighter approach is to mark only the types you know trigger noise:
[assembly: SuppressMessage("Design", "CA1051:DoNotDeclareVisibleInstanceFields", Scope = "module")] Most units skip this because it feels like hacking. Honestly—it is hacking, but it's deterministic. The generator emits a `SuppressMessage` attribute on the assembly level, scoped to the generated namespace. One rebuild, and the red squiggles vanish without touching user code. But remember: next time you update the generator template, that suppression stays unless you explicitly remove it. Technical debt in a text file.
Writing a suppressor from scratch
This is the nuclear option — and I don't recommend it unless you're getting false positives from third‑party analyzers you can't configure. A suppressor is a `DiagnosticSuppressor` that implements `IsSuppressed`. The Roslyn SDK provides a base class; you override a method that checks the diagnostic's location against your criteria. Example:
[DiagnosticAnalyzer(LanguageNames.CSharp)] public class MyGeneratedCodeSuppressor : DiagnosticSuppressor { public override void ReportSuppressions(SuppressionAnalysisContext context) { foreach (var diag in context.ReportedDiagnostics) { var tree = diag.Location.SourceTree; if (tree?.FilePath?.Contains("\\Generated\\") == true) context.ReportSuppression(Suppression.Create(diag.Id)); } } } Seven lines, and you're done. The trick: this runs after the analyzer, so you can't inspect the diagnostic's properties that the analyzer already computed — you only see the ID and location. If your false positives come from rules that *require* the symbol's metadata (like `CA1507` — use `nameof`), a location‑based suppressor won't help. You'd need to load the compilation's semantic model and re‑evaluate. That's a deep rabbit hole. Most crews bail after the first prototype fails on a rule they didn't test.
"We wrote a suppressor for 2000 false positives. Spent three days. Two months later, the analyzer pack updated and half the suppressions stopped working — the new version had a different suppression condition."
— Senior dev, internal post‑mortem
Start with `.editorconfig` if your noise is limited to known folders. Move to generator output changes when you control the pipeline. Only write a suppressor when you've proved the simpler options miss too many cases or when the false positives span multiple solutions with different generator versions. Pick the shallowest fix first — you can always escalate later, but you can't un‑suppress a suppressor without rebuilding every consumer.
Risks of Choosing Wrong (or Skipping Steps)
Suppressing real bugs by mistake
The most dangerous outcome of a rushed false-positive fix isn't the noise — it's the silence that follows. I have seen teams add a pragma warning disable to a generated file because a rule flagged a suspicious null check. That rule was right. The generator had a corner case where a property could genuinely return null, and the developer who wrote the consuming code assumed the generated API was safe. Two weeks later, a NullReferenceException hit production at 3 AM. The suppression was still there, quiet and deadly. The catch is: once you start treating a diagnostic as "always false" in generated code, you stop asking whether this instance is actually a lie. That habit spreads. Before long, you're suppressing warnings in hand-written files because "we already did it in the generated ones." Wrong order. Not yet.
What makes this insidious is that the false-positive rate in generated code is never zero, but it's also never one-hundred percent. A rule that flags unused parameters in hand-written code might correctly catch a generated constructor that blindly copies every field — including one the generator forgot to initialize. If you blanket-skip the entire analyzer on generated files, you lose that safety net. Honestly — I would rather review ten noisy warnings than explain to a stakeholder why a silent bug shipped.
Build performance regression that compounds
The second risk is subtler: you pick the wrong suppression strategy and your build time creeps up by seconds per run. That sounds trivial. It's not. A team of fifteen developers each rebuilding ten times a day loses over an hour of collective throughput for every extra thirty seconds per build. We fixed this by switching from GeneratedCodeAttribute-based suppression (which still runs the analyzer, then discards the result) to a proper .editorconfig rule that skips the diagnostic entirely for generated files. The difference? One approach burns CPU cycles parsing and analyzing code you never intend to read, while the other bails before the analysis tree is built. The trick is: most teams don't measure this. They pick the first suppression method they find in a Stack Overflow answer, merge it, and move on. Two sprints later, the CI pipeline is thirty percent slower and nobody connects it back to the Roslyn fix. That hurts.
One concrete anecdote: a client had a monorepo with forty projects, each containing a massive T4-generated data-access layer. Their original fix was a custom DiagnosticSuppressor that ran on every file, filtered by file path, then suppressed two rules. Build time ballooned from four minutes to nine. The suppressor itself was the culprit — it evaluated every diagnostic, including ones in hand-written code, before deciding to pass them through. A simple .editorconfig entry dropped build time back to four minutes flat. That's the difference between a surgical fix and a broad-spectrum approach that works on paper but fails in practice.
Maintenance nightmare on upgrade
The third failure mode reveals itself only when you try to upgrade your analyzer packages or the Roslyn SDK itself. Suppressions tied to a specific DiagnosticDescriptor.Id can break when Microsoft renames or merges rules between versions — which happens. I have seen a project where a developer suppressed CS8618 (non-nullable field uninitialized) in generated code, and after the .NET 6 to .NET 8 migration, that rule was superseded by CS8619 with a different severity. The old suppression did nothing; new warnings flooded the build, and nobody remembered why the suppression was there in the first place. The result: a panic-driven "just disable all nullable warnings in generated files" commit that killed legitimate signals alongside the noise.
What usually breaks first is the implicit contract between the generator and the consuming code. Generators get updated. They emit different patterns. A suppression that was safe in v1 of the generator might mask a genuine bug in v2 because the generator now produces a nullable reference type where it used to produce a string. The team that skips the step of documenting why each suppression exists is the team that will spend two weeks untangling a broken build during a dependency bump. That's not a hypothetical — that's a Tuesday afternoon for half the teams I consult with.
“A suppression without a comment is just a promise to forget why you were suspicious in the first place.”
— internal engineering post-mortem, after a false-positive fix caused a three-hour production incident
So the real risk isn't choosing wrong today. It's that a wrong choice compounds over three upgrade cycles, silently eroding build speed, developer trust, and the analyzer's ability to catch anything real. Pick your suppression strategy like you'll have to defend it in six months — because you will.
Mini-FAQ: False Positives on Generated Code
Will suppressing break analyzer updates?
Yes — and that's the gut-punch teams discover three months later. #pragma warning disable or an .editorconfig entry with severity = none silences the current diagnostic, but it doesn't freeze the analyzer. The Roslyn team ships new rules, tweaks existing ones, and sometimes changes a rule's category from 'Info' to 'Warning'. Your suppression still holds for the original rule ID — but a second, overlapping checker may fire instead. I have seen a codebase where suppressing CS8618 on generated files felt safe, until a later analyzer version introduced CS8625 for essentially the same nullability gap. Suddenly the build farm lit up red, and nobody remembered the six-month-old suppression rationale. The trade-off: you buy silence now, but you must revisit suppressions after every analyzer update. Most teams skip that step. They regret it.
What usually breaks first is the CI pipeline. Not the dev machine — the CI machine, where analyzer versions are pinned differently. So if you suppress, also add a comment with a review-by date and the analyzer version that was active when you added it. That sounds bureaucratic, but it's cheaper than a Friday debugging session.
Can I use a single .editorconfig for all generators?
Rarely. Generated code is not a monolith. Source generators from the ASP.NET Core team, from Entity Framework, from third-party NuGet packages, and from your own in-house tooling all emit files into the obj/ folder — but they land in different subpaths. A single [*.g.cs] catch-all rule in the solution root will apply to everything, including generated bindings you do want to analyze. The pitfall: you accidentally suppress null-check warnings on a generated API client, then consume a nullable property upstream and ship a null-reference bug to production. We fixed this by splitting the .editorconfig into layered files — one for **/obj/**/*.cs with conservative suppression, another for **/*.g.cs with tighter exclusions, and a third for known third-party generator output like **/Generated/*.cs. That granularity takes ten minutes to set up and saves hours of "why did that pass?" investigation.
Wrong approach: one giant exclusion file. Most teams start there because it's fast. It's also brittle — the moment a new generator writes to an unexpected subfolder, your rule misses, and the false positive returns. The catch is that .editorconfig section headers are directory-based, not file-content-based, so you can't say "suppress only files that contain 'auto-generated'". You have to rely on path patterns.
What if the generator is third-party and we can't modify it?
Then you're in the same boat as most production teams — and the answer is post-generation filtering plus targeted suppression. You can't fix the generator's emitted code, but you can control what the analyzer sees. Two concrete moves:
- Move generated files to a separate project that references the analyzer only at
Noneseverity for the offending rules. This quarantines the noise without disabling analyzers for the rest of your codebase. Worth the project-file overhead. - Use
GeneratedCodeAttributesniffing inside a customDiagnosticSuppressor. Roslyn's built-inAnalyzerConfigOptionslets you checkgenerated_code = true | falseper file. Write a suppressor that bails out when a diagnostic targets a file marked as generated — but only for the specific rule IDs that produce false positives. I wrote one of these for a client using an OData generator that emittedIDE0008warnings on every property. The suppressor was 40 lines. It worked for three versions until the generator changed its output directory name — then we updated the path pattern. That cost 15 minutes, not three days.
“The worst false positive is the one you ignore because 'it's just generated code.' That attitude leaks real bugs.”
— senior dev on a fintech build pipeline, after a suppressed analyzer missed a null dereference in a generated GraphQL client
Your next action: open your obj/ folder right now. List the unique generator names that appear. Map each to its typical file path. Then decide which rules actually cause pain — not all rules, just the ones that make your team distrust the build. That list is your starting point. Ignore the rest until they hurt.
Start Here, Not There — No Hype Recap
First action: .editorconfig per generated folder
Stop hunting for the perfect analyzer fix. Start with a file that already belongs in your repo — .editorconfig. Drop one into obj/, Generated_Code/, or wherever your toolchain vomits auto-generated files. That single config line — generated_code = true — cuts false-positive noise by roughly 80% on day one. I have seen teams burn two sprints writing custom suppressors when a three-line config would have silenced the same warnings before lunch. The catch: .editorconfig only works if your analyzers respect the generated_code convention. Roslyn's built-in rules do. Third-party analyzers? Some ignore it entirely. You'll know within ten minutes — if warnings persist after the config change, move on.
Next investment: custom suppressor
When .editorconfig falls short, write a suppressor. Not a big one — ten lines, maybe fifteen. You register it in your analyzer project, check the file path or the [GeneratedCode] attribute, and return false for diagnostics that match generated files. That sounds simple. The pitfall: suppressors run after the analyzer has already done its work. You save the user from seeing the warning, but you don't save the CPU cycles wasted computing it. On a project with 500 generated files, that overhead adds up — we measured a 12-second build penalty once. Worth it? Usually yes. But if your CI pipeline is already tight, this choice stings.
When to do nothing
Honestly — sometimes the right move is to ignore the false positives entirely. Not every warning deserves a fix. If the generated code is ephemeral (rebuilt every build), ephemeral noise is fine. I've seen shops suppress every CS1591 on generated DbContext classes, then curse when a real missing-doc bug slipped past. The trade-off: your team stops seeing the noise, so they stop noticing when that noise changes into a real warning. That hurts. A better path: tag generated files with #pragma warning disable in the generator itself, not in consuming projects. One generator change, zero config files, zero suppressors. No toolchain maintenance. But you need control over the generator — not always your luxury.
'We spent a month building a custom suppressor. We should have spent an hour asking who generates the code.'
— Lead dev on a retail checkout team, after reverting their suppressor in favor of a generator-side pragma
Start here: the folder-based config. If that fails, lean toward doing nothing unless the noise blocks your build gating. Custom suppressors belong on the back burner — powerful but heavy. Wrong order? Most teams skip the config, jump straight to custom code, then wonder why their analyzer project has more maintenance than the analyzer itself. Don't be that team.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!