Analyzers, Code Style & Warnings
In a team, code is read far more often than it is written, and it is reviewed by people who are short of time. Arguments about tabs, braces or var waste those reviews, and real bugs such as a possible null or a lost stack trace slip through. Professional .NET teams let tools catch these problems on every build, so humans can review the logic.
In this lesson you will learn the kinds of warnings .NET gives you, make the build stricter step by step, write nullable-friendly APIs with attributes, and set up dotnet format so code style is checked automatically.
Four kinds of diagnostics
Every message from the build has an ID that tells you who raised it:
| Prefix | Comes from | Example |
|---|---|---|
CS | The C# compiler | CS8602: dereference of a possibly null reference |
CA | .NET code quality analyzers | CA2200: rethrow to preserve stack details |
IDE | Code style analyzers | IDE0161: use a file-scoped namespace |
| Others | Third-party analyzer packages | xUnit1004, NS1000, S1481 |
Analyzers are small plug-ins that run inside the compiler, read your code and report problems. The .NET SDK includes the CA and IDE analyzers, and for projects that target .NET 5 or later the code quality rules are on by default. Only a small set of them are warnings by default; many more exist as suggestions or switched off.
A few default warnings worth recognising: CA2200 (you wrote throw ex; and lost the original stack trace; write throw;), CA2017 (the number of placeholders in a log message template does not match the arguments) and CA1416 (you called a Windows-only API in cross-platform code).
Walkthrough: make a project stricter
Step 1: treat warnings as errors
Warnings that nobody fixes pile up into hundreds, and then new important ones are invisible. Put this in Directory.Build.props next to your solution file so it applies to every project (see Solutions, Projects & NuGet Packages):
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<AnalysisLevel>latest-recommended</AnalysisLevel>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
</Project>TreatWarningsAsErrorsfails the build on any warning. On an old project with many warnings, start with<WarningsAsErrors>nullable</WarningsAsErrors>to make only the nullable warnings errors, and fix the rest gradually.AnalysisLevelset tolatest-recommendedturns on a larger, recommended set ofCArules as warnings. Other modes areminimumandall. Setting a number such as10.0instead oflatestfreezes the rule set, so upgrading the SDK never surprises you with new warnings.EnforceCodeStyleInBuildmakesIDEstyle rules run duringdotnet build, not only in the editor.
Step 2: add an .editorconfig file
An .editorconfig file in the solution folder sets style preferences and the severity of each rule. Create one with dotnet new editorconfig, or start small:
root = true
[*.cs]
indent_style = space
indent_size = 4
csharp_style_namespace_declarations = file_scoped:warning
csharp_style_var_for_built_in_types = false:suggestion
# Library code should not capture the caller's context
dotnet_diagnostic.CA2007.severity = warning
# This project does not need culture-specific formatting checks
dotnet_diagnostic.CA1305.severity = noneEach dotnet_diagnostic.<ID>.severity line sets one rule to error, warning, suggestion, silent or none. Editors (Visual Studio, VS Code with C# Dev Kit, Rider) read the same file, so everyone sees the same squiggles. CA2007 is the rule that asks for ConfigureAwait(false), which you met in Async Patterns: Cancellation, ValueTask & Channels; turn it on for libraries, not apps.
Step 3: build and fix
Run dotnet build. Here is a typical new error after turning on nullable checks:
string? city = Console.ReadLine();
Console.WriteLine(city.Length);Program.cs(2,19): error CS8602: Dereference of a possibly null reference.ReadLine returns null when input ends, so the compiler is right. Fix the code (city?.Length ?? 0, or check if (city is null)) rather than silencing the message.
Step 4: format and check in CI
dotnet format rewrites files to follow .editorconfig: whitespace, using order and fixable style rules.
dotnet format
dotnet format --verify-no-changes
dotnet format style --severity infoThe first command fixes everything it can. The second changes nothing and exits with a non-zero code if any file would change, so a CI pipeline (for example a GitHub Actions workflow) can reject unformatted code. The third applies only style fixes, including those at suggestion level.
Writing nullable-friendly APIs
The compiler's null analysis works inside one method, but it cannot see what another method promises. Attributes from System.Diagnostics.CodeAnalysis describe those promises. The most useful is [NotNullWhen(true)] for Try... methods:
using System.Diagnostics.CodeAnalysis;
var emails = new Dictionary<string, string> { ["Priya"] = "priya@example.com" };
if (TryGetEmail(emails, "Priya", out string? email))
{
Console.WriteLine(email.ToUpperInvariant()); // no warning here
}
Console.WriteLine(TryGetEmail(emails, "Rohan", out _));
static bool TryGetEmail(
Dictionary<string, string> book,
string name,
[NotNullWhen(true)] out string? email)
=> book.TryGetValue(name, out email);PRIYA@EXAMPLE.COM False
Without the attribute, the compiler would warn on email.ToUpperInvariant() because email is declared string?. With it, the compiler knows that a true result means email is not null. Related attributes include [MaybeNullWhen(false)], [NotNull] (a parameter is guaranteed non-null after the call) and [MemberNotNull(nameof(Field))] (a helper method sets a field). The Nullable Types & Null Safety lesson covers the basics these build on.
Suppressing a warning the right way
Sometimes a rule does not fit one place. Prefer the narrowest suppression, and always say why:
using System.Diagnostics.CodeAnalysis;
public class LegacyReport
{
[SuppressMessage("Performance", "CA1822:Mark members as static",
Justification = "Called through reflection by the old reporting tool.")]
public string Title() => "Monthly fee report";
}#pragma warning disable CA1822 and #pragma warning restore CA1822 around a few lines also work. Setting a rule to none in .editorconfig switches it off for the whole folder, which is right only when the team agrees the rule does not apply. Never add ! (the null-forgiving operator) just to make a nullable warning disappear; it hides real bugs.
Common mistakes
- Turning on
TreatWarningsAsErrorson a large old project in one go. The build breaks everywhere and people start suppressing blindly. Go rule by rule. - Silencing with
!or#pragmainstead of fixing. Fix the cause; suppress only with a written reason. - Different style settings on different laptops. Commit
.editorconfigwith the code. - Style checks only in the editor. Without
EnforceCodeStyleInBuildordotnet format --verify-no-changesin CI, nothing is enforced. - Turning on
AnalysisLevelallfor everything. Some rules are for libraries or special cases. Start withlatest-recommended.
Key points
CSdiagnostics come from the compiler,CAfrom quality analyzers,IDEfrom style analyzers; the SDK includes all three.Nullable,TreatWarningsAsErrors,AnalysisLevelandEnforceCodeStyleInBuildinDirectory.Build.propsmake a whole solution stricter..editorconfigsets style preferences and per-rule severity withdotnet_diagnostic.<ID>.severity.dotnet formatfixes style, anddotnet format --verify-no-changesenforces it in CI.- Attributes such as
[NotNullWhen(true)]teach the compiler your null-safety rules. - Suppress narrowly with
[SuppressMessage]and a justification, never by habit.
Exercise
A teammate in Pune sends this method for review. The build reports CS8603 (possible null reference return) and CA2200. Rewrite it so both are fixed properly: the method should return null explicitly when the file is missing (change the return type to show that), and rethrow I/O errors without losing the stack trace.
public string LoadNotice(string path)
{
try
{
return File.Exists(path) ? File.ReadAllText(path) : null;
}
catch (IOException ex)
{
Console.Error.WriteLine($"Could not read {path}");
throw ex;
}
}Show solution
public string? LoadNotice(string path)
{
if (!File.Exists(path))
{
return null; // callers must handle a missing notice
}
try
{
return File.ReadAllText(path);
}
catch (IOException)
{
Console.Error.WriteLine($"Could not read {path}");
throw; // keeps the original stack trace
}
}Changing the return type to string? is honest: callers now get a compiler warning if they use the result without checking for null, which fixes CS8603 at its source. throw; rethrows the same exception object with its stack trace intact, which fixes CA2200, and the unused ex variable is gone.
Quick Quiz
1. Which prefix do code style rules such as "use a file-scoped namespace" have?
IDE rules are code style analyzers; CA rules are code quality analyzers.
2. What does dotnet format --verify-no-changes do?
It is meant for CI pipelines that should reject unformatted code.
3. What does [NotNullWhen(true)] out string? email tell the compiler?
The attribute links the return value to the null state of the out parameter.
4. Which is the best way to handle a rule that is wrong for one specific method?
Narrow, documented suppressions keep the rule working everywhere else.
Learn C# with a mentor
Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
