NDepend's 2020 release has a new Dependency Graph view. I am keen to try it out, and I just have the prefect project for the test. In this post I will use the Dependency Graph view on one of my most 'messy' projects. The project is started as a Proof Of Concept (POC), then I based 3 other Proof Of Concept apps on it.
This made all namespaces, dependencies and responsibilities mixed. The Visual Studio solution consists of three projects. An ASP.NET Core project of the chatbot implementation, an Azure Functions project for managing IoT devices and a shared library project common types.
NDepend has a VS Extension, which streamlines work a lot, but at the same time it also has standalone executable, which does not require installation. I like these apps, because I can just drop them to my \tools folder and use them as needed. Once I notice that I use an app extensively for a period of time, I can install the VS Extension.
Dependency Graph First View
After building my projects and importing the solution to NDepend, we can choose whether we want to investigate the App Metrics / Quality Gates or one of the Diagrams. In this case I choose Dependency Graph for further investigation. The initial image looks orderly, but do not take it as a good sign. NDepend does its best to group things together in a sensible way, so one can reason about.
It also worth spending time on understanding color coding of the arrows. As it can been on the above image, there are multiple arrows, colored differently while hover over of code elements.
Fortunately, there is a help sidebar with details. I really like direct/indirect calls highlighted differently, that it nice touch. My only concern is having the selected element colored to dark red, makes me think that I have many issues with it.
Let's dive into some of the details, to find things to fix. Keep in mind, that the code is still 'designed' to be layered, but because of multiple rounds of evolution, namespaces are mixed up.
Refactoring the solution
Zooming into to the IotProcessor namespace I see there are a mixture of types:
The icon in the top left corner of each node can help to understand if we are looking at an assembly, a namespace or a type. Here, I see a mixture of data types, functions, extensions and service types in one namespace. By hovering over the types, I see my attempt to have a clear layered structure. Typically functions depend on data types and service types, while service types on data types. The first step cleaning up namespaces. After the cleanup, the whole assembly takes a different shape:
Refactoring took me a couple of iterations: moving classes, updating namespaces, moving files to different folders, then carefully observing changes on the Dependency Graph view. The above result shows a clear layered architecture. Functions, services and data types are all separated. Bedsides this, I have a startup class to register some dependencies to IoC container and extensions to enable work with data types in a more convenient way. Note, that these data types are only used within the IotProcessor namespace, they are not shared between the chatbot and the function projects. The nice thing about NDepend is that I had no additional effort to manually move code elements around to look good, the default renderer seems to render items in a left-to-right fashion making the graph easier to understand.
Another thing that shows straight up: mutually dependent namespaces are marked with light red arrows. Hovering over the arrow shows a help in a tooltip explaining the issue.
By double clicking the red arrow we may dive into a coupling graph, that helps to understand which A uses B, and B uses A. I only realized this feature exists later, and it seems very helpful to pin-point the actual issue:
In this case it is clearly visible that the
CoreBots.Bots.DialogBot class is dependent on
CoreBot.Service.MessageRepository, using a data type called
NoiseLevelEx within a sub-namespace of
CoreBot.Bots. Carefully thinking about this, the data type is not specific to Bots, so to resolve the issue, I moved it out from the sub-namespace into a new, shared namespace.
After diving into the CoreBot namespace and having a couple of iterations, I ended up refactoring the assembly and removing the mutual dependency:
Finally, looking at the shared library, and just selecting types to see their dependents, I noticed, that one type is only used by one of the assemblies. This can be instantaneously spotted by choose looking at the arrow colors. Here we have a green arrow from the CoreBot, while the arrow from IotProcessor to IoTProcessorCore is gray meaning that there direct caller for that given type. Why do I have a type in the shared assembley not being used by both assemblies?
Right clicking the type and selecting Open Source Declaration takes me to the source in Visual Studio. Looking at this file, I soon realized it is part of a refactoring that I have not completed earlier, during iterating my POC solutions.
After the refactoring, I feel much happier with my types, dependencies and code structure. It is still not perfect, but this visualization helped me to clear up some of the technical depth that the project has accumulated throughout its iterations. Dependency Graph is an excellent visualization tool, to explore types and their relation to other types very quickly. Without such a visualization, it would have taken significantly longer to explore these dependencies by reading the code and creating a mental graph in my head or spending hours creating one by myself.
Disclaimer: NDepend supported the necessary license keys for the product, without that I would not have been able to create this post.