Skip to main content

ESLint your C# in VS Code with Roslyn Analyzers

· 11 min read
John Reilly
OSS Engineer - TypeScript, Azure, React, Node.js, .NET

ESLint provides a great linting experience for TypeScript and JavaScript in VS Code. The suggestions, fixes and ignore options make creating clean code a joy. A similar experience is available for C# in VS Code through Roslyn Analyzers - this post tells us more.

title image reading "ESLint your C# in VS Code with Roslyn Analyzers" with the C# and VS Code logos`

Linting and C#

JavaScript and TypeScript benefit from a tremendous tooling ecosystem which allows us to simply format and lint our codebases as we're editing. Similar tooling exists for C#. Previously I wrote about using dotnet-format to have a Prettier-like experience for formatting our C#. If that last post focussed on formatting C#; looking through the lens of Prettier, this post focusses on linting; looking through the lens of ESLint.

Roslyn Analyzers

There's often overlap between linting and formatting tooling; and so it goes with C# as well. Linting and formatting in the .NET space make use of the Roslyn Analyzers:

Roslyn analyzers analyze your code for style, quality and maintainability, design and other issues. The documentation for Roslyn Analyzers can be found at docs.microsoft.com/dotnet/fundamentals/code-analysis/overview.

To learn more about them, it's worth reading the excellent piece on the topic by Ian Griffiths.

"Analyse this"

In order that we can see what the linting experience is like in VS Code, we're going to need a project to work on. We have the .NET 6 SDK installed, so we'll create ourselves a project:

dotnet new webapi -o AnalyseThis

We have the C# extension installed already, but we're getting no feedback on the code. Maybe it's already beautiful?

Or maybe not. We're going to need an .editorconfig file to control all the code style settings. You can create this directly using the dotnet CLI like so;

dotnet new editorconfig

Once this runs, it creates a file with all of the settings in with their default values. Alongside that, we need to wake VS Code up to our brave new world by setting the following in our settings.json:

{
"omnisharp.enableRoslynAnalyzers": true,
"omnisharp.enableEditorConfigSupport": true
}

Or alternatively, use the GUI in VS Code to set these settings directly:

screenshot of the VS Code settings screen

It's then a good idea to turn OmniSharp off and on again, so it picks up these changes:

screenshot of the VS Code "restart OmniSharp"

Then, excitingly, we start to see code analysis, or linting, messages in the problems pane of VS Code:

screenshot of a first linting message and the code to which it applies

It's possible to use the dotnet-format command to surface this information:

dotnet format style -v detailed --severity info --verify-no-changes
The dotnet runtime version is '6.0.2'.
Formatting code files in workspace '/workspaces/AnalyseThis.csproj'.
Determining projects to restore...
All projects are up-to-date for restore.
Project AnalyseThis is using configuration from '/workspaces/.editorconfig'.
Project AnalyseThis is using configuration from '/workspaces/obj/Debug/net6.0/AnalyseThis.GeneratedMSBuildEditorConfig.editorconfig'.
Project AnalyseThis is using configuration from '/usr/share/dotnet/sdk/6.0.200/Sdks/Microsoft.NET.Sdk/analyzers/build/config/analysislevel_6_default.editorconfig'.
Running 45 analyzers on AnalyseThis.
/workspaces/Controllers/WeatherForecastController.cs(14,57): info IDE0052: Private member 'WeatherForecastController._logger' can be removed as the value assigned to it is never read [/workspaces/AnalyseThis.csproj]
Formatted code file '/workspaces/Controllers/WeatherForecastController.cs'.
Formatted 1 of 6 files.
Format complete in 7993ms.

Note the IDE0052: Private member 'WeatherForecastController._logger' can be removed as the value assigned to it is never read message above.

Now fail my build!

This is all very exciting - we've a world of extra linting at our fingertips! But what's a touch disappointing, is that the above information isn't surfaced in my build. What if as a team we commit to a particular code style? If I can't enforce that in the build, it's likely not going to happen.

So what do I do? Well, the information is out there on how to do this, but it's easy to miss. You can find the details here. We update our AnalyseThis.csproj to include an EnforceCodeStyleInBuild setting like so:

  <PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>

<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>

We're going to replace our exhaustive .editorconfig file with a much simpler one:

# Remove the line below if you want to inherit .editorconfig settings from higher directories
root = true

[*.cs]
# Default severity for analyzer diagnostics with category 'Style' (escalated to build warnings)
dotnet_analyzer_diagnostic.category-Style.severity = warning

Do you see what we did here? We told our build to treat Style diagnostics (lints) as warnings. Once OmniSharp picks this up, more linting messages start to appear in the problems pane of VS Code:

screenshot of more linting messages

And what's more, if we attempt to build, guess what?

dotnet build
Microsoft (R) Build Engine version 17.1.0+ae57d105c for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

Determining projects to restore...
All projects are up-to-date for restore.
/workspaces/AnalyseThis/Controllers/WeatherForecastController.cs(3,1): warning IDE0160: Convert to block scoped namespace [/workspaces/AnalyseThis/AnalyseThis.csproj]
/workspaces/AnalyseThis/WeatherForecast.cs(1,1): warning IDE0160: Convert to block scoped namespace [/workspaces/AnalyseThis/AnalyseThis.csproj]
/workspaces/AnalyseThis/Program.cs(1,1): warning IDE0008: Use explicit type instead of 'var' [/workspaces/AnalyseThis/AnalyseThis.csproj]
/workspaces/AnalyseThis/Program.cs(10,1): warning IDE0008: Use explicit type instead of 'var' [/workspaces/AnalyseThis/AnalyseThis.csproj]
/workspaces/AnalyseThis/Program.cs(15,5): warning IDE0058: Expression value is never used [/workspaces/AnalyseThis/AnalyseThis.csproj]
/workspaces/AnalyseThis/Program.cs(16,5): warning IDE0058: Expression value is never used [/workspaces/AnalyseThis/AnalyseThis.csproj]
AnalyseThis -> /workspaces/AnalyseThis/bin/Debug/net6.0/AnalyseThis.dll

Build succeeded.

/workspaces/AnalyseThis/Controllers/WeatherForecastController.cs(3,1): warning IDE0160: Convert to block scoped namespace [/workspaces/AnalyseThis/AnalyseThis.csproj]
/workspaces/AnalyseThis/WeatherForecast.cs(1,1): warning IDE0160: Convert to block scoped namespace [/workspaces/AnalyseThis/AnalyseThis.csproj]
/workspaces/AnalyseThis/Program.cs(1,1): warning IDE0008: Use explicit type instead of 'var' [/workspaces/AnalyseThis/AnalyseThis.csproj]
/workspaces/AnalyseThis/Program.cs(10,1): warning IDE0008: Use explicit type instead of 'var' [/workspaces/AnalyseThis/AnalyseThis.csproj]
/workspaces/AnalyseThis/Program.cs(15,5): warning IDE0058: Expression value is never used [/workspaces/AnalyseThis/AnalyseThis.csproj]
/workspaces/AnalyseThis/Program.cs(16,5): warning IDE0058: Expression value is never used [/workspaces/AnalyseThis/AnalyseThis.csproj]
6 Warning(s)
0 Error(s)

Time Elapsed 00:00:06.53

That's right! The same messages from the problems pane are now surfaced in our build as warnings. And we can kick it up a notch too; let's make them errors:

# Remove the line below if you want to inherit .editorconfig settings from higher directories
root = true

[*.cs]
# Default severity for analyzer diagnostics with category 'Style' (escalated to build errors)
dotnet_analyzer_diagnostic.category-Style.severity = error

Once OmniSharp catches up we see our warnings transform into errors:

screenshot of a more linting messages

And if we build...

dotnet build
Microsoft (R) Build Engine version 17.1.0+ae57d105c for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

Determining projects to restore...
All projects are up-to-date for restore.
/workspaces/AnalyseThis/WeatherForecast.cs(1,1): error IDE0160: Convert to block scoped namespace [/workspaces/AnalyseThis/AnalyseThis.csproj]
/workspaces/AnalyseThis/Controllers/WeatherForecastController.cs(3,1): error IDE0160: Convert to block scoped namespace [/workspaces/AnalyseThis/AnalyseThis.csproj]
/workspaces/AnalyseThis/Program.cs(1,1): error IDE0008: Use explicit type instead of 'var' [/workspaces/AnalyseThis/AnalyseThis.csproj]
/workspaces/AnalyseThis/Program.cs(10,1): error IDE0008: Use explicit type instead of 'var' [/workspaces/AnalyseThis/AnalyseThis.csproj]
/workspaces/AnalyseThis/Program.cs(15,5): error IDE0058: Expression value is never used [/workspaces/AnalyseThis/AnalyseThis.csproj]
/workspaces/AnalyseThis/Program.cs(16,5): error IDE0058: Expression value is never used [/workspaces/AnalyseThis/AnalyseThis.csproj]

Build FAILED.

/workspaces/AnalyseThis/WeatherForecast.cs(1,1): error IDE0160: Convert to block scoped namespace [/workspaces/AnalyseThis/AnalyseThis.csproj]
/workspaces/AnalyseThis/Controllers/WeatherForecastController.cs(3,1): error IDE0160: Convert to block scoped namespace [/workspaces/AnalyseThis/AnalyseThis.csproj]
/workspaces/AnalyseThis/Program.cs(1,1): error IDE0008: Use explicit type instead of 'var' [/workspaces/AnalyseThis/AnalyseThis.csproj]
/workspaces/AnalyseThis/Program.cs(10,1): error IDE0008: Use explicit type instead of 'var' [/workspaces/AnalyseThis/AnalyseThis.csproj]
/workspaces/AnalyseThis/Program.cs(15,5): error IDE0058: Expression value is never used [/workspaces/AnalyseThis/AnalyseThis.csproj]
/workspaces/AnalyseThis/Program.cs(16,5): error IDE0058: Expression value is never used [/workspaces/AnalyseThis/AnalyseThis.csproj]
0 Warning(s)
6 Error(s)

Time Elapsed 00:00:04.22

Yes! Our style diagnostics are now failing the build. This is terrific!

Categories

It's worth pausing a second and considering the category upgrade we did here:

dotnet_analyzer_diagnostic.category-Style.severity = error

There's a number of different categories that encapsulate groups of rules, they're documented here. Taken from there you can see the wealth of different categories that exist:

CategoryDescriptionEditorConfig value
Design rulesDesign rules support adherence to the .NET Framework Design Guidelines.dotnet_analyzer_diagnostic.category-Design.severity
Documentation rulesDocumentation rules support writing well-documented libraries through the correct use of XML documentation comments for externally visible APIs.dotnet_analyzer_diagnostic.category-Documentation.severity
Globalization rulesGlobalization rules support world-ready libraries and applications.dotnet_analyzer_diagnostic.category-Globalization.severity
Portability and interoperability rulesPortability rules support portability across different platforms. Interoperability rules support interaction with COM clients.dotnet_analyzer_diagnostic.category-Interoperability.severity
Maintainability rulesMaintainability rules support library and application maintenance.dotnet_analyzer_diagnostic.category-Maintainability.severity
Naming rulesNaming rules support adherence to the naming conventions of the .NET design guidelines.dotnet_analyzer_diagnostic.category-Naming.severity
Performance rulesPerformance rules support high-performance libraries and applications.dotnet_analyzer_diagnostic.category-Performance.severity
SingleFile rulesSingle-file rules support single-file applications.dotnet_analyzer_diagnostic.category-SingleFile.severity
Reliability rulesReliability rules support library and application reliability, such as correct memory and thread usage.dotnet_analyzer_diagnostic.category-Reliability.severity
Security rulesSecurity rules support safer libraries and applications. These rules help prevent security flaws in your program.dotnet_analyzer_diagnostic.category-Security.severity
Style rulesStyle rules support consistent code style in your codebase. These rules start with the "IDE" prefix.dotnet_analyzer_diagnostic.category-Style.severity
Usage rulesUsage rules support proper usage of .NET.dotnet_analyzer_diagnostic.category-Usage.severity
N/AYou can use this EditorConfig value to enable the following rules: IDE0051, IDE0064, IDE0076. While these rules start with "IDE", they are not technically part of the Style category.dotnet_analyzer_diagnostic.category-CodeQuality.severity

The IDE0052 information we saw when we used dotnet format earlier is technically part of the CodeQuality category. If we wanted to, we we could dial that up that category to an error like so:

# Default severity for analyzer diagnostics with category 'CodeQuality' (escalated to build errors)
dotnet_analyzer_diagnostic.category-CodeQuality.severity = error

Opt out of rules

As it turns out, I disagree with the complaints I'm getting on the codebase right now, so I'd like to dial those down to ignore. To do that globally, you simply put configuration in the .editorconfig to reflect that:

# Remove the line below if you want to inherit .editorconfig settings from higher directories
root = true

[*.cs]
# Default severity for analyzer diagnostics with category 'Style' (escalated to build warnings)
dotnet_analyzer_diagnostic.category-Style.severity = error

dotnet_diagnostic.IDE0008.severity = none
dotnet_diagnostic.IDE0058.severity = none
dotnet_diagnostic.IDE0160.severity = none

What we're doing here is saying "upgrade all style rules to be errors, but IDE0008, IDE0058 and IDE0160 (which are style rules) - ignore those; don't tell me about them".

Now I'm not going to be bothered by those errors in future. Great.

Dial up information to warning

If we look again at our problems pane in VS Code, we can see there's an entry there. It's not an error, it's not a warning. It's information:

screenshot of a first linting message and the code to which it applies

Let's say we want to take that and dial it up to be a warning, such that it surfaces in the build too. We can with a simple addition to our .editorconfig:

# Remove the line below if you want to inherit .editorconfig settings from higher directories
root = true

[*.cs]
# Default severity for analyzer diagnostics with category 'Style' (escalated to build warnings)
dotnet_analyzer_diagnostic.category-Style.severity = error

dotnet_diagnostic.IDE0008.severity = none
dotnet_diagnostic.IDE0058.severity = none
dotnet_diagnostic.IDE0160.severity = none

# Roslyn analzer surfaces this as information - we'll dial it up to a warning
dotnet_diagnostic.IDE0052.severity = warning

Once OmniSharp notices:

screenshot of our information now a warning

And if we run the build, there it is!

dotnet build
Microsoft (R) Build Engine version 17.1.0+ae57d105c for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

Determining projects to restore...
All projects are up-to-date for restore.
/workspaces/AnalyseThis/Controllers/WeatherForecastController.cs(14,57): warning IDE0052: Private member 'WeatherForecastController._logger' can be removed as the value assigned to it is never read [/workspaces/AnalyseThis/AnalyseThis.csproj]
AnalyseThis -> /workspaces/AnalyseThis/bin/Debug/net6.0/AnalyseThis.dll

Build succeeded.

/workspaces/AnalyseThis/Controllers/WeatherForecastController.cs(14,57): warning IDE0052: Private member 'WeatherForecastController._logger' can be removed as the value assigned to it is never read [/workspaces/AnalyseThis/AnalyseThis.csproj]
1 Warning(s)
0 Error(s)

Time Elapsed 00:00:02.21

Deactivate linting partially

Let's say we want to ignore that one warning. We'd like the equivalent functionality to // eslint-disable-next-line. That doesn't exist alas. However, what does is the equivalent to this:

/* eslint-disable */

alert('foo');

/* eslint-enable */

In our case what we'd do is this:

#pragma warning disable
private readonly ILogger<WeatherForecastController> _logger;
#pragma warning restore

Or to be more specific:

#pragma warning disable IDE0052
private readonly ILogger<WeatherForecastController> _logger;
#pragma warning restore IDE0052

And now we can opt out of that rule in this specific place - whilst maintaining it more generally.

Conclusion

There's powerful linting tools in C#, hopefully this guide has made it easier for you to surface them, control them and apply them both to VS Code and to your build.

Thanks to Joey Robichaud, Tim Heuer and Youssef Victor for some excellent pointers that fed into the writing of this post. You can see the help they provided here.