An XPath query for long methods by Mark Seemann
A filter for Visual Studio code metrics.
I consider it a good idea to limit the size of code blocks. Small methods are easier to troubleshoot and in general fit better in your head. When it comes to vertical size, however, the editors I use don't come with visual indicators like they do for horizontal size.
When you're in the midst of developing a feature, you don't want to be prevented from doing so by a tool that refuses to compile your code if methods get too long. On the other hand, it might be a good idea to regularly run a tool over your code base to identify which methods are getting too long.
With Visual Studio you can calculate code metrics, which include a measure of the number of lines of code for each method. One option produces an XML file, but if you have a large code base, those XML files are big.
The XML files typically look like this:
<?xml version="1.0" encoding="utf-8"?> <CodeMetricsReport Version="1.0"> <Targets> <Target Name="Restaurant.RestApi.csproj"> <Assembly Name="Ploeh.Samples.Restaurants.RestApi, ..."> <Metrics> <Metric Name="MaintainabilityIndex" Value="87" /> <Metric Name="CyclomaticComplexity" Value="537" /> <Metric Name="ClassCoupling" Value="208" /> <Metric Name="DepthOfInheritance" Value="1" /> <Metric Name="SourceLines" Value="3188" /> <Metric Name="ExecutableLines" Value="711" /> </Metrics> <Namespaces> <Namespace Name="Ploeh.Samples.Restaurants.RestApi"> <Metrics> <Metric Name="MaintainabilityIndex" Value="87" /> <Metric Name="CyclomaticComplexity" Value="499" /> <Metric Name="ClassCoupling" Value="204" /> <Metric Name="DepthOfInheritance" Value="1" /> <Metric Name="SourceLines" Value="3100" /> <Metric Name="ExecutableLines" Value="701" /> </Metrics> <Types> <NamedType Name="CalendarController"> <Metrics> <Metric Name="MaintainabilityIndex" Value="73" /> <Metric Name="CyclomaticComplexity" Value="14" /> <Metric Name="ClassCoupling" Value="34" /> <Metric Name="DepthOfInheritance" Value="1" /> <Metric Name="SourceLines" Value="190" /> <Metric Name="ExecutableLines" Value="42" /> </Metrics> <Members> <Method Name="Task<ActionResult> CalendarController.Get(..."> <Metrics> <Metric Name="MaintainabilityIndex" Value="64" /> <Metric Name="CyclomaticComplexity" Value="2" /> <Metric Name="ClassCoupling" Value="12" /> <Metric Name="SourceLines" Value="28" /> <Metric Name="ExecutableLines" Value="7" /> </Metrics> </Method> <!--Much more data goes here--> </Members> </NamedType> </Types> </Namespace> </Namespaces> </Assembly> </Target> </Targets> </CodeMetricsReport>
How can you filter such a file to find only those methods that are too long?
That sounds like a job for XPath. I admit, though, that I use XPath only rarely. While the general idea of the syntax is easy to grasp, it has enough subtle pitfalls that it's not that easy to use, either.
Partly for my own benefit, and partly for anyone else who might need it, here's an XPath query that looks for long methods:
//Members/child::*[Metrics/Metric[@Value > 24 and @Name = "SourceLines"]]
This query looks for methods longer that 24 lines of code. If you don't agree with that threshold you can always change it to another value. You can also change @Name
to look for CyclomaticComplexity
or one of the other metrics.
Given the above XML metrics report, the XPath filter would select (among other members) the CalendarController.Get
method, because it has 28 lines of source code. It turns out, though, that the filter produces some false positives. The method in question is actually fine:
/* This method loads a year's worth of reservations in order to segment * them all. In a realistic system, this could be quite stressful for * both the database and the web server. Some of that concern can be * addressed with an appropriate HTTP cache header and a reverse proxy, * but a better solution would be a CQRS-style architecture where the * calendars get re-rendered as materialised views in a background * process. That's beyond the scope of this example code base, though. */ [ResponseCache(Duration = 60)] [HttpGet("restaurants/{restaurantId}/calendar/{year}")] public async Task<ActionResult> Get(int restaurantId, int year) { var restaurant = await RestaurantDatabase .GetRestaurant(restaurantId).ConfigureAwait(false); if (restaurant is null) return new NotFoundResult(); var period = Period.Year(year); var days = await MakeDays(restaurant, period) .ConfigureAwait(false); return new OkObjectResult( new CalendarDto { Name = restaurant.Name, Year = year, Days = days }); }
That method only has 18 lines of actual source code, from the beginning of the method declaration to the closing bracket. Visual Studio's metrics calculator, however, also counts the attributes and the comments.
In general, I only add comments when I want to communicate something that I can't express as a type or a method name, so in this particular code base, it's not much of an issue. If you consistently adorn every method with doc comments, on the other hand, you may need to perform some pre-processing on the source code before you calculate the metrics.