Improving Code Quality in C# With Static Code Analysis
Learn how static code analysis with Roslyn analyzers, EditorConfig, and StyleCop helps you write cleaner, safer C# in .NET 10 with simple examples.
Think about a school exam. After you finish writing your answers, a teacher checks your paper. The teacher does not redo the whole exam. They just read what you wrote and circle the mistakes in red ink. Wrong spelling, a missed step, an answer that does not make sense. You then fix those red marks before the paper goes to the principal.
Static code analysis is that red-ink teacher, but for your C# code. A tool reads your code and circles the problems before your program ever runs. It catches bugs, risky patterns, and messy style early, while they are cheap to fix. In this post you will learn what static analysis is, how it already lives inside the .NET SDK, and how to switch it on properly in .NET 10 (the current LTS release) with C# 14.
What "static" really means
The word static here means "without running the program". The analyzer reads your source code as text and shapes, like a teacher reading a paper. The opposite is dynamic analysis, where you actually run the program and watch what happens (that is what tests and profilers do).
Both are useful. But static analysis is special because it is fast and it runs every time you build. You do not need to write a single test to get value from it.
Meet Roslyn, the engine inside the compiler
When you build a C# project, a compiler called Roslyn turns your code into a program. Roslyn does not just compile. It also understands your code deeply, line by line. Microsoft built a system on top of this understanding called Roslyn analyzers.
These analyzers come free inside the .NET SDK. You do not install them. For any project that targets .NET 5 or later, the code-quality rules are already on. Each rule has a short code:
- CA rules (like
CA1822) check quality: security, performance, design, and correctness. - IDE rules (like
IDE0005) check style: naming, spacing, unused imports.
Here is a tiny example. This method never uses the object it belongs to, so it could be static:
public class Calculator
{
// CA1822 will warn: this can be marked static
public int AddOne(int value)
{
return value + 1;
}
}The analyzer sees that AddOne never touches any field, so it suggests static. Making it static is a tiny speed win and tells readers "this method does not depend on object state".
A quick tour of the rule families
You do not need to memorise every rule. But it helps to know the main groups so a warning code does not look scary.
| Rule prefix | What it checks | Example |
|---|---|---|
CA1xxx | Design and API shape | CA1822 mark members static |
CA2xxx | Reliability and usage | CA2007 await without context |
CA3xxx / CA5xxx | Security | CA5359 do not disable cert validation |
CA18xx | Performance | CA1825 avoid zero-length array allocations |
IDExxxx | Code style | IDE0005 remove unused using |
When you see a code like CA2007 in your build output, you can search "CA2007" on Microsoft Learn and get a clear page that explains the rule and how to fix it.
Turning the dial up: analysis level and mode
By default you get a sensible set of warnings. But you can ask for more. Two project settings control how strict the analyzer is.
AnalysisLevelpicks which set of rules to use. The default islatest, so you always get the newest rules as you move to newer SDKs.AnalysisModepicks how many of those rules are on. The default is balanced. Setting it toAllturns on almost everything.
You set these in your .csproj file or, better, in a shared Directory.Build.props so every project in the solution agrees:
<Project>
<PropertyGroup>
<AnalysisLevel>latest</AnalysisLevel>
<AnalysisMode>All</AnalysisMode>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>
</PropertyGroup>
</Project>Two of these deserve a closer look:
EnforceCodeStyleInBuildmakes theIDEstyle rules run during a normaldotnet build, not just inside Visual Studio. Without it, style rules are easy to ignore.CodeAnalysisTreatWarningsAsErrorsturns code-analysis warnings into build-breaking errors, so nobody can sneak a warning past the gate.
From loose to strict analysis
Steps
Default
CA rules on, balanced
Add level
AnalysisLevel latest
Add mode
AnalysisMode All
Enforce style
IDE rules in build
Warnings as errors
build fails on issues
The .editorconfig file: your rulebook
A big project needs one shared rulebook so every developer and the build server agree on the rules. In .NET that rulebook is a file named .editorconfig placed at the root of your repository.
Inside it you can set the severity of any single rule. Severity answers the question "how loud should this warning be?" The choices are none, silent, suggestion, warning, and error.
# .editorconfig at the repo root
root = true
[*.cs]
# Make "mark as static" a hard error
dotnet_diagnostic.CA1822.severity = error
# Treat unused usings as a warning
dotnet_diagnostic.IDE0005.severity = warning
# This rule is noisy for us, turn it off
dotnet_diagnostic.CA1303.severity = noneStarting in .NET 9, the severity you set here is respected at build time, not only in the editor. That means a route like GET /{id} in a controller and every other file gets the same checks whether you build locally or on a server.
Adding extra analyzers as NuGet packages
The built-in rules are great, but the community has built more. You add these as normal NuGet packages. They plug into the same Roslyn engine and report through the same severity system.
| Package | What it adds | Cost |
|---|---|---|
| StyleCop.Analyzers | Strict layout and naming style (the SAxxxx rules) | Free, open source |
| Roslynator.Analyzers | Hundreds of extra quality and refactoring rules | Free, open source |
| Meziantou.Analyzer | Practical bug and performance rules | Free, open source |
| SonarAnalyzer.CSharp | Bug, smell, and security rules from SonarSource | Free analyzer package |
A quick note on licensing, because it matters in 2026: some popular .NET libraries such as MediatR and MassTransit have moved to commercial licensing. The analyzer packages listed above are still free and open source, but always check a package's licence before you add it to a work project. Static analysis tools change less often than libraries, but the habit of checking is a good one.
You install one like any package:
// In the .csproj, this looks like:
// <PackageReference Include="StyleCop.Analyzers" Version="1.2.0" PrivateAssets="all" />
// StyleCop will now flag style issues, for example SA1101:
public class Order
{
private int total;
public int Total => total; // SA1101 may ask for this.total
}The PrivateAssets="all" part means the analyzer helps you build your code but is not forced onto anyone who later uses your library. That keeps your published package clean.
Fixing warnings without drowning in them
If you turn on every rule on an old, large project, you might see thousands of warnings at once. That is scary and unhelpful. Here is a calmer plan.
Adopting analysis on an old project
Steps
Baseline
see current warnings
Pick a few
choose high-value rules
Fix them
clean those issues
Lock as error
stop them returning
Repeat
add more rules over time
The trick is to treat code quality like cleaning one shelf at a time, not the whole house in one day. Pick a handful of rules that catch real bugs first (security and reliability), fix every instance, then set those rules to error so they can never come back. Next week, pick a few more. The wall of warnings shrinks steadily and your team never feels overwhelmed.
Many warnings also have an automatic fix. In Visual Studio or Rider you press the lightbulb (or Ctrl + .) and the editor rewrites the code for you. On the command line, some fixes run in bulk:
// Example of a fix the analyzer can apply for you.
// Before (IDE0090 suggests simpler 'new'):
List<string> names = new List<string>();
// After the suggested fix:
List<string> names = new();How a warning travels from rule to your screen
It helps to picture the whole journey. You write code. Roslyn parses it into a tree. Each analyzer walks that tree looking for its pattern. When it finds one, it raises a diagnostic with an ID and a severity. The severity is then adjusted by your .editorconfig. Finally the build either prints a warning or stops with an error.
Why this is worth the effort
Some people feel that more warnings just slow them down. In practice, static analysis saves far more time than it costs. It catches a missing await, an empty array allocation in a hot loop, or a disabled security check before any user ever sees it. Fixing a bug at build time costs minutes. Fixing the same bug in production, after a customer reports it, can cost days.
It also teaches you. Every warning links to a clear page that explains why the pattern is risky. Over a few months your team naturally writes better C# because the tool keeps nudging everyone in the same direction. New teammates pick up your house style on day one, because the build itself enforces it.
A sensible starting setup
If you want one good default to copy, this is a safe, friendly starting point for a new .NET 10 solution. Put it in Directory.Build.props at the repo root:
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
<AnalysisLevel>latest</AnalysisLevel>
<AnalysisMode>Recommended</AnalysisMode>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
</Project>Notice AnalysisMode is Recommended here, not All. For a brand-new project that is a gentler start: you get the most valuable rules without a flood. You can raise it to All later once the team is comfortable. Pair this with a small .editorconfig and you have real protection from day one, with almost no effort.
Suppressing a rule the right way
Sometimes a rule is wrong for one special spot in your code. Maybe a piece of code looks risky to the analyzer but you know it is safe. You should not turn the whole rule off for the whole project just for one line. Instead, suppress it in the smallest place possible and leave a note for the next reader.
The cleanest way is an attribute right above the code, with a Justification that explains why:
using System.Diagnostics.CodeAnalysis;
public class ReportBuilder
{
[SuppressMessage("Performance", "CA1822",
Justification = "Kept as instance method for a future override.")]
public string Header()
{
return "Monthly report";
}
}The golden rule is simple: a suppression must always say why. A suppression without a reason is just hiding a problem. A suppression with a clear reason is a small, honest decision that a teammate can trust. If you ever find yourself suppressing the same rule again and again, that is a signal that the rule does not fit your project, and you should turn it off properly in .editorconfig instead.
Quick recap
- Static code analysis reads your C# without running it and warns you about bugs and messy style, like a teacher checking a paper in red ink.
- Roslyn analyzers ship inside the .NET SDK. Quality rules start with
CA, style rules start withIDE, and they are on by default for .NET 5 and later. - Use
AnalysisLevelandAnalysisModeto choose how strict the checks are, andEnforceCodeStyleInBuildso style rules run duringdotnet build. - A single
.editorconfigat the repo root sets each rule's severity (nonetoerror) and gives everyone the same rulebook. - Add extra free analyzers like StyleCop, Roslynator, and Meziantou.Analyzer as NuGet packages when you want more rules. Always check a package's licence.
- On old projects, adopt rules a few at a time, fix them, lock them as
error, then repeat. Never try to fix everything in one day.
References and further reading
- Code analysis in .NET (Microsoft Learn)
- Configure code analysis rules (Microsoft Learn)
- Roslyn analyzers overview (Microsoft Learn)
- Improving Code Quality in C# With Static Code Analysis (Milan Jovanović)
- Enforcing .NET code style rules at compile time (Genezini)
Related Posts
5 Awesome C# Refactoring Tips to Write Cleaner Code
Learn 5 simple C# refactoring tips with examples in modern C# 14. Make your code cleaner, safer, and easier to read using guard clauses and more.
Central Package Management in .NET: Simplify NuGet Dependencies
Learn Central Package Management (CPM) in .NET to manage all NuGet versions in one Directory.Packages.props file. Simple guide with diagrams and examples.
How to Write Elegant Code with C# Pattern Matching
Learn C# pattern matching the easy way. Use is, switch expressions, property and list patterns to write clean, readable, and elegant .NET code.
How to Apply Functional Programming in C#: A Beginner's Guide
Learn functional programming in C# the simple way: pure functions, immutability, records, LINQ, pattern matching, and composition with friendly examples.
New Features in C# 13: A Friendly Beginner's Guide
Learn the new features in C# 13 with simple words, real-life examples, diagrams, and code you can read in minutes. Great for beginners.
Mastering Exception Handling in C#: A Comprehensive Guide
Learn C# exception handling the friendly way: try, catch, finally, custom exceptions, filters, throw vs throw ex, and real best practices for .NET 10.