Let’s imagine the monolith project with an enormous code base that has been developed for a couple of decades (unbelievable, right?). This project is probably going to have a myriad of features and a considerable (hopefully!) number of automated tests covering the verification of our priceless features on multiple levels. All the way up the famous, or infamous, depending on who’re you gonna ask, testing pyramid from the unit foundation down below to the end-to-end peak high above.
Now, after some exercises with imagining technical stuff, you should be good with an idea that such a complex piece of software should stand in need of pretty solid test management tools to facilitate the development life cycle and make continuous integration a smooth and sleek process.
As a number of product versions, development branches, test environments, and all the other imaginable things grow, so is a number of attributes that are used to tag test classes and methods to properly categorize them, and define the tests that should go together. Having said that, what should you do when there’s a need to add another attribute and mark a bunch of the selected tests across the solution? When a bunch is just two tests, that’s pretty easy, right? But what if a bunch is a two thousand size bunch?
That’s where Roslyn comes to the rescue!
What Is It All About?
Chances are you’ve already heard about Roslyn and might even work with it. But, for people out there who are not familiar with this piece of the .NET ecosystem, Roslyn is a .NET Compiler Platform that provides a set of open-source compilers and APIs for code analysis and refactoring.
In this article I will show you how to utilize one of the prominent Roslyn features to automate some of the routine tasks from our refactoring or test automation agenda:
- Add custom attribute with string argument for test methods with specific names.
- Add custom attribute with enumeration argument for test classes from specific projects.
- Add using directives for changed files.
- Properly format added elements.
We’re gonna use CSharpSyntaxRewriter — a base class that implements Visitor pattern and allows to override a multitude of Visit methods for any type of the source code elements and blocks with our own implementation. We are particularly interested in VisitMethodDeclaration and VisitClassDeclaration methods that are going to help us to achieve the above goals.
Create Attribute with String Argument
Let’s start with creating an attribute that is going to accept a string argument. In our case, we would want to mark specific tests in our test projects with a “Top Priority” category attribute.
Create Attribute with Enum Argument
But what if we don’t want to use the magic strings in our solution but rather have a dedicated enumeration type. So, instead of this,
we would rather have this?
Let’s create another attribute with an enumeration argument.
Add Attribute with Proper Formatting
Okay, we’ve created attribute definitions but now we need to actually insert our attributes next to our method or class names. Also, it would be nice to have proper formatting. And by proper formatting, I mean correct position of the inserted attribute: the same level of indentation as for other attributes of a class or a method, the class/method signature starting on the next line after a list of attributes, etc.
It’s pretty easy to do it right if we already have proper formatting for existing members. We just need to copy the leading and trailing trivia of existing members and re-use it for our attribute.
Our custom AddAttribute method is going to receive a MemberDeclarationSyntax parameter. We’re going to provide it in the overridden VisitMethodDeclaration and VisitClassDeclaration methods as both Method and Class nodes inherit this base class. The trivia stuff will take care of the formatting.
Both visitor methods are going to have just the same basic logic for now. We’re gonna get the node and add our attribute to the attribute list of either a method node or a class node.
Add Using Directive
To avoid the manual update of the modified files that got new attributes, we would like Roslyn to deal with “using” directives too. The following code snippet should take care of that.
We’re gonna add our namespace only if it’s not already present in the list of using directives. Note another two pieces of the formatting magic above.
Call to NormalizeWhitespace method is necessary to avoid concatenated using string: “usingOurNamespace” without a space between the directive and the namespace. Don’t ask me why it works like this!
Using the trailing trivia with ElasticCarriageReturnLineFeed parameter is the most simple way to insert an empty line between a block of using directives and a namespace definition.
Now, let’s get everything we learned above together and create a small program that we can utilize for our refactoring purposes.
BaseRewriter class is going to inherit the CSharpSyntaxRewriter and contain a common logic used by both MethodRewriter and ClassRewriter guys.
We’re gonna use several code analysis APIs in our ModifySolutionAsync method to traverse the solution documents and modify them according to our needs. The logic should be pretty straightforward. We’re gonna go through the list of selected projects calling the Visit method on each appropriate file and then replace the old syntax tree with the new one containing our custom attribute nodes. We also are going to track the document state using the IsModified flag and insert the using directives only for modified files.
MethodRewriter and ClassRewriter will have their own CreateAttribute logic and a different list of projects or methods to go through. Also, we’re gonna change the IsModified flag’s value in the overridden Visit methods here.
Finally, the Program.cs is going to have a program entry point to initialize our rewriters and call the ModifySolutionAsync method. The final tip is to call MSBuildLocator.RegisterDefaults() to properly register the correct MSBuild path for the installed Visual Studio version. Odds are it can be corrupted in some cases that will prevent Roslyn to discover any documents in the provided solution.
I wanted to demonstrate how Roslyn can be used to automate some routine refactoring or test automation tasks that can pop-up on your agenda any time if you work on a mature project with a significant codebase. The scenarios covered in this article are just a tip of the iceberg as Roslyn is able to deal with much more difficult and elaborate cases.
Don’t hesitate to play with it — it may save you a lot of time and effort.
That’s all folks!