Boosting Productivity with Analyzers

06/05/2024

I am a big fan of Analyzers, and in this blog post, I will showcase some of my favorite ones!

What is the fuse about?

Analyzers are basically a part of the compiler chain that can and will constantly check your code for potential issues. Often times this is categorized (like "performance", "potential bugs", "design" and so on) and has some severity. The idea is to catch issues early and to help you write better code. And this is why I like them - you get immediate feedback.

.NET Analyzers

Of course the .NET team themselves ship a bunch of analyzers. With each new installment new analyzers are added. You can find the overview here: https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview?tabs=net-8.

To enable them you can add the following to your project file:

<PropertyGroup Label="Analyzer settings">
    <EnableNETAnalyzers>true</EnableNETAnalyzers> <!-- Since .NET 5 this is true by default -->
    <AnalysisLevel>latest</AnalysisLevel>
    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>

Bonus points, if you add this to your Directory.Build.props file, so it applies to all projects in your solution. The EnableNETAnalyzers will enable all analyzers that are shipped with the .NET SDK (since .NET 5 it is enabled by default so you don't have to set this flag at all). The AnalysisLevel gives you a bit more fine-grained control over which analyzers are enabled. In the example I used latest. This is refers to the used NET SDK version. So if your building with .NET 8 SDK then the latest analyzers for .NET 8 will be used. You can also specific version number like 8.0 or 7.0. That can be helpful if you upgrade your SDK and there is a new wave of analyzers introduced. As earlier mentioned the analzers are categorized. You can enable or disable them by category as well: <AnalysisLevelPerformance>latest</AnalysisLevelPerformance>. This will enable all performance related analyzers for the latest SDK building the project.

If you check the code-analysis link above you'll notice that they have different severity levels. Sometimes you want to override the severity level - either because you don't want something as an error or warning or the opposite: You want to enforce a certain rule. My preferred way is to use the .editorconfig file for this (you can also adjust this in your project file - this is just my preferred way):

[*.cs]
dotnet_diagnostic.IDE0290.severity = none # IDE0290: Use primary constructor - by default it gives a suggestion

Now some bonus points for you if you also add the following to your Directory.Build.props file:

<PropertyGroup>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

This will make sure that all warnings are treated as errors. You can also explicitly set this only for Analyzers:

<PropertyGroup>
    <CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>
</PropertyGroup>

Now I did give you a lot of information about how to enable and configure the analyzers. Let's see a few of them in action. For example: CA2021: Don't call Enumerable.Cast<T> or Enumerable.OfType<T> with incompatible types.

var foods = new List<Food>();
// Violation - Food is incompatible with Beverages.
var drinks = Enumerable.Cast<Beverages>(foods);
// Violation - Food is incompatible with Beverages.
var drinks2 = Enumerable.OfType<Beverages>(foods);

class Food { }
class Bread : Food { }
class Beverages { }

Source: https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2021

Here we have a list of Food and we try to cast it to Beverages. So the compiler already knows that this is not going to work. The Cast will thrown an exception and OfType will always result in an empty enumeration. So this is a good example of how the analyzers can help you to write better code.

SonarAnalyzer

I don't think I did setup or worked in a project the last 5 years where this one isn't part of the project. It is a great addition to the .NET analyzers. You can find the documentation here: https://github.com/SonarSource/sonar-dotnet and the massive ruleset here: https://rules.sonarsource.com/csharp.

Here are some examples from their documentation:

public string GetReadableStatus(Job j)
{
  return j.IsRunning ? "Running" : j.HasErrors ? "Failed" : "Succeeded";  // Noncompliant
}

The analyzer will warn you as nested ternary operator can be really hard to read. It is better to split this into multiple lines.

public string GetReadableStatus(Job j)
{
  if (j.IsRunning)
  {
    return "Running";
  }
  return j.HasErrors ? "Failed" : "Succeeded";
}

It can also prevent bugs or silly mistakes like the following:

if (x < 0)
{
    new ArgumentException("x must be nonnegative");
}

Here the analyzer will warn you that you probably want a throw statement in front of the new ArgumentException. Otherwise the exception will be created but not thrown. Another cool thing is that they incorporates common standards and best practices and it can warn you if you deviate from them.

public override string ToString ()
{
  if (this.collection.Count == 0)
  {
    return null; // Noncompliant
  }
  else
  {
    // ...
  }
}

Ideally ToString should not return a null string but "at least" string.Empty.

Meziantou.Analyzer

I see this also more and more in projects. Here the link to the documentation: https://github.com/meziantou/Meziantou.Analyzer. The GitHub page gives a nice overview over the severity of the rules and if code-fixes are provided as well (so you can automatically fix the issue). An all-time classic s string comparison:

string.Equals("a", "b"); // non-compliant as the default comparison of string.Equals is Ordinal

// Should be
string.Equals("a", "b", StringComparison.Ordinal);

IDisposableAnalyzers

This one I used more recently, especially in my NCronJob library. Handling with unmanaged resources can be a pain and a potential area of memory leaks. This analyzer will help you to identify potential issues with IDisposable objects. You can find the documentation here: https://github.com/DotNetAnalyzers/IDisposableAnalyzers. Such analyzers are especially helpful if you have long running things where such things can accumulate over time.

Here is an example from my personal code, can you detect the issue?

using var provider = ServiceCollection.BuildServiceProvider();
using var executor = provider.CreateScope().ServiceProvider.GetRequiredService<JobExecutor>();

...

internal sealed partial class JobExecutor : IDisposable {}

Looks okay, doesn't it? We are disposing provider that is an ServiceProvider (which indeed is a IDisposable) and we are disposing executor. The problem is that CreateScope returns a IServiceScope which is also IDisposable. So we should dispose this as well. The analyzer will warn you about this. So the fix is something like this:

using var provider = ServiceCollection.BuildServiceProvider();
using var serviceScope = provider.CreateScope();
using var executor = serviceScope.ServiceProvider.GetRequiredService<JobExecutor>();

This analyzer makes a lot of sense when you deal with many IDisposable objects and it is easy to forget to dispose them.

A word of caution

I know it is tempting to enable all the analyzers and set them to a higher severity. Please don't do this. Analyzers are a nice safety net for things you might struggle with. So blindly adding them will make it worse over time. I know that especially the "Performance" category can be tempting. But please be aware that premature optimization can lead to long-term consequences and can make your code harder to read and maintain. So please be careful with this.

3
An error has occurred. This application may no longer respond until reloaded. Reload x