Roslyn allows us to access our code through code. That permits us to analyse and work with code in a way that was impossible before. In this post we look at the basic parts of working with a Visual Studio solution to figure out what parts make up our application.
The syntax of Roslyn has not changed much in the 5 years since my first experiments. However, the same is not true for the dependencies and the things that work behind the API. Here we need a new combination of Roslyn and its dependencies to explore our solutions through code.
Preparations for .Net 7
Make sure that your Roslyn Project is on .Net 7 and add these 3 packages using the Package Manager console:
1 2 3 |
Install-Package Microsoft.Build.Locator -Version 1.5.5 Install-Package Microsoft.CodeAnalysis.CSharp.Workspaces -Version 4.4.0 Install-Package Microsoft.CodeAnalysis.Workspaces.MSBuild -Version 4.4.0 |
Those versions work together, but they will not work in .Net 6 or any other older version.
Read your solution file
The simplest starting point is to load the solution into a workspace. That allows us to iterate through our projects and list them on the console:
1 2 3 4 5 6 7 8 9 10 11 12 |
string solutionPath = @"..\..\..\..\..\..\NUnit\NunitUpgrade\NunitUpgrade.sln"; // Register MSBuild MSBuildLocator.RegisterDefaults(); var workspace = MSBuildWorkspace.Create(); var solution = await workspace.OpenSolutionAsync(solutionPath); foreach (var project in solution.Projects) { Console.WriteLine($"Project {project.Name} [{project.Id.Id}]\n"); } |
For my NUnit solution the code will produce this output:
Project MyBizLogic [0967cfce-a386-41ac-965f-aefc512ecf37]
Project TestsForNUnit2 [e1a15f36-4f86-44ac-910e-705ef47bb1bd]
Project TestsForNUnit3 [37fec393-c860-4c20-9aa8-f9490aaff268]
Find the project dependencies
We can iterate over our projects in the solution and explore the projects it depends on through the property AllProjectReferences:
1 2 3 4 5 6 7 8 9 |
foreach (var project in solution.Projects) { Console.WriteLine($"Project '{project}' depends on:"); foreach (var projectReference in project.AllProjectReferences) { Console.WriteLine($"\t- {projectReference.ProjectId.Id}"); } } |
That gives us this output:
Project ‘Microsoft.CodeAnalysis.Project’ depends on:
Project ‘Microsoft.CodeAnalysis.Project’ depends on:
– 0967cfce-a386-41ac-965f-aefc512ecf37Project ‘Microsoft.CodeAnalysis.Project’ depends on:
– 0967cfce-a386-41ac-965f-aefc512ecf37
The GUID to reference the projects is great to compile code, but not so much to understand what is going on. A simple fix for a human-readable name is to iterate twice through your project list, the first time to create a dictionary that maps the GUID to the project name, and then we iterate through the projects to show their dependencies:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var names = new Dictionary<Guid, string>(); foreach (var project in solution.Projects) { names[project.Id.Id] = project.Name; } foreach (var project in solution.Projects) { Console.WriteLine($"Project '{project}' depends on:"); foreach (var reference in project.AllProjectReferences) { Console.WriteLine($"\t- {names[reference.ProjectId.Id]}"); } } |
The output shows us that both our test projects reference the MyBizLogic project – this time in an understandable manner:
Project ‘Microsoft.CodeAnalysis.Project’ depends on:
Project ‘Microsoft.CodeAnalysis.Project’ depends on:
– MyBizLogicProject ‘Microsoft.CodeAnalysis.Project’ depends on:
– MyBizLogic
Unfortunately, there is no way to access the referenced packages from Roslyn.
Helper methods for classes and interfaces
We can access interfaces and classes through the same methods. Therefore, I created these 3 helper methods to write the code only once:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
private static async Task<SyntaxNode> GetRootNode(Document document) { var root = await document.GetSyntaxTreeAsync() .Result?.GetRootAsync()!; return root; } private static List<T> FilterNodes<T>(SyntaxNode root) { List<T> interfaces = root.DescendantNodes().OfType<T>().ToList(); return interfaces; } private static void ShowMethods(SyntaxNode root) { var nodes = root.DescendantNodes() .OfType<MethodDeclarationSyntax>().ToList(); if (nodes.Count > 0) { foreach (var method in nodes) { Console.WriteLine($" * {method.Identifier}()"); } } } |
List the interfaces
With the helper methods in place, we can filter the nodes for the type InterfaceDeclarationSyntax and iterate through the results:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
foreach (var project in solution.Projects) { foreach (var document in project.Documents) { var root = await GetRootNode(document); var interfaces = FilterNodes<InterfaceDeclarationSyntax>(root); if (interfaces.Count > 0) { Console.WriteLine($"Interfaces in {project.Name}:"); foreach (var unit in interfaces) { Console.WriteLine( $" - [{unit.Identifier.Text} - {unit.Keyword}]"); ShowMethods(root); } } } } |
When we run the code, we get the interface and its methods:
Interfaces in MyBizLogic:
– [IDebtCalculator – interface]
* BatchProcessing()
* ByMethod()
List the classes
To get the classes in a project, we filter for ClassDeclarationSyntax and iterate through the result:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
foreach (var project in solution.Projects) { Console.WriteLine($"\nClasses in {project.Name}:"); foreach (var document in project.Documents) { var root = await GetRootNode(document); var classes = FilterNodes<ClassDeclarationSyntax>(root); if (classes.Count > 0) { foreach (var unit in classes) { Console.WriteLine( $" - [{unit.Identifier.Text} - {unit.Keyword}]"); ShowMethods(root); } } } } |
This shows us all classes and their methods in our solution:
Classes in MyBizLogic:
– [DebtCalculator – class]
* ByMethod()
* BatchProcessing()
– [MyException – class]Classes in TestsForNUnit2:
– [DebtCalculatorTestOld – class]
* SetUp()
* Calculation_can_only_be_called_with_valid_CalculatorMethods()
* Extended_calculation_isnt_allowed()
* Data_can_be_loaded_from_file_system()
* Something()Classes in TestsForNUnit3:
– [DebtCalculatorTestNew – class]
* SetUp()
* Calculation_can_only_be_called_with_valid_CalculatorMethods()
* Extended_calculation_isnt_allowed()
* Extended_calculation_isnt_allowed_CheckInstanceOf()
* Data_can_be_loaded_from_file_system()
* Something()
List the enumerations
To get the definitions for enumerations in a project, we need to filter for EnumDeclarationSyntax and use the Members property to get the named constants:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
foreach (var project in solution.Projects) { foreach (var document in project.Documents) { var root = await GetRootNode(document); var enums = FilterNodes<EnumDeclarationSyntax>(root); if (enums.Count > 0) { Console.WriteLine($"Enums in {project.Name}:"); foreach (var unit in enums) { Console.WriteLine( $" - [{unit.Identifier.Text} - {unit.EnumKeyword}]"); foreach (var member in unit.Members) { Console.WriteLine($" * {member}"); } } } } } |
This lists us the enumerations, its defined constants and their corresponding int value:
Enums in MyBizLogic:
– [CalculatorMethod – enum]
* Basic = 0
* Extended = 1
Next
Roslyn allows us to work with our code base through code. We can access classes, interfaces and projects without writing much code. From the basic examples in this post, you can build on and dive a lot deeper into the other packages that come with Roslyn.
Next week we use Roslyn to create a dependency graph of our projects and write a bit of Python code to analyse it.
1 thought on “Access your C# Projects Through Code With Roslyn”