Writing a Custom Roslyn Analyser from Scratch
Roslyn analysers run inside the compiler and IDE, providing real-time diagnostics without requiring developers to run a separate tool. In this article, we will build a custom analyser that flags methods exceeding a configurable line count.
Project Setup
Create a new class library targeting netstandard2.0 — analysers must target this framework for broad compatibility:
dotnet new classlib -n MethodLengthAnalyser -f netstandard2.0
cd MethodLengthAnalyser
dotnet add package Microsoft.CodeAnalysis.Analyzers
dotnet add package Microsoft.CodeAnalysis.CSharp
You also need to mark the project as an analyser in the .csproj:
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
Defining the Diagnostic
Every analyser reports one or more diagnostics. Each diagnostic needs an ID, a title, a message format, a category, and a default severity.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class MethodLengthAnalyser : DiagnosticAnalyzer
{
public const string DiagnosticId = "ML001";
private static readonly DiagnosticDescriptor Rule = new(
id: DiagnosticId,
title: "Method is too long",
messageFormat: "Method '{0}' is {1} lines long (maximum: {2})",
category: "Maintainability",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "Methods that exceed the configured line limit " +
"are harder to understand and maintain.");
public override ImmutableArray<DiagnosticDescriptor>
SupportedDiagnostics => ImmutableArray.Create(Rule);
The DiagnosticDescriptor is what the IDE uses to display the squiggly line, populate the Error List, and show the rule in EditorConfig settings.
Registering the Analysis Action
Analysers work by registering callbacks for specific syntax or symbol events. For our case, we want to inspect every method declaration:
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(
GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(
AnalyseMethod,
SyntaxKind.MethodDeclaration);
}
Two important calls here. ConfigureGeneratedCodeAnalysis(None) tells Roslyn to skip generated files — you do not want to flag auto-generated code. EnableConcurrentExecution() allows the analyser to run on multiple threads, which is essential for performance.
Implementing the Analysis Logic
private void AnalyseMethod(SyntaxNodeAnalysisContext context)
{
var method = (MethodDeclarationSyntax)context.Node;
// Read the threshold from an .editorconfig option, defaulting to 30
var config = context.Options.AnalyzerConfigOptionsProvider
.GetOptions(context.Node.SyntaxTree);
int maxLines = 30;
if (config.TryGetValue(
"dotnet_diagnostic.ML001.max_lines", out var value)
&& int.TryParse(value, out var parsed))
{
maxLines = parsed;
}
if (method.Body is null && method.ExpressionBody is null)
return;
var span = method.GetLocation().GetLineSpan();
int lineCount = span.EndLinePosition.Line
- span.StartLinePosition.Line + 1;
if (lineCount > maxLines)
{
var diagnostic = Diagnostic.Create(
Rule,
method.Identifier.GetLocation(),
method.Identifier.Text,
lineCount,
maxLines);
context.ReportDiagnostic(diagnostic);
}
}
}
We read from EditorConfig so teams can configure their preferred threshold. A consumer can add this to their .editorconfig:
[*.cs]
dotnet_diagnostic.ML001.max_lines = 50
Packaging and Consuming
To distribute your analyser as a NuGet package, add the following to your .csproj:
<PropertyGroup>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<IncludeBuildOutput>false</IncludeBuildOutput>
<DevelopmentDependency>true</DevelopmentDependency>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
</PropertyGroup>
<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll"
Pack="true"
PackagePath="analyzers/dotnet/cs" />
</ItemGroup>
The key detail is the PackagePath — analysers must be placed in the analyzers/dotnet/cs folder inside the NuGet package for the compiler to discover them.
Consumer projects then simply reference the package:
<PackageReference Include="MethodLengthAnalyser" Version="1.0.0" />
No additional configuration is needed. The compiler picks up the analyser automatically, and the IDE shows warnings in real time as the developer types.
Key Takeaways
- Target
netstandard2.0for analyser projects. - Always call
EnableConcurrentExecution()andConfigureGeneratedCodeAnalysis(). - Use
DiagnosticDescriptorfor each rule — this integrates with EditorConfig suppression. - Read configuration from
AnalyzerConfigOptionsProviderto make thresholds tuneable. - Package analysers into the
analyzers/dotnet/cspath inside your NuGet package.
With this foundation, you can build analysers for any team convention — from naming rules to architectural constraints.