Roslyn Analyzers in .NET: A Practical Guide to Writing Better C# Code



When developing C# and .NET applications, it is easy for small coding issues to accumulate over time. Unused variables, incorrect naming conventions, inefficient code, potential null-reference problems, and other issues can make a codebase harder to maintain.

Roslyn Analyzers help developers identify these problems while they are writing code.

Instead of discovering coding issues during code review or after an application has been deployed, analyzers can detect many problems directly inside Visual Studio, JetBrains Rider, or during a CI/CD build.

In this article, you'll learn what Roslyn Analyzers are, how they work, how to use them in a .NET project, and how to create a custom analyzer using C#.


What Is Roslyn?

Roslyn is the .NET Compiler Platform developed by Microsoft.

It provides APIs that allow developers and tools to inspect C# and Visual Basic source code.

The compiler doesn't simply convert C# source code into an executable. During compilation, Roslyn creates a structured representation of the code that tools can inspect.

This makes it possible to build tools such as:

  • Code analyzers

  • Code fixes

  • Refactoring tools

  • IDE features

  • Source generators

  • Developer productivity tools

Roslyn is therefore much more than just the C# compiler.


What Is a Roslyn Analyzer?

A Roslyn Analyzer is a .NET component that examines source code and identifies potential problems or violations of coding rules.

For example, an analyzer could detect code such as:

string name = null;

and report a warning because the variable may contain a null value.

An analyzer can also enforce company-specific rules.

For example:

public class UserService
{
}

A custom organization might require service classes to end with Service.

An analyzer could detect violations such as:

public class UserManager
{
}

and report:

Class names representing services should end with 'Service'.


Why Are Roslyn Analyzers Useful?

Roslyn Analyzers are useful because they move code-quality checks closer to the developer.

Without analyzers, the workflow might look like this:

Developer writes code
        ↓
Code review
        ↓
Reviewer finds issue
        ↓
Developer fixes issue

With analyzers:

Developer writes code
        ↓
Roslyn Analyzer examines code
        ↓
Warning/Error appears in IDE
        ↓
Developer fixes issue

This can reduce the number of problems reaching code review.


Roslyn Analyzers vs Compiler Errors

A compiler error prevents code from compiling.

For example:

int number = "hello";

The compiler reports a type conversion error.

An analyzer is different.

It can identify problems that are technically valid C# code but violate a coding rule or could lead to maintainability or performance problems.

For example:

var result = list.Where(x => x > 10).ToList();

This is valid C#.

A custom analyzer could nevertheless identify a project-specific issue with unnecessary materialization, depending on the application's coding standards.


Built-in .NET Analyzers

Modern .NET projects include a large collection of built-in analyzers.

For example:

<PropertyGroup>
    <AnalysisLevel>latest</AnalysisLevel>
</PropertyGroup>

These analyzers can identify categories of issues involving:

  • Code quality

  • Performance

  • Security

  • Reliability

  • API usage

  • Maintainability

  • Design

Warnings typically have diagnostic IDs such as:

CA1001
CA1822
CA2000
CA2016

You can see these diagnostics directly in Visual Studio.


What Does a Diagnostic ID Mean?

Every analyzer diagnostic generally has an identifier.

For example:

CA1822

The ID allows developers and build systems to identify a particular analyzer rule.

A custom analyzer could use an identifier such as:

DOTNET001
DOTNET002
DOTNET003

For example:

DOTNET001 - Avoid empty catch blocks
DOTNET002 - Use async APIs where available
DOTNET003 - Avoid unnecessary ToList()

This becomes particularly useful when building an internal coding standard.


Where Do Roslyn Analyzers Run?

Roslyn analyzers can run in several places.

1. Visual Studio

Developers can see diagnostics while writing code.

For example:

Program.cs

Warning DOTNET001:
Avoid empty catch blocks.

The IDE can underline the problematic code.


2. .NET CLI

Analyzers can also participate in builds.

For example:

dotnet build

This allows analyzer diagnostics to appear during compilation.


3. CI/CD

Analyzers can be integrated into pipelines such as:

Developer
   ↓
Git commit
   ↓
Pull Request
   ↓
CI Build
   ↓
Roslyn Analyzers
   ↓
Quality Checks
   ↓
Build Result

This helps ensure that coding standards are applied consistently.


Analyzer Severity

Analyzer rules can have different severity levels.

Common levels include:

SeverityMeaning
HiddenNot normally shown to the developer
SuggestionOptional improvement
WarningPotential problem
ErrorBuild-blocking problem

For example:

DOTNET001 = warning
DOTNET002 = error

You can configure these rules using an .editorconfig file.


Configuring Analyzer Rules

An .editorconfig file can be used to configure analyzer severity.

For example:

[*.cs]

dotnet_diagnostic.DOTNET001.severity = warning

dotnet_diagnostic.DOTNET002.severity = error

Now DOTNET001 produces a warning while DOTNET002 is treated as an error.

This is useful for enforcing important project rules.


What Can a Custom Roslyn Analyzer Detect?

A custom analyzer can enforce many types of rules.

For example:

Naming rules

public class customerService
{
}

An analyzer could report:

Class name should use PascalCase.

Empty catch blocks

try
{
    DoSomething();
}
catch
{
}

An analyzer could report:

Empty catch blocks should be avoided.

Logging rules

For example, an organization might require structured logging.

Instead of:

_logger.LogInformation(
    $"User {userId} logged in");

the project could require:

_logger.LogInformation(
    "User {UserId} logged in",
    userId);

A custom analyzer could identify violations.


How Does a Roslyn Analyzer Work?

At a high level, the process looks like this:

C# Source Code
      ↓
Roslyn Compiler
      ↓
Syntax Tree
      ↓
Semantic Model
      ↓
Analyzer
      ↓
Diagnostic
      ↓
Visual Studio / Build

The analyzer examines the code and determines whether a particular rule has been violated.


Syntax Trees

Roslyn represents C# source code as a syntax tree.

Consider:

int number = 10;

Roslyn can represent this as a hierarchy of syntax nodes.

Conceptually:

LocalDeclarationStatement
        |
        +-- VariableDeclaration
                |
                +-- Variable: number
                |
                +-- EqualsValueClause
                        |
                        +-- LiteralExpression: 10

An analyzer can inspect these nodes.


Syntax Analysis vs Semantic Analysis

There are two important concepts when developing analyzers:

Syntax analysis

Syntax analysis examines the structure of the source code.

For example:

public class Customer
{
}

The analyzer can identify that a class declaration exists.

Semantic analysis

Semantic analysis examines what the code actually means.

For example:

Customer customer = GetCustomer();

Semantic analysis can determine that Customer is a particular type.

This distinction is important when writing sophisticated analyzers.


Creating a Roslyn Analyzer

You can create a custom analyzer using Visual Studio or the .NET CLI.

One common approach is to create an analyzer project from a template.

For example:

dotnet new install Microsoft.CodeAnalysis.ProjectTemplates

Then you can create an analyzer project using the available Roslyn analyzer template.

The exact templates can vary depending on the installed .NET SDK and Roslyn tooling.


Basic Analyzer Structure

A typical analyzer contains a class derived from:

DiagnosticAnalyzer

For example:

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class MyAnalyzer : DiagnosticAnalyzer
{
}

The analyzer then defines the diagnostics it can report.


Defining a Diagnostic

A diagnostic describes the problem detected by the analyzer.

For example:

private static readonly DiagnosticDescriptor Rule =
    new DiagnosticDescriptor(
        "DOTNET001",
        "Avoid empty catch blocks",
        "Empty catch blocks should be avoided",
        "CodeQuality",
        DiagnosticSeverity.Warning,
        isEnabledByDefault: true);

The important information includes:

  • Diagnostic ID

  • Title

  • Message

  • Category

  • Severity

  • Whether the rule is enabled by default


Registering an Analyzer

The analyzer registers callbacks that tell Roslyn what it wants to inspect.

For example:

public override void Initialize(
    AnalysisContext context)
{
    context.ConfigureGeneratedCodeAnalysis(
        GeneratedCodeAnalysisFlags.None);

    context.EnableConcurrentExecution();

    context.RegisterSyntaxNodeAction(
        AnalyzeCatchClause,
        SyntaxKind.CatchClause);
}

This tells Roslyn that the analyzer wants to inspect catch clauses.


Detecting an Empty Catch Block

We can implement a simple rule that detects empty catch blocks.

For example:

private static void AnalyzeCatchClause(
    SyntaxNodeAnalysisContext context)
{
    var catchClause =
        (CatchClauseSyntax)context.Node;

    if (catchClause.Block.Statements.Count == 0)
    {
        context.ReportDiagnostic(
            Diagnostic.Create(
                Rule,
                catchClause.GetLocation()));
    }
}

Now code such as:

try
{
    DoSomething();
}
catch
{
}

can trigger:

DOTNET001: Empty catch blocks should be avoided.

Adding a Code Fix

An analyzer can identify a problem, but it can also provide an automatic fix.

This is called a Code Fix Provider.

For example, Visual Studio could display:

DOTNET001: Empty catch blocks should be avoided.

Quick Actions:
    Fix

The developer could select the suggested action.

A code fix provider typically derives from:

CodeFixProvider

Analyzer + Code Fix

The relationship looks like this:

                 C# Code
                    ↓
               Analyzer
                    ↓
              Diagnostic
                    ↓
              Code Fix
                    ↓
             Updated Code

This is one of the most powerful aspects of Roslyn tooling.

You aren't limited to telling developers that something is wrong.

You can potentially help them fix it automatically.


Example: Naming Analyzer

Imagine an organization has this rule:

All service classes must end with Service.

Valid:

public class PaymentService
{
}

Invalid:

public class PaymentManager
{
}

The analyzer can inspect class declarations and report:

DOTNET004:
Service classes should end with 'Service'.

A code fix could potentially rename:

PaymentManager

to:

PaymentService

Roslyn Analyzers for Enterprise Applications

Roslyn Analyzers become particularly interesting in large development teams.

Organizations can create rules for internal architecture and coding standards.

For example:

Company Analyzer
│
├── Naming Rules
├── Logging Rules
├── Security Rules
├── API Rules
├── Architecture Rules
├── Performance Rules
└── Maintainability Rules

These rules can then be distributed through NuGet.


Distributing an Analyzer Through NuGet

A custom analyzer can be packaged as a NuGet package.

For example:

<PackageReference Include="MyCompany.CodeAnalyzers"
                  Version="1.0.0" />

Developers can then add the package to their projects.

The analyzer automatically becomes part of their development environment.

This makes NuGet a convenient distribution mechanism for organization-wide coding standards.


Analyzer Packages vs Normal Libraries

A normal library is generally referenced so that application code can call its APIs.

An analyzer package is different.

Its purpose is to inspect source code during development or compilation.

Conceptually:

Normal Library

Application
    ↓
Library
    ↓
Runtime


Analyzer

Application Source
    ↓
Roslyn
    ↓
Analyzer
    ↓
Diagnostic

Roslyn Analyzer Performance Matters

Analyzers can run frequently while developers are typing.

Therefore, performance is important.

A poorly designed analyzer can negatively affect the development experience.

For example, avoid unnecessarily performing expensive operations for every syntax node.

Prefer targeted registrations.

Instead of analyzing everything:

context.RegisterSyntaxNodeAction(
    Analyze,
    SyntaxKind.IdentifierName);

consider registering only the syntax kinds relevant to your rule.


Generated Code

Many .NET projects contain generated source code.

You generally don't want your analyzer reporting hundreds of warnings against generated files.

Roslyn provides APIs to configure generated-code analysis.

For example:

context.ConfigureGeneratedCodeAnalysis(
    GeneratedCodeAnalysisFlags.None);

The appropriate configuration depends on the rule you are implementing.


Testing a Roslyn Analyzer

Analyzer projects should have automated tests.

A test can provide source code:

var testCode = @"
class CustomerManager
{
}
";

The test then verifies whether the expected diagnostic was produced.

For example:

Expected:

DOTNET004
Service classes should end with 'Service'.

You can also test code fixes.


Analyzer Testing Workflow

A useful testing workflow is:

Input C# Code
      ↓
Run Analyzer
      ↓
Expected Diagnostic?
      ↓
Run Code Fix
      ↓
Expected Updated Code?

This helps prevent regressions when your analyzer evolves.


Where Roslyn Analyzers Fit in a .NET Development Workflow

A mature development workflow might look like:

Developer
    ↓
Visual Studio
    ↓
Roslyn Analyzers
    ↓
Local Build
    ↓
Pull Request
    ↓
CI/CD
    ↓
Automated Tests
    ↓
Production

The same rules can therefore be applied during both development and CI.


Built-in Analyzers vs Custom Analyzers

FeatureBuilt-in AnalyzersCustom Analyzers
Microsoft-provided rulesYesNo
General code qualityYesYes
Organization-specific rulesLimitedYes
Custom architecture rulesLimitedYes
Custom naming rulesYesYes
Custom diagnosticsNoYes
Custom code fixesSomeYes
NuGet distributionYesYes

Built-in analyzers are useful for general code quality.

Custom analyzers are useful when you need rules specific to your organization or product.


When Should You Create a Custom Analyzer?

Creating a custom analyzer makes sense when:

  • The same code-review comment appears repeatedly.

  • A coding rule is easy to describe precisely.

  • Developers frequently make the same mistake.

  • A security rule needs to be enforced automatically.

  • Your architecture has specific restrictions.

  • Your organization has internal coding standards.

  • A rule can be automatically fixed.

For example, if reviewers repeatedly tell developers:

"Don't use this API. Use our wrapper instead."

that is a strong candidate for an analyzer.


When Should You Not Create One?

Not every coding preference needs an analyzer.

Avoid creating analyzers for rules that:

  • Are subjective.

  • Are difficult to define precisely.

  • Produce many false positives.

  • Require significant business context.

  • Cannot provide useful feedback.

A good analyzer should provide clear, actionable feedback.


Roslyn Analyzers and AI

Roslyn analyzers can also complement AI-assisted development.

AI coding tools can generate large amounts of code quickly.

Analyzers can provide deterministic checks against that generated code.

For example:

AI generates C# code
        ↓
Roslyn Analyzer
        ↓
Architecture rules
Security rules
Performance rules
Coding standards
        ↓
Diagnostics

AI can help generate code, while analyzers can enforce predefined rules consistently.


Building Your Own .NET Code Quality Toolkit

If you work with C# professionally, a collection of custom Roslyn rules can become a useful developer tool.

For example:

DotNetCodeQualityKit
│
├── Naming
├── Exception Handling
├── Logging
├── Dependency Injection
├── Async/Await
├── LINQ
├── Performance
├── Security
├── API Design
└── Architecture

Each rule can have its own diagnostic ID.

For example:

DOTNET001
DOTNET002
DOTNET003
...
DOTNET025

You can package these rules into a NuGet package and distribute them across projects.


Example Rule Catalog

A custom toolkit could contain rules such as:

RuleExample
DOTNET001Avoid empty catch blocks
DOTNET002Avoid async void
DOTNET003Avoid unnecessary ToList()
DOTNET004Enforce service naming
DOTNET005Require structured logging
DOTNET006Avoid direct new HttpClient()
DOTNET007Require cancellation tokens
DOTNET008Avoid synchronous blocking on async code
DOTNET009Avoid hard-coded connection strings
DOTNET010Enforce dependency injection

The exact rules should be designed around real problems in the target codebase rather than simply maximizing the number of diagnostics.


Benefits of Roslyn Analyzers

The major benefits include:

Earlier feedback

Problems can be detected while developers are writing code.

Consistent standards

Teams can apply the same rules across projects.

Better code reviews

Reviewers can focus on architecture and business logic instead of repetitive style issues.

CI/CD enforcement

Rules can be applied automatically during builds.

Automated fixes

Code Fix Providers can help developers correct common problems.

Reusable tooling

An analyzer can be packaged and reused across many projects.


Limitations of Roslyn Analyzers

Analyzers are powerful, but they aren't a replacement for every form of testing or review.

They generally should not replace:

  • Unit tests

  • Integration tests

  • Security testing

  • Architecture reviews

  • Performance testing

  • Human code review

Instead, they complement these activities.


Roslyn Analyzer Architecture at a Glance

A simplified architecture looks like this:

                  C# Source Code
                         │
                         ▼
                 Roslyn Compiler
                         │
              ┌──────────┴──────────┐
              │                     │
        Syntax Tree            Semantic Model
              │                     │
              └──────────┬──────────┘
                         ▼
                  Roslyn Analyzer
                         │
                         ▼
                    Diagnostic
                         │
              ┌──────────┴──────────┐
              │                     │
          IDE Warning          Build Warning
              │
              ▼
         Code Fix Provider
              │
              ▼
          Fixed Source

Conclusion

Roslyn Analyzers provide a powerful way to improve C# and .NET code quality.

They can identify coding problems, enforce organization-specific standards, integrate with Visual Studio and CI/CD pipelines, and even provide automatic code fixes.

For individual developers, built-in .NET analyzers are a good starting point.

For development teams, custom analyzers can enforce rules specific to their architecture and coding standards.

And for developers building reusable developer tools, Roslyn provides an opportunity to turn common code-review rules into a real .NET developer product.

If you repeatedly see the same coding mistake during code reviews, consider this question:

Could this rule be detected automatically?

If the answer is yes, a Roslyn Analyzer may be the right tool.


Quick Reference

Roslyn
   ↓
.NET Compiler Platform

Analyzer
   ↓
Inspects C# code

Diagnostic
   ↓
Reports a coding problem

Code Fix
   ↓
Provides an automatic correction

NuGet
   ↓
Distributes the analyzer

CI/CD
   ↓
Enforces rules during builds

Key Classes

DiagnosticAnalyzer
DiagnosticDescriptor
CodeFixProvider
AnalysisContext
SyntaxNodeAnalysisContext

Key Concepts

Syntax Tree
Semantic Model
Diagnostic
Code Fix
AnalyzerConfig
.editorconfig
NuGet
CI/CD

Roslyn turns the C# compiler into a platform that developers can extend—and that makes it possible to build sophisticated code-quality and developer-productivity tools for the .NET ecosystem.

Home

Looking for 1:1

Comments

Popular posts from this blog

How to Install .NET 10 SDK on Windows, Linux, and macOS

Microsoft Agent Framework Explained: A .NET Developer’s Architecture Guide

Why Is Your ASP.NET Core API Using So Much Memory? 7 Causes and Fixes