The .NET analyser ecosystem: built-in CA code quality analysers and IDE code style analysers, AnalysisLevel and AnalysisMode, EnforceCodeStyleInBuild, .editorconfig severities, third-party analyser packages, suppressions, and rolling stricter rules out to a team.
Analysers are plug-ins for the Roslyn compiler. They report diagnostics in the editor and during the build. CA rules (CA1xxx to CA5xxx) are the SDK's code quality analysers, covering design, performance, reliability and security. The .NET analysers are enabled by default for net5.0+ projects (EnableNETAnalyzers), but only a default subset of rules reports as warnings; AnalysisLevel and AnalysisMode set how many apply. IDE rules (IDE0xxx) are code style rules, such as var usage or file-scoped namespaces. They show in the editor but only run in the build when EnforceCodeStyleInBuild is true, which is why style issues often pass CI. Severities for both are set in .editorconfig (or a .globalconfig) with dotnet_diagnostic.<id>.severity. Third-party analysers come in as NuGet packages. For a team, I put the levels in Directory.Build.props, the severities in one root config file, and tighten them step by step.
"Analysers are plug-ins that run inside the Roslyn compiler, so they report diagnostics both in the editor and during the build. The SDK ships two families. CA rules are code quality analysers covering design, performance, reliability and security; they're on by default for net5.0 and later, but only a default subset reports as warnings.
AnalysisLevelpicks the rule set version, likelatestor10.0, andAnalysisModeits breadth, fromNonethroughRecommendedtoAll.IDE rules are code style rules, like file-scoped namespaces or
varusage. They run in the editor, but in the build only whenEnforceCodeStyleInBuildis true, so the mistake I see is a team expecting them to fail CI when they never run there. Severities for both families go in.editorconfigor a.globalconfigwithdotnet_diagnostic.<ID>.severity, which every editor honours. Third-party analysers such as StyleCop, Roslynator or Meziantou come in as package references withPrivateAssets="all".For a team I put the levels in
Directory.Build.props, pinAnalysisLevelif warnings are errors, and keep severities in one root config. I suppress narrowly, with[SuppressMessage]and a justification or a scoped config section rather than a blanketNoWarn. New rules roll out as suggestion, then warning, then error, withdotnet formatdoing the mechanical fixes in CI and security rules at error, so the warning list stays something people trust."
| Family | IDs | Comes from | Runs in build by default? |
|---|---|---|---|
| Compiler warnings | CS0xxx, CS8xxx (nullable) | The C# compiler | Yes |
| Code quality | CA1xxx–CA5xxx | .NET SDK (Microsoft.CodeAnalysis.NetAnalyzers) |
Yes, for the enabled set |
| Code style | IDE0xxx | .NET SDK code style analysers | No, only with EnforceCodeStyleInBuild=true |
| Third-party | e.g. SA (StyleCop), RCS (Roslynator), MA (Meziantou) | NuGet packages | Yes |
CA categories include Design, Globalization, Performance (CA18xx, such as CA1848 for high-performance logging), Reliability (CA2xxx, such as CA2007 ConfigureAwait), Security (CA2100 SQL injection, CA5xxx crypto rules) and Usage.
<PropertyGroup>
<AnalysisLevel>latest</AnalysisLevel> <!-- or a version such as 10.0 -->
<AnalysisMode>Recommended</AnalysisMode> <!-- None, Default, Minimum, Recommended, All -->
</PropertyGroup>
You can combine them as <AnalysisLevel>latest-recommended</AnalysisLevel>. Pinning a version (10.0) means SDK upgrades don't silently add new warnings; latest picks them up automatically. Per-category modes exist too, such as AnalysisModeSecurity.
Code style preferences live in .editorconfig (csharp_style_namespace_declarations = file_scoped:warning). To make them fail the build, set <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> and give the rules a warning or error severity.
[*.cs]
dotnet_diagnostic.CA2007.severity = none # ASP.NET Core apps have no sync context
dotnet_diagnostic.CA1848.severity = warning # use LoggerMessage / source-generated logging
dotnet_diagnostic.IDE0161.severity = warning # file-scoped namespaces
[tests/**.cs]
dotnet_diagnostic.CA1707.severity = none # underscores in test names
For a single justified exception, use [SuppressMessage("Reliability", "CA2000", Justification = "Ownership transferred to caller")] or #pragma warning disable with a comment. Avoid project-wide NoWarn for anything but well-understood rules.
A team's CI passed while developers' IDEs were full of style suggestions, because EnforceCodeStyleInBuild was off. Turning it on with .editorconfig severities at suggestion first, then warning for a curated set, and running dotnet format once to fix existing code, made the IDE and CI agree. Enabling the security CA rules at warning also caught two string-concatenated SQL commands (CA2100) in a legacy repository.
Directory.Build.props, severities in .editorconfig.AnalysisLevel to a version if you treat warnings as errors, and upgrade it deliberately.Checks practical knowledge of code analysis in .NET.
Green flags: distinguishes CA and IDE rules; knows AnalysisLevel/AnalysisMode and EnforceCodeStyleInBuild; configures severities in .editorconfig; suppresses narrowly; rolls rules out gradually.
Red flags: thinks analysers are an IDE-only feature; uses blanket NoWarn; enables everything at once.
Roslyn compilation (CoreCompile -> Csc)
source + references + analyser assemblies (SDK + NuGet analyzers/ folders)
|
+-----------------+------------------+
| |
Compiler diagnostics Analyser diagnostics
CS0xxx, CS8xxx (nullable) CA (SDK code quality, NetAnalyzers)
IDE (code style) -- build only if EnforceCodeStyleInBuild
SA / RCS / MA / RS (packages)
| |
+---------------+--------------------+
v
severity from: .editorconfig / .globalconfig > AnalysisLevel + AnalysisMode defaults
then: NoWarn (drop) WarningsAsErrors / TreatWarningsAsErrors (promote)
|
IDE: squiggles + quick fixes Build: warnings or errors in the output
Analysers are .NET assemblies loaded into the compiler. The IDE runs them live on open files (and optionally the whole solution in the background); the build runs them inside CoreCompile. Source generators are loaded the same way.
| Family | IDs | Shipped in | In the build by default | Configured by |
|---|---|---|---|---|
| Compiler | CS0xxx, CS8xxx | The C# compiler | Yes | Nullable, WarningLevel, .editorconfig |
| Code quality | CA1xxx to CA5xxx | The .NET SDK (EnableNETAnalyzers, on by default for net5.0+) |
Yes, for the rules enabled by the level and mode | AnalysisLevel, AnalysisMode, .editorconfig |
| Code style | IDE0xxx | The .NET SDK | No: only with EnforceCodeStyleInBuild=true |
.editorconfig style options and severities |
| Third party | SA (StyleCop), RCS (Roslynator), MA (Meziantou), RS0030 (banned APIs) and others | NuGet packages | Yes | Package docs, .editorconfig |
The SDK's CA analysers are versioned with the SDK. To get newer rules without a new SDK, reference the Microsoft.CodeAnalysis.NetAnalyzers package explicitly.
| Category | Range or examples | Catches |
|---|---|---|
| Design | CA1000 to CA1070 | API shape problems in libraries |
| Globalization | CA1304, CA1305, CA1307, CA1309 | Culture-sensitive parsing and comparison (ToUpper(), string.Format without culture) |
| Performance | CA18xx: CA1822, CA1848, CA1859, CA1860 | Static candidates, high-performance logging, avoidable allocations |
| Reliability | CA2000, CA2007, CA2016 | Undisposed objects, ConfigureAwait, not forwarding CancellationToken |
| Security | CA2100, CA3xxx, CA5xxx | SQL injection, taint flow, weak crypto (CA5350, CA5351), insecure deserialisation |
| Usage | CA2200, CA2201, CA2211 | Rethrowing incorrectly, reserved exception types |
<PropertyGroup>
<AnalysisLevel>10.0</AnalysisLevel> <!-- which version of the rule set -->
<AnalysisMode>Recommended</AnalysisMode> <!-- how many rules are on as warnings -->
<!-- or combined: <AnalysisLevel>10.0-recommended</AnalysisLevel> -->
<AnalysisModeSecurity>All</AnalysisModeSecurity> <!-- per-category override -->
</PropertyGroup>
| Setting | Values | Notes |
|---|---|---|
AnalysisLevel |
A version (8.0, 9.0, 10.0), latest, preview, none |
A pinned version stays stable when the SDK changes; latest picks up new rules with each SDK |
AnalysisMode |
None, Default, Minimum, Recommended, All |
How many rules are raised as warnings; All is very noisy in older code |
AnalysisMode<Category> |
Same values | Per category: Security, Performance, Reliability, Design, Usage, Naming, Globalization and others |
CodeAnalysisTreatWarningsAsErrors |
true/false |
Lets CA warnings stay warnings even when TreatWarningsAsErrors is on, useful during rollout |
| Severity | IDE | Build |
|---|---|---|
error |
Red squiggle | Fails the build |
warning |
Green squiggle | Warning (error with TreatWarningsAsErrors) |
suggestion |
Dots under the code | Not reported |
silent |
Quick fix only | Not reported |
none |
Off | Off |
Three levels of precision, most specific wins:
[*.cs]
dotnet_analyzer_diagnostic.severity = suggestion # every analyser rule
dotnet_analyzer_diagnostic.category-Security.severity = error # a whole category
dotnet_diagnostic.CA2007.severity = none # one rule
Code style rules have two parts: the preference and the severity.
csharp_style_namespace_declarations = file_scoped:warning # preference + severity shorthand
dotnet_diagnostic.IDE0161.severity = warning # explicit severity (wins in the build)
Set the dotnet_diagnostic severity explicitly for rules you want in the build; the :warning shorthand isn't always honoured by the command-line build.
"Remove unnecessary usings" (IDE0005) only reports in the build when GenerateDocumentationFile is true, because the compiler needs documentation-comment analysis to know a using is unused. Teams often enable the rule and wonder why CI never reports it.
| Mechanism | Scope | Best for |
|---|---|---|
Root .editorconfig with root = true |
Files under that folder, by path pattern | Style preferences and per-folder severities (for example relaxed rules for tests) |
.globalconfig (is_global = true) |
Every file in the projects that include it | Organisation-wide rule sets; can be shipped in a NuGet package |
Directory.Build.props |
Every project | AnalysisLevel, AnalysisMode, EnforceCodeStyleInBuild, GlobalAnalyzerConfigFiles items |
Directory.Packages.props GlobalPackageReference |
Every project, as a private asset | Third-party analysers |
A shared package (for example Contoso.CodeStandards) that contains a .globalconfig, analyser references and a .props file lets many repositories adopt the same baseline and upgrade it with one version bump.
| Technique | Scope | Use when |
|---|---|---|
[SuppressMessage("Category", "CA2000:...", Justification = "...")] |
One member or type | A reviewed, justified exception |
GlobalSuppressions.cs with Target = "~M:..." |
A specific symbol, from one file | Generated by the IDE; keep justifications |
#pragma warning disable CA2000 // reason ... restore |
A few lines | Local, temporary exceptions |
.editorconfig section [Generated/**.cs] |
A folder or pattern | Generated code, tests, samples |
NoWarn |
The whole project | Rarely; only for rules the team has decided don't apply |
Always restore a #pragma, always add a justification, and review suppressions like code.
1. pick the rules (security and reliability first, then performance, then style)
2. set them to suggestion -> see the count, nobody's build breaks
3. fix mechanically where possible: dotnet format analyzers --diagnostics CA1822 IDE0161
4. raise to warning; fix the rest in small PRs per area
5. raise to error for rules that catch real bugs
6. upgrade AnalysisLevel in its own PR on each SDK upgrade, repeat
Track the warning count per project over time. A number that only goes down is the best evidence the rollout is working.
-p:ReportAnalyzer=true -bl and open the binary log: it shows the time each analyser took.RunAnalyzersDuringBuild=false (or -p:RunAnalyzers=false) speeds up a quick local build, as long as CI still runs them.AnalysisModeSecurity=All) and make it an error on main. Rules such as CA2100 (SQL injection), CA3001 to CA3012 (taint analysis for injection) and CA5350/CA5351 (weak hashing and encryption) are cheap insurance.Microsoft.CodeAnalysis.BannedApiAnalyzers with a BannedSymbols.txt can ban dangerous APIs, such as BinaryFormatter or direct HttpClient construction, across the codebase.The .NET 11 SDK will ship a new rule set (AnalysisLevel 11.0) with new CA rules. With a pinned level, upgrading the SDK doesn't change your warnings; raise the level in a separate PR once you've reviewed the new rules. Keep production on the .NET 10 SDK until then.
string.Format without a culture, then build and note the warnings.<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> and dotnet_diagnostic.IDE0161.severity = warning, and build again.using and enable IDE0005 at warning; build with and without GenerateDocumentationFile.<AnalysisLevel>10.0-all</AnalysisLevel> and count the new warnings, then go back to recommended.dotnet_analyzer_diagnostic.category-Security.severity = error and write a SqlCommand with string concatenation to trigger CA2100.[tests/**.cs] section and confirm it only applies to tests.-p:ReportAnalyzer=true -bl and find the slowest analyser in the binary log.<!-- Directory.Build.props -->
<Project>
<PropertyGroup>
<AnalysisLevel>10.0-recommended</AnalysisLevel>
<AnalysisModeSecurity>All</AnalysisModeSecurity>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
</Project>
<!-- Directory.Packages.props: a third-party analyser for every project (private to each project) -->
<ItemGroup>
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
</ItemGroup>
# .editorconfig
root = true
[*.cs]
csharp_style_namespace_declarations = file_scoped:warning
dotnet_diagnostic.IDE0161.severity = warning
dotnet_diagnostic.CA1848.severity = warning
dotnet_diagnostic.CA2100.severity = error
dotnet_diagnostic.CA2007.severity = none
[tests/**.cs]
dotnet_diagnostic.CA1707.severity = none
using System.Diagnostics.CodeAnalysis;
namespace Orders.Data;
public sealed class ConnectionFactory
{
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope",
Justification = "The caller owns and disposes the returned connection.")]
public SqlConnection Create(string connectionString) => new(connectionString);
}
The last example assumes a reference to Microsoft.Data.SqlClient and using Microsoft.Data.SqlClient;.
Mistake: expecting IDE style rules to fail CI.
IDE0xxx rules run in the editor but not in the command-line build unless EnforceCodeStyleInBuild is true, so style problems pass CI while developers see them locally.
Instead: set EnforceCodeStyleInBuild in Directory.Build.props, give the agreed rules an explicit dotnet_diagnostic.<ID>.severity, and run dotnet format --verify-no-changes in CI.
Mistake: combining AnalysisLevel=latest with TreatWarningsAsErrors.
A new SDK can add rules to the enabled set, so the first build on the new SDK fails on code nobody changed.
Instead: pin AnalysisLevel to a version (for example 10.0-recommended) and raise it in its own pull request after reviewing the new rules.
Mistake: turning on AnalysisMode=All in a legacy codebase.
Thousands of warnings appear at once, nobody can triage them, and the team responds with blanket suppressions.
Instead: start with Recommended, raise categories one at a time (security and reliability first), and roll new rules out as suggestion, then warning, then error.
Mistake: using blanket NoWarn lists.
Project-wide suppressions hide real problems in new code as well as old, and nobody remembers why each entry was added.
Instead: suppress narrowly with [SuppressMessage] and a justification, a restored #pragma, or a scoped .editorconfig section, and review suppressions like code.
Mistake: scattered .editorconfig files without root = true.
Files in different folders get different severities, and a stray file above the repo can change the rules for everyone.
Instead: keep one root .editorconfig with root = true (or a shared .globalconfig), and allow folder overrides only for agreed cases such as tests or generated code.
Mistake: enabling IDE0005 and expecting the build to report unused usings.
The rule only reports in the build when GenerateDocumentationFile is true, so CI stays silent.
Instead: set GenerateDocumentationFile to true (and suppress CS1591 if you don't want missing-comment warnings) when you enforce IDE0005.
Mistake: adding analyser packages to libraries without PrivateAssets="all".
The analyser dependency flows to every consumer, who then inherits rules and version conflicts they never asked for.
Instead: reference analysers with PrivateAssets="all", or use GlobalPackageReference under Central Package Management.
Mistake: ignoring the security rules.
SQL injection (CA2100), weak cryptography (CA5350, CA5351) and insecure deserialisation slip through review because the rules are off or only suggestions.
Instead: set AnalysisModeSecurity=All, make the security category an error on main, and ban dangerous APIs with the banned-API analyser.
Mistake: running heavy analysers without measuring their cost.
Build times grow by minutes on large solutions and developers start disabling analysers locally.
Instead: measure with -p:ReportAnalyzer=true -bl, drop or scope slow packages, and allow quick local builds with RunAnalyzers=false as long as CI always runs them.
Mistake: treating analyser findings as all-or-nothing debt. Either every warning must be fixed before anything ships, or none ever are. Instead: fix bug-finding rules (security, reliability, nullable) everywhere, apply style rules to new and touched code, and track the remaining count per project so it goes down over time.
Analysers are the cheapest code reviewer you'll ever have, but only if their output is trusted. A senior engineer curates the rule set rather than turning everything on: a pinned AnalysisLevel, recommended mode, security rules at error, a short list of style rules that the team agreed on, and EnforceCodeStyleInBuild so the IDE and CI agree. New rules roll out as suggestion, then warning, then error, with dotnet format doing the mechanical fixes. Suppressions carry justifications and are reviewed, so the warning list stays meaningful.
At scale (many teams, many repositories) the configuration itself becomes a product: a shared .globalconfig and analyser GlobalPackageReferences distributed through a common Directory.Build.props or an internal NuGet package, versioned and upgraded deliberately. That gives every repo the same baseline while letting a team document an exception in its own config.
Debt versus purity. Not every rule is worth fixing in old code. Seniors fix rules that catch bugs (security, reliability, nullable) everywhere, apply style rules to new and touched code only, and track the remaining warning count per project so debt goes down rather than being hidden.
The file under review turns on TreatWarningsAsErrors and then hides 25 warnings with NoWarn. That list includes nullable warnings, obsolete-API warnings and a security rule, so the policy looks strict while silencing the warnings that matter most. I'd remove those suppressions. Rules that genuinely don't apply would get a severity in .editorconfig, scoped to the right folders and commented. Rules we can't fix yet would go in WarningsNotAsErrors, so they stay visible as warnings. I'd also pin AnalysisLevel so an SDK upgrade can't break the build overnight, apply the same settings locally and in CI, and ratchet a legacy codebase towards zero warnings rather than flipping everything at once.
I commit an .editorconfig at the repo root with root = true. It holds formatting (indentation, new lines), .NET code style preferences such as file-scoped namespaces, and severities for analyser and code style rules via dotnet_diagnostic.<id>.severity. Visual Studio, VS Code and Rider all read it, so style no longer depends on the editor. To enforce it, I set EnforceCodeStyleInBuild so IDE rules run during dotnet build, and run dotnet format --verify-no-changes in CI.
The pipeline restores three times, builds in Debug, runs dotnet format in a way that silently fixes files instead of failing, and publishes from a different build than the one it tested. I'd use global.json for the SDK, restore once with --locked-mode, build once in Release with --no-restore, then test and publish with --no-build. I'd run dotnet format --verify-no-changes so formatting drift fails the build, collect test results and coverage, and cache the NuGet folder.
How to tell it (STAR)
**Situation:** In our [40-project] solution at [company], the IDE showed style suggestions everywhere but CI never enforced them, so reviewers spent time on style comments. **Task:** I wanted CI and the IDE to agree, and to use analysers for more than style. **Action:** I found EnforceCodeStyleInBuild was off, agreed a short list of rules with the team, fixed [3,000] violations in one mechanical PR with dotnet format, pinned the analysis level, and turned on the security rules. **Result:** Style comments in reviews dropped [noticeably], and the security rules caught [two] SQL injection risks before they reached production.
Last updated 1 Nov 2026.