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:
| Severity | Meaning |
|---|---|
| Hidden | Not normally shown to the developer |
| Suggestion | Optional improvement |
| Warning | Potential problem |
| Error | Build-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
| Feature | Built-in Analyzers | Custom Analyzers |
|---|---|---|
| Microsoft-provided rules | Yes | No |
| General code quality | Yes | Yes |
| Organization-specific rules | Limited | Yes |
| Custom architecture rules | Limited | Yes |
| Custom naming rules | Yes | Yes |
| Custom diagnostics | No | Yes |
| Custom code fixes | Some | Yes |
| NuGet distribution | Yes | Yes |
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:
| Rule | Example |
|---|---|
| DOTNET001 | Avoid empty catch blocks |
| DOTNET002 | Avoid async void |
| DOTNET003 | Avoid unnecessary ToList() |
| DOTNET004 | Enforce service naming |
| DOTNET005 | Require structured logging |
| DOTNET006 | Avoid direct new HttpClient() |
| DOTNET007 | Require cancellation tokens |
| DOTNET008 | Avoid synchronous blocking on async code |
| DOTNET009 | Avoid hard-coded connection strings |
| DOTNET010 | Enforce 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.
Looking for 1:1
Comments
Post a Comment