Introduction

There are several code analysers available, and I’m not just referring to those that highlight code smells. I’m talking about analysers that assess technical debt, anti-patterns, and even poor architectural decisions.

SonarQube is one of the first tools to stand out, as it integrates with many different languages and seamlessly integrates with delivery pipelines to ensure that only the cleanest code reaches production (we don’t ignore SonarQube warnings, do we?).

What sets NDepend apart from the usual bunch then? Let’s take a dive into it and see what we uncover.

Disclosure - NDepend reached out to me with a professional key but absolutely zero obligations. Anything in this post is my unbiased opinion, as an individual, and your mileage may vary.

What is bad code?

Let’s start by exploring what qualifies as bad code and the expectations we should have for code analysers like NDepend and SonarQube.

This is not an exhaustive list, but here are a few examples:

Code Smells

The most basic form of bad code. What makes it especially dangerous is that it usually compiles without a problem and probably won’t give off any runtime errors, but it will sit there and contaminate your codebase.

A simple example is starting with a method that has a parameter as shown below:

public class Circle
{
    public double CalculateCircumference(double radius)
    {
        return 2 * 3.14159 * radius; // Magic number 3.14159 for Pi
    }
}

The above seems pretty standard, right? Except for the following:

  • It’s hard to read at a glance.
  • It’s hard to change.

Let’s say, for instance, that the solution uses Pi frequently in its calculations, and someone notices inaccuracies because not enough decimal places are being used. They’d have to search the codebase for that specific number and replace/update it with the new value.

A better approach would be:

public class Circle
{
    private const double Pi = 3.14159; // Constant with a meaningful name

    public double CalculateCircumference(double radius)
    {
        return 2 * Pi * radius;
    }
}

Design Smells

These issues often occur when we design after we’ve already started coding—or when we simply don’t understand what we are designing.

A good example would be creating an instantiated class with member methods that could absolutely have been static:

public class RestBroker : IRestBroker
{
    private readonly HttpClient _httpClient;

    public RestBroker(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<string> GetAsync(string url)
    {
        var response = await _httpClient.GetAsync(url);
        return await response.Content.ReadAsStringAsync();
    }
}

I’ve been guilty of this in the past. Ultimately, it’s unnecessary to use dependency injection for an object that doesn’t need instantiation.

A better implementation would look something like this:

public static class RestBroker
{
    private static readonly HttpClient _httpClient = new HttpClient();

    public static async Task<string> GetAsync(string url)
    {
        var response = await _httpClient.GetAsync(url);
        return await response.Content.ReadAsStringAsync();
    }
}

Architecture Smells

This happens when we mix architectural approaches or blatantly ignore them. It leads to code that’s hard to maintain and very hard to fix if you let it fester.

Consider mutual dependencies between namespaces—where namespaces depend on each other, as shown below:

namespace NamespaceA
{
    using NamespaceB;

    public class ClassA
    {
        private readonly ClassB _classB;

        public ClassA(ClassB classB)
        {
            _classB = classB;
        }

        public void MethodInA()
        {
            Console.WriteLine("Method in ClassA called.");
            _classB.MethodInB();
        }
    }
}

namespace NamespaceB
{
    using NamespaceA;

    public class ClassB
    {
        private readonly ClassA _classA;

        public ClassB(ClassA classA)
        {
            _classA = classA;
        }

        public void MethodInB()
        {
            Console.WriteLine("Method in ClassB called.");
            _classA.MethodInA();
        }
    }
}

Why is this a problem?

  • Tight Coupling Across Namespaces: Both NamespaceA and NamespaceB must include each other’s references to function, creating a circular dependency.
  • Hard to Refactor: Any changes in NamespaceA.ClassA or NamespaceB.ClassB might require changes in the other namespace, propagating across the codebase.
  • Complex Build Dependencies: This can cause compilation issues in larger projects where namespaces are split across different assemblies.

Tech Debt

The above examples and more contribute to your tech debt—a metric that estimates the time required to bring a codebase up to best practices. A good codebase would have tech debt measured in minutes, but as issues accumulate, this can escalate to hours, days, or even months.

Unaddressed tech debt accrues interest yearly, based on the time estimated to resolve it. This is less impactful when the debt is at least covered by sufficient test coverage (100%).

Eventually, your tech debt will reach a breaking point—when the cost to fix it equals or exceeds the cost of leaving it unaddressed. At this point, addressing the debt becomes critical to avoid a complete rewrite of the codebase.

Why do we need tools like NDepend?

As codebases grow, so does the risk of accumulating tech debt, code smells, and architectural issues. Manual reviews can only go so far—automated tools are essential for maintaining code quality and keeping projects healthy over time.

What is NDepend?

Simply put, NDepend is a code-quality-report generating tool built for .NET developers. It can be run as a standalone application, integrated with Visual Studio (but not Rider, yet), or run as part of your CI/CD pipelines.

In short, it helps us better understand our codebase. If you’ve ever managed a large codebase, you’ll know how hard it is to track tech debt and other issues without some form of tooling.

Installation is straightforward and works as a portable executable. From there, you can install the integration with Visual Studio. Since I don’t use Visual Studio, I decided to explore how to use it as a standalone tool.

NDepend Analysis

I ran NDepend on our Roslin codebase, anticipating several issues, and the results were quite… overwhelming.

NDepend acknowledges this in their documentation: the report can be overwhelming at first. Here’s an example of the initial report view:

Image showing the report window of NDepend

There’s a lot to digest here, so let’s break it down into smaller sections.

Dashboard

The central part of the screen is the dashboard, which provides a great overview of your project. It’s perfect for any numbers geek out there.

Image of the dashboard

I haven’t added any coverage data yet, which I still need to figure out and may address in a future post. This view, however, breaks the solution into actionable numbers. It’s not just a static dashboard—almost everything is clickable, leading to filtered views in other sections.

Gif of the dashboard being clicked

Query and Rules Explorer

At the bottom of the screen is the query and rules explorer.

Image showing the query and rules explorer

This is a great way to view specific issues by criticality and allows for customizable querying. It’s particularly helpful for understanding where quality gates have been breached or where tech debt is accumulating.

Filtering through the query explorer narrows down results shown in the query editor, so you can quickly locate offending namespaces, classes, and files.

Query Editor

On the left-hand side is the query editor.

Image showing the query edit window

By default, this shows assemblies and allows you to drill down. As you filter in the query explorer, the editor view dynamically updates. It’s invaluable for pinpointing problematic code, and when configured correctly, it can open files in your favourite IDE for quick fixes.

One minor gripe is that it opens the file in Rider but doesn’t load the entire solution.

Further Results

I was able to dig into more of the issues with my codebase with ease. things from cyclical dependencies to too many levels of inheritance. This all adds up and helps give you a clearer view of the code.

They’ve even provided the ability to bring your own rules into the fold, if you wanted to.

So far, I’ve only scratched the surface of NDepend. It was incredibly easy to get started—setup took a few minutes, and I was able to run a one-off analysis on my repository.

Within minutes, I gained invaluable insights into our codebase, including:

  • Tech Debt: A quick overview of accumulated debt, its interest, and hotspots.
  • Method Complexity: A clear and concise view of method complexity, including maximum and average values.

I haven’t even explored features like the Dependency Graph and Matrix, which I’ll likely cover in a follow-up post.

There is a lot to take in, but it’s clear what the end-goal is after you’ve fired up analysis on your code: Start improving it. Whether you take on the smallest of code smells or larger architectural issues, consistent analysis will give you a baseline and trend analysis to see how the code has improved over time.

Once-off, it allows you to, at a glance, determine what needs to be improved on or even enable you to write up a full system analysis. A powerful tool, not to be underestimated.

More useful features

Visualising Dependencies: Graph and Matrix

Interactive diagrams that map out how your code’s components relate. Can be tailored to show dependencies between assemblies, namespaces, classes, or even methods.

Code Diff and Trend Analysis

Beyond the one-off analysis I’ve mentioned above, NDepend supports comparing analyses over time. Code Diff lets you compare your codebase and two different points in time, highlighting what’s chaned: which rules violations are new or fixed, how metrics shifted, and even differences in code structure.

Hand-in-hand with diffs, Trend Charts provide a historical perspective on various code quality metrics.

Integration with IDEs and CI/CD Workflows

To make all these features practical, NDepend integrates with the tools developers use daily. First-class support is provided for Microsoft Visual Studio: NDepend comes with a VS extension (for VS 2010 up to 2022) that embeds its UI and menus into the IDE.

NDepend unfortunately does not yet have a dedicated Rider plugin, however, the NDepend team has expressed plans to support Rider and VSCode in future (NDepend.com).

On the Continuous Integration front, NDepend is designed to fit into build pipelines. It provides a console runner (NDepend.Console.exe) that can be invoked in your CI scripts to perform analysis on a build server. In fact, there are specialized licenses for Build Machines, as well as official extensions/integrations for popular CI/CD platforms.

NDepend vs SonarQube vs ReSharper: Key Differences

NDepend often draws comparisons with SonarQube and ReSharper, since all three are popular tools for improving code quality. However, they serve different needs and can even complement each other in practice. Here’s a practical look at how they differ in use cases, strengths, and limitations:

Scope and Focus: SonarQube supports many languages and focuses on continuous quality monitoring across teams and pipelines. NDepend is .NET-specific, diving deep into code structure, complexity, and architecture. ReSharper lives inside the IDE, helping developers refactor and fix issues as they type. Think of SonarQube as a manager’s quality tracker, ReSharper as a developer’s daily assistant, and NDepend as an architect’s deep-dive tool.

Integration and Workflow: SonarQube typically runs in CI/CD, offering dashboards and enforcing quality gates post-commit. ReSharper works in real-time inside Visual Studio or Rider, offering immediate suggestions while coding. NDepend bridges both worlds – it integrates with CI but also runs interactively on a developer’s machine with dashboards, graphs, and custom queries.

Strengths: NDepend excels in architectural analysis, custom rules, and visualising complexity. SonarQube is strong in enterprise-wide visibility, multi-language support, and DevOps pipelines. ReSharper is ideal for live code improvements and productivity within the IDE. NDepend shows you the why behind complex code issues, SonarQube shows what needs fixing, and ReSharper helps you fix it fast.

Limitations: NDepend is premium-priced and only supports .NET. It also has a learning curve and isn’t integrated with Rider yet. SonarQube requires a server (unless you use SonarCloud) and isn’t as customisable for .NET-specific metrics. ReSharper can slow down Visual Studio in large solutions and lacks holistic or historical insights into a codebase.

How They Work Together: Many teams use all three. ReSharper for everyday coding, SonarQube to guard the main branch in CI, and NDepend for deep audits and trend tracking. They complement each other well, with NDepend even importing results from other tools to give a unified view.

Summary

NDepend stands out for its deep architectural analysis, actionable insights, and ability to visualize and track code quality trends. While it has a learning curve and a premium price, the value it brings to .NET projects is significant, especially for teams serious about code health.

Conclusion

I can see NDepend becoming an invaluable tool for any .NET engineer. The amount of information it provides is incredible, helping to improve codebases and maintain cleanliness from the outset. It’s clearly a tool developed by software engineers, for software engineers.

However, I can’t end this review without mentioning the price: $492 for a developer seat for one year (with discounts applied if you subscribe for more than a year). That’s a not-insignificant cost, especially if you’re footing the bill yourself. Even SonarQube has a free community edition and if NDepend at least offered a one-time purchase for the current version with indefinite usage, it might be more appealing (though still pricey).

They do offer a 14-day free trial if you want to try before you buy, and I’m certain that you’ll find it’s more than worth it’s price, given the value gained from it’s analysis.

In short, it’s a fantastic, no-nonsense development tool that can improve both new and existing codebases. I’m always a proponent for working with as much data on-hand as possible and NDepend provides more data than I know what to do with.