Currently if I want to check for circular references inside a solution I select Architecture - Generate Dependency Graph - For Solution
.
Then from the new tab that opens I select Layout - Analyzers - Circular References Analyzer
.
Finally if I drill down from the individual assemblies and there are circular references I can see them highlighted in red on the graph and they also appear as warnings in the Error List.
Since I intend to spot circular references even between methods of the same class this is quite error prone and time consuming on a moderately large codebase.
I'd like to know if there is a way to get all the warnings at once without having to expand the nodes or maybe to turn on highlighting for parent nodes so that I can drill down only on assembles that surely contain circular references.
NDepend should be able to help but I prefer to keep things as simple as possible so I'm always wary about adopting additional tools.
Yes NDepend can Find Circular References Efficiently let me explain how because it might be easier than you might think (Disclaimer: I am one of the developers on NDepend). So far you can find namespace dependency cycle out-of-the-box, but, as I am explaining below, it is easy to find as well cycles between types in a namespace, or methods of a type.
There is a default C# LINQ code rule that lists namespaces dependency cycles. Such cycle can then be exported to the dependency graph, or dependency matrix. Here is a screenshot of the rule executed on the Roslyn code base CTP June 2012 (notice it took only 16 milliseconds to run). It found 11 distinct cycles, and as shown on the screenshot, you can drill down into each cycle and export a cycle to the graph:
Here is a dependency graph of the 7 namespaces cycle. Notice that it looks more complicated than just a classic O-ring cycle. The key here, is that from any of these namespaces, you can reach all the other ones. This is the generalized notion of cycle (entangling).
The code of the default C# LINQ code rule that lists namespaces dependency cycles might look daunting at first glance. But a C# developer should understand it in a few minutes and can then adapt it easily to find any kind of dependency cycle.
For example, to find methods of same types cycles (instead of namespaces of same assembly cycles) it is almost as simple as replacing all namespace word by method, and assembly word by type.
// <Name>Avoid methods of a type to be in cycles</Name>
warnif count > 0
from t in Application.Types
.Where(t => t.ContainsMethodDependencyCycle != null &&
t.ContainsMethodDependencyCycle.Value)
// Optimization: restreint methods set
// A method involved in a cycle necessarily have a null Level.
let methodsSuspect = t.Methods.Where(m => m.Level == null)
// hashset is used to avoid iterating again on methods already caught in a cycle.
let hashset = new HashSet<IMethod>()
from suspect in methodsSuspect
// By commenting this line, the query matches all methods involved in a cycle.
where !hashset.Contains(suspect)
// Define 2 code metrics
// - Methods depth of is using indirectly the suspect method.
// - Methods depth of is used by the suspect method indirectly.
// Note: for direct usage the depth is equal to 1.
let methodsUserDepth = methodsSuspect.DepthOfIsUsing(suspect)
let methodsUsedDepth = methodsSuspect.DepthOfIsUsedBy(suspect)
// Select methods that are both using and used by methodSuspect
let usersAndUsed = from n in methodsSuspect where
methodsUserDepth[n] > 0 &&
methodsUsedDepth[n] > 0
select n
where usersAndUsed.Count() > 0
// Here we've found method(s) both using and used by the suspect method.
// A cycle involving the suspect method is found!
// 8Feb2021: invoke extension method Append() explicitly to avoid ambiguous compiler error
// because of the new .NET BCL extension methods Append() method
let cycle = NDepend.Helpers.ExtensionMethodsEnumerable.Append(usersAndUsed,suspect)
// Fill hashset with methods in the cycle.
// .ToArray() is needed to force the iterating process.
let unused1 = (from n in cycle let unused2 = hashset.Add(n) select n).ToArray()
select new { suspect, cycle }
...and here is how the result of this rule looks like (still with the possibility to export the method cycle to the dependency graph or matrix). Notice that since the number of methods and types is much higher than the number of namespaces and assemblies, this query took like 10 seconds to be run on a large code base like Roslyn (instead of 16ms for namespaces cycle) so you might need to adjust the CQLinq query execution time-out (which is 2 seconds per default).
To be complete, what I noticed is that cycle are most of the time provoked by a few bi-directional references (i.e A is using B, B is using A). Hence removing bi-directional references is the first thing to do to break cycle. This is why we provided the default CQLinq rule Avoid namespaces mutually dependent, that can still be adapted to types or methods cycles.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With