Code Quality & Static Analysis
Enforcing quality in the build -- Roslyn analyzers, .editorconfig rule severities, warnings as errors, third-party analyzer packs, and NDepend for architecture governance
Code Quality & Static Analysis
The cheapest bug to fix is the one the compiler catches before you commit. C#'s tooling lets you push a remarkable amount of quality enforcement into the build itself, so that style violations, likely bugs, and even architectural rule breaks surface as red squiggles in the IDE and as build failures in CI -- not as review comments or production incidents. The skill is knowing which tool covers which scope: Roslyn analyzers for per-statement rules, .editorconfig for codifying them, and a tool like NDepend for the whole-solution structure that line-level analysis cannot see.
Roslyn Analyzers
A Roslyn analyzer is a component that runs inside the compiler, walks the same syntax trees and semantic model the compiler builds, and reports diagnostics -- the warnings and errors you see with codes like CA1822 or CS8602. Because it runs as part of compilation, the same analysis fires in the IDE as you type and in CI when the build runs, so there is no gap between "looks fine on my machine" and "fails the gate." This is the same Roslyn extensibility that source generators use; an analyzer reports problems where a generator emits code.
The .NET SDK ships a large built-in set, the .NET analyzers (the CAxxxx rules), covering correctness, performance, and API-usage pitfalls -- flagging things like a string.Contains that should be culture-aware, or a class that allocates where a static method would not. They are on by default in recent SDKs; you tune their aggressiveness with AnalysisMode and AnalysisLevel.
<PropertyGroup>
<AnalysisLevel>latest</AnalysisLevel>
<AnalysisMode>Recommended</AnalysisMode>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>Many analyzers also ship a code fix -- the lightbulb in the IDE that rewrites the offending code for you. That pairing is what makes an analyzer feel like help rather than nagging: it does not just tell you the using should be a declaration, it converts it on a keystroke.
.editorconfig: Codifying the Rules
An analyzer can report a diagnostic, but you decide whether each one is a silent suggestion, a warning, or a hard error -- and that decision lives in .editorconfig. This is the single most important file for code quality in a C# repo, because it is version-controlled, applies to everyone who opens the project, and is the bridge between "we agreed on a style" and "the build enforces it."
# .editorconfig
[*.cs]
# Promote a likely-bug analyzer to an error
dotnet_diagnostic.CA2007.severity = error
# Code style: prefer 'var', enforced at build time
csharp_style_var_when_type_is_apparent = true:warning
# Demote a noisy rule to a suggestion
dotnet_diagnostic.CA1031.severity = suggestionSeverities go from none through silent, suggestion, warning, to error. The progression that works in practice is to adopt a ruleset, run it, triage the noise -- silencing what does not fit your codebase with a documented reason -- and then ratchet the rules you care about up to error so they cannot regress. A rule that only ever warns is a rule everyone learns to ignore.
Warnings as Errors
A warning that does not fail the build is advice, and advice accumulates. The teams with genuinely clean codebases set TreatWarningsAsErrors so that a warning breaks CI and has to be dealt with -- fixed or explicitly suppressed -- before code merges.
<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<!-- Or scope it to specific codes -->
<WarningsAsErrors>CS8602;CA2007</WarningsAsErrors>
</PropertyGroup>The honest caveat is that you cannot flip this on a legacy codebase with ten thousand warnings overnight. The workable path is to set a clean baseline -- accept the existing warnings, fail on any new one -- and burn down the backlog over time. When you do need an exception, prefer a narrowly scoped #pragma warning disable with a comment explaining why, over disabling the rule globally; the local suppression documents the intent at the exact line it applies to.
Third-Party Analyzer Packs
Beyond the built-in rules, the ecosystem offers analyzer packages you add like any NuGet dependency, each with a different emphasis. StyleCop.Analyzers enforces consistent layout and naming. SonarAnalyzer.CSharp brings the bug and code-smell rules from SonarQube into the local build. Roslynator adds a deep catalogue of refactorings and fixes. Meziantou.Analyzer targets correctness and performance traps the defaults miss. You reference them with PrivateAssets="all" so they participate in your build but never flow to consumers of your package.
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.3.0.106239" PrivateAssets="all" />The trap is enabling several at once and drowning in thousands of diagnostics. Add one pack, triage it to a severity profile your team agrees with via .editorconfig, get the build green, and only then consider the next. An analyzer set nobody has curated is noise, and noise trains people to ignore the very warnings that matter.
NDepend: Governing Architecture
Analyzers see one file at a time; they are excellent at "this statement is wrong" and blind to "this layer should not depend on that one." NDepend is a commercial static-analysis tool that operates at the opposite scale -- the whole solution as a graph -- and answers structural questions the per-file analyzers cannot. It computes dependency metrics (cyclomatic complexity, coupling, cohesion, instability), renders the dependency matrix and graph so you can see the coupling, and -- its signature feature -- lets you write rules over your own codebase in CQLinq, a LINQ dialect for querying code.
// CQLinq: forbid the domain layer from depending on the web framework
warnif count > 0
from t in Types
where t.Namespace.StartsWith("MyApp.Domain")
&& t.IsUsing("Microsoft.AspNetCore")
select tThat is the role analyzers cannot fill: encoding the layering rule from your project structure as an automated check, tracking technical-debt and complexity trends across builds, and catching the dependency cycle that crept in between two assemblies. The cost is that NDepend is paid, Windows-centric, and another tool in the pipeline -- so it earns its place on large, long-lived, multi-team codebases where architecture erosion is a real and expensive risk, far less so on a small service a single team holds in its head.
Putting It Together
The layers stack rather than compete. The built-in .NET analyzers and a curated third-party pack catch statement- and method-level problems, .editorconfig codifies which of those matter and at what severity, TreatWarningsAsErrors makes the build enforce them, and NDepend -- where the scale justifies it -- guards the structural properties none of the others can see. Wire the whole stack into CI so quality is a gate the build checks, not a hope that review will catch. In the audit-conscious New Zealand government and enterprise market, that automated, version-controlled, reproducible quality bar is also exactly the kind of evidence procurement and governance reviews want to see.