Skip to main content
SEMastery
.NET Corebeginner

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.

11 min readUpdated January 18, 2026

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.

Static analysis checks your code before it runs, dynamic checks happen while it runs

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 prefixWhat it checksExample
CA1xxxDesign and API shapeCA1822 mark members static
CA2xxxReliability and usageCA2007 await without context
CA3xxx / CA5xxxSecurityCA5359 do not disable cert validation
CA18xxPerformanceCA1825 avoid zero-length array allocations
IDExxxxCode styleIDE0005 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.

  • AnalysisLevel picks which set of rules to use. The default is latest, so you always get the newest rules as you move to newer SDKs.
  • AnalysisMode picks how many of those rules are on. The default is balanced. Setting it to All turns 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:

  • EnforceCodeStyleInBuild makes the IDE style rules run during a normal dotnet build, not just inside Visual Studio. Without it, style rules are easy to ignore.
  • CodeAnalysisTreatWarningsAsErrors turns code-analysis warnings into build-breaking errors, so nobody can sneak a warning past the gate.

From loose to strict analysis

Default
Add level
Add mode
Enforce style
Warnings as errors

Steps

1

Default

CA rules on, balanced

2

Add level

AnalysisLevel latest

3

Add mode

AnalysisMode All

4

Enforce style

IDE rules in build

5

Warnings as errors

build fails on issues

You can dial the strictness up one step at a time

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 = none

Starting 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.

One .editorconfig file feeds the same rules to every place code is built

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.

PackageWhat it addsCost
StyleCop.AnalyzersStrict layout and naming style (the SAxxxx rules)Free, open source
Roslynator.AnalyzersHundreds of extra quality and refactoring rulesFree, open source
Meziantou.AnalyzerPractical bug and performance rulesFree, open source
SonarAnalyzer.CSharpBug, smell, and security rules from SonarSourceFree 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

Baseline
Pick a few
Fix them
Lock as error
Repeat

Steps

1

Baseline

see current warnings

2

Pick a few

choose high-value rules

3

Fix them

clean those issues

4

Lock as error

stop them returning

5

Repeat

add more rules over time

Start small, raise the bar slowly, never go backwards

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.

The path a single diagnostic takes from your code to a build result

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 with IDE, and they are on by default for .NET 5 and later.
  • Use AnalysisLevel and AnalysisMode to choose how strict the checks are, and EnforceCodeStyleInBuild so style rules run during dotnet build.
  • A single .editorconfig at the repo root sets each rule's severity (none to error) 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

Related Posts