Supporting code for this article.
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 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. 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:
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:
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:
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. 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:
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:
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:
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 for it to see all the functionality it provides. Here is an example of finding usages of an interface
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:
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:
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:
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. 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 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 to get some ideas (analyses itself).
Top comments (0)