Analysing a .NET Codebase with Roslyn
Tags: dotnet, roslyn, codeanalysis, csharp
Published: 2021-06-13 13:08:53

[Supporting code for this article.](https://github.com/mattjhosking/roslyn-analysis)

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7mo9v22kz4qrh6r1ws9l.png)
# But Why Would I Want To Do This?
When you're dealing with a complex system, particularly one with a lot of interconnections (such as a microservice architecture), it can be very useful to extract and even visualise metadata about those connections. You may also want to assess the usage of a particular NuGet package, or where that critical helper class is actually used. You can certainly do all of this with Visual Studio itself, or even more effectively with ReSharper, but what if you then want to drill down further or extract the data about the usages to document or visualise some how? This is where using the Roslyn compiler can be very handy.

# What Is Roslyn?
> [Roslyn](https://github.com/dotnet/roslyn) is the open-source implementation of both the C# and Visual Basic compilers with an API surface for building code analysis tools.

To give you an idea of how it works and what it's capable of, have a look at the [.NET Compiler Platform SDK model](https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/compiler-api-model). Essentially, you can use .NET to analyse .NET code, representing all the metadata about types, usages and structure as objects. You can load a single file, a project or even a whole solution. This is all provided through a series of NuGet packages, some that leverage the MSBuild libraries.

# Getting Started
The key NuGet package you'll need is `Microsoft.CodeAnalysis`. You can use `Microsoft.CodeAnalysis.CSharp.Workspaces` if you don't need Visual Basic support (does anyone really these days?) This will allow you to do everything with C# (.cs) files, but you'll likely also want to load C# projects (.csproj) and solution (.sln) files. For this you'll need `Microsoft.CodeAnalysis.Workspaces.MSBuild`. This has a dependency on `Microsoft.Build`, so also add a direct reference on this so it uses the latest (it defaults to 15.3 at time of writing, where 16.10 is latest). Don't worry about the `restored using .NETFramework` warning - it uses the binaries at runtime separately. Finally, you'll need `Microsoft.Build.Locator`, so your existing Visual Studio build tools can be used (or you'll promptly get a cryptic error at runtime).

# Loading a Solution
The very first thing you'll need in your code looks like this:
```csharp
if (!MSBuildLocator.IsRegistered)
{
  var instances = MSBuildLocator.QueryVisualStudioInstances().ToArray();
  MSBuildLocator.RegisterInstance(instances.OrderByDescending(x => x.Version).First());
}
```
This will find all .NET Core SDKs (including .NET 5+) and use the latest version. Feel free to change the logic for which version to use to suit your needs (if you don't want the latest version installed). Next you need to create an `MSBuildWorkspace` to load your solution into:
```csharp
var workspace = MSBuildWorkspace.Create();
workspace.SkipUnrecognizedProjects = true;
workspace.WorkspaceFailed += (sender, args) =>
{
  if (args.Diagnostic.Kind == WorkspaceDiagnosticKind.Failure)
  {
    Console.Error.WriteLine(args.Diagnostic.Message);
  }
};
```
I use `SkipUrecognizedProjects` for those cases where you are using non C# projects mixed in (like anything kind of database or deployment project). The `WorkspaceFailed` handler is to log any issues that occur, as it will silently fail with no loaded documents or projects, depending on the error, without this. Now you can load the solution:
```csharp
var solution = await workspace.OpenSolutionAsync(@"C:\Dev\MyDevThing\MySolution.sln", new ProgressBarProjectLoadStatus());

public class ProgressBarProjectLoadStatus : IProgress<ProjectLoadProgress>
{
  public void Report(ProjectLoadProgress value)
  {
    Console.Out.WriteLine($"{value.Operation} {value.FilePath}");
  }
}
```
The `OpenSolutionAsync` requires the file name, but also a class that implements IProgress<ProjectLoadProgress>. I've provided a simple console output implementation here.

# Working With a Project
It's pretty easy to get the project you want from the solution with something like:
```csharp
var project = solution.Projects.SingleOrDefault(x => string.Equals(x.Name, projectName, StringComparison.OrdinalIgnoreCase))
```
But what next? First you'll want to get a compilation context, as this is required for a lot of actions you may want to perform:
```csharp
var compilation = await project.GetCompilationAsync();
```
Once you have this, retrieving any type that would be visible in the context of this project is as simple as:
```csharp
var typeSymbol = compilation.GetTypeByMetadataName(typeName);
```

# Find Usages of a Type
For this kind of scenario, the static class, `SymbolFinder`, is provided. Have a look at the [documentation](https://docs.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.findsymbols.symbolfinder) for it to see all the functionality it provides. Here is an example of finding usages of an interface
```csharp
var typeSymbol = compilation.GetTypeByMetadataName("MyNamespace.IMyInteface");
await SymbolFinder.FindImplementationsAsync(typeSymbol, solution)
```

# Working With Files
This needs to be the full name of the type (although you won't usually need Assembly information) and for generics, you'll need to use the ``MyType`1`` syntax (`` `1 `` indicates one type parameter, `` `2 `` is two, etc.). To load a file is easy as well:
```csharp
var document = _project.Documents.Single(x => x.Name == fileName);
var syntaxRoot = await document.GetSyntaxRootAsync();
var semanticModel = await document.GetSemanticModelAsync();
```
The first line speaks for itself, and the syntax root and semantic model are used for analyzing the document syntax and symbols, respectively. For example, to find a method in a file, then find all usages of that method in your solution:
```csharp
var methodDeclaration = syntaxRoot.DescendantNodes().OfType<MethodDeclarationSyntax>().Single(x => x.Identifier.Text == methodName);
var semanticModel = compilation.GetSemanticModel(methodDeclaration .SyntaxTree);
var methodSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration);
var callers = await SymbolFinder.FindCallersAsync(methodSymbol, solution);
```
Or the same for a property in that file:
```csharp
var propertyDeclaration = syntaxRoot.DescendantNodes().OfType<PropertyDeclarationSyntax>().Single(x => x.Identifier.Text == propertyName);
var semanticModel = compilation.GetSemanticModel(propertyDeclaration.SyntaxTree);
var methodSymbol = semanticModel.GetDeclaredSymbol(propertyDeclaration);
var callers = await SymbolFinder.FindCallersAsync(propertySymbol, solution);
```
I find a great way to discover the class I'm after, is either to set a breakpoint and dig through the syntax root in the debugger, or explore the [C# syntax namespace](https://docs.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax). From your found syntax instance, you then need the semantic model to find the symbol that relates to it. For this you can use `GetDeclaredSymbol` or `GetSymbolInfo` (see [here](https://stackoverflow.com/questions/33492992/when-to-use-semanticmodel-getsymbolinfo-and-when-semanticmodel-getdeclaredsymbol) for a good explanation on when to use which). This symbol is then something which has global meaning across your solution (as opposed to a syntax, which is just code in a file), and can therefore be used with the `SymbolFinder` class.

# Where to Next?
There's plenty you can do with the calls you find with `SymbolFinder` and a lot more analysis you could perform. I highly recommend you take a look at the [supporting source code](https://github.com/mattjhosking/roslyn-analysis) to get some ideas (analyses itself).