The .NET platform exists for two decades and nowadays the technology is evolving faster than ever. It is now time for serious .NET applications to be refactored to run over the brand new .NET stack based on .NET 5.0, and later this year .NET 6.0 LTS (Long Time Support Version).
NDepend is quite a large .NET applications with both tons of logics and tons of UI code. Since the inception we’ve been carefully using the guidelines provided by the NDepend default rules set. As a consequence our code is well layered, well tested and maintaining it is a joy.
Nevertheless one thing we couldn’t anticipate 15 years ago was that one day .NET would officially become a multi-platform technology. As a consequence we have plenty of Winforms and WPF specific code spawned all over our code. Fortunately both Winforms and WPF are part of .NET 5.0, but only on Windows. At this point we’d like most of our non-UI code to be platform independent, compiled against .NET Standard 2.0. This way in a first phase we will run the NDepend analysis within Linux boxes. In a second phase the whole UI will be ported to a multi-OS technology not yet determined (MAUI? Blazor?…)
It is now time to separate properly UI code from non-UI code. Since the code is well layered there is no dreaded big ball of muds to demystify. It will be just a matter of moving all UI code within a dedicated UI assembly. Some interfaces will be introduced to inject UI code into non-UI cod logic when needed.
We started planning this large-scale-reactoring and quickly figured out that NDepend was the perfect tool for that. Here is how we are planning our large-scale refactoring.
A Code Query to list all UI code
The first thing we did was to to write a code query to list all methods that depend on some UI code. Here is the query we came up with. Note that it can be easily refined to your large-scale refactoring situation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// <Name>Methods using UI</Name> let mUI = ThirdParty.Assemblies.WithNameWildcardMatchIn( "DevExpress.*", "System.Windows.*", "System.Drawi*", "WindowBase*", "Presentation*", "Microsoft.Msagl.GraphViewerGdi.*").ChildMethods().ToHashSetEx() from m in Application.Methods.UsingAny(mUI) select new { m, m.NbLinesOfCode, locJustMyCode = JustMyCode.Contains(m) ? m.NbLinesOfCode : null, mUIUsed = mUI.Intersect(m.MethodsCalled), } |
3.689 methods are matched for a total of 55K lines of code. The query is refined to also count lines of code not generated by a tool, like a Winforms designer for example. This refinement is handled by the line locJustMyCode = JustMyCode.Contains(m) ? m.NbLinesOfCode : null
. We actually have 32K lines of UI code in non-generated methods. In other words 41% of UI code is generated which is not surprising!
The query is also refined to provide the details of which UI methods is consumed by each method matched:
Visual Estimation of the Refactoring Impact
This large and detailed set of 3.689 methods matched is certainly a good start. But most of our work will consist in relocating matched elements to isolate UI code from non-UI code. The thousands of rows in the query result cannot provide us with a good sense of where is actually declared the UI code.
Fortunately the code metrics view highlight the results of the currently edited code query. On this view each rectangle is a method. The size of each rectangle/method is proportional to the number of lines of code. The color of each rectangle/method indicates the ratio of code coverage by automatic tests. This view immediately makes us realize that:
- With no surprise the project NDepend.UI consists of mostly UI code. However there are still a bit of non-UI code that we could move to another non-UI project.
- A good portion of UI code is green. It means that it is well covered by tests. One essential goal of our large-scale refactoring is to avoid changing any logic, only the code structure will be impacted. Our tests will be of a great help to make sure that we don’t introduce any regression.
- The project NDepend.Core contains some UI code too. However rectangles highlighted are pretty much grouped into larger rectangles. This means that UI code declared in NDepend.Core is quite cohesive. Large lumps of core UI code will be moved up to NDepend.UI.
- This view also shows a few isolated highlighted rectangle in NDepend.Core that represent isolated UI methods. Each of these methods will represent a mini-decision to take to figure out how UI usage can be injected in the surrounding non-UI component.
Identifying where code injection will be needed
The NDepend.UI project is referencing the NDepend.Core project. It means that NDepend.Core UI code will be moved at a higher level within NDepend.UI. Thus we also need to estimate which NDepend.Core non-UI code is using NDepend.Core UI code. Here also this code element’s can be listed with a code query:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// <Name>NDepend.Core methods using UI methods</Name> let mUI = ThirdParty.Assemblies.WithNameWildcardMatchIn( "DevExpress*", "System.Windows.*", "System.Drawi*", "WindowBase*", "Presentation*", "Microsoft.Msagl.GraphViewerGdi*").ChildMethods().ToHashSetEx() // All NDepend.Core methods let coreMethods = Assemblies.WithName("NDepend.Core").Single().ChildMethods // NDepend.Core UI methods let coreUI = coreMethods.UsingAny(mUI) // Non-UI NDepend.Core methods using NDepend.Core UI methods from m in coreMethods.Except(coreUI).UsingAny(coreUI) select new { m, uiMethodsCalled = m.MethodsCalled.Intersect(coreUI) } |
Here 434 methods are matched. For each of these methods we’ll have to decide if UI code needs to be injected through an interface (whose implementation is in NDepend.UI) or if it makes sense to move this non-UI code within NDepend.UI (and then again check if its called from within NDepend.Core).
These 434 methods can be highlighted on the metric view. Here also we get a mix of cohesive groups of methods (within the same rectangles) and isolated methods. Certainly the cohesive groups will represent opportunities for dependency injection while isolated ones might be candidate to be moved at the upper NDepend.UI project.
Actual Architecture of Code to Refactor
So far only the code querying and metric view facilities have been used. However the NDepend dependency graph and dependency matrix will help a lot figuring how areas to refactor are actually architectured. For example in the screenshots below, we can see the dependencies between the namespaces containing UI code. Those are obtained by replacing in the first code query from m in Application.Methods.UsingAny(mUI)
by from m in Application.Namespaces.UsingAny(mUI)
. Then these namespaces can be exported to the graph and matrix.
Both views make clear that the code is entirely layered:
- There is no red arrow nor cycles in the graph.
- The matrix is perfectly triangularized.
As explained earlier, this layered characteristic will simplify a lot the large-scale refactoring. There is no big ball of muds to demystify.
Summary
No matter how clean is your code, there are chances that in the future some unexpected requirements will force to refactor a large portion of it.
By enforcing a well layered architecture and a large coverage by tests ratio, the code is maintainable. This means that the code can undergo any large-scale refactoring with no big drama.
Tooling like code querying, treemap view of code, dependency graph and matrix can help assessing and visualizing precisely which parts of the code will be impacted. These tools can also be used to gamify the refactoring process by estimating daily how far we are from the desired result. For that code queries exposed can be refined to only match classes and methods that will need to be moved. Such query can also provide a definition of refactoring done when they no more match any code element.