Introduction
This article originated from the need to find out the methods, fields and properties that an assembly accesses through its references. While Visual Studio shows the list of assemblies that an assembly references, it does not show the actual members being referenced. FindRefs is a simple tool that uses Cecil [^] as the back-end to analyze all methods and properties in an assembly and list the method calls, field and property references made by it on types residing in referenced assemblies.
Background
The motivation to write the tool was to figure out the logical dependencies of an assembly, so that refactoring or redesigning its referenced assemblies would not break it. Just the list of referenced assemblies wasn't obviously enough and I couldn't find any tool, except Reflector, that gave me the list of methods, properties and fields that are actually referenced. However, I couldn't find a way to export the output from Reflector and so I decided to roll my own. Besides, I had just discovered Cecil and I thought this would be a nice opportunity to play around with it.
Using FindRefs
Using FindRefs is very simple. FindRefs is a command line application that takes the assembly to be analyzed as the argument. You could optionally filter the types to be considered (for output) using the -include
and -exclude
flags. The command line syntax is:
findrefs <assembly_path> [-include filterlist] [-exclude filterlist]</assembly_path>
filterlist
is a list of fully qualified type names. It can also include namespaces with an asterisk (*) to indicate all types within the namespace, like we do with the using
C# statement. It also supports using * as a wildcard to match type names. Example usage:
findrefs blah.exe -exclude System.*
findrefs blah.exe -include System.Console -exclude System.*
findrefs blah.exe -include System.Console -exclude *
findrefs blah.exe -include Sys* -exclude *
The output will be written to blah.xml (the assembly name without the extension + .xml) and it gets generated in the current directory. Not specifying -exclude
does not exclude anything and therefore -include
makes sense only when used in conjunction with -exclude
.
Sample Output
For a simple program like:
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello");
}
}
Running findrefs on the compiled assembly generates the following output:
="1.0" ="utf-8"
<References>
<Assembly name="mscorlib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089">
<Type name="System.Console">
<Reference name="System.Void System.Console::WriteLine(System.String)" />
</Type>
<Type name="System.Object">
<Reference name="System.Void System.Object::.ctor()" />
</Type>
</Assembly>
</References>
How It Works
The Cecil library makes it really simple to walk through all methods in an assembly. You load the assembly and get a reference to AssemblyDefinition
using the AssemblyFactory.GetAssembly(assemblyPath)
and then use the AssemblyDefinition
object to recursively walk through modules, types and methods. The following code snippet shows how to walk through all methods in an assembly:
public static void FindReferences(string assemblyPath, TypeFilter typeFilter)
{
AssemblyDefinition assemblyDefinition = AssemblyFactory.GetAssembly(assemblyPath);
foreach (ModuleDefinition moduleDefinition in assemblyDefinition.Modules)
{
Console.WriteLine("Processing module [" + moduleDefinition.Name + "]");
foreach (TypeDefinition typeDefinition in moduleDefinition.Types)
{
ProcessType(typeDefinition, typeFilter);
}
}
}
static void ProcessType(TypeDefinition typeDefinition, TypeFilter typeFilter)
{
Console.WriteLine("Processing type [" + typeDefinition.FullName + "]");
foreach (MethodDefinition methodDefinition in typeDefinition.Methods)
{
ProcessMethod(methodDefinition, typeFilter);
}
foreach (MethodDefinition methodDefintion in typeDefinition.Constructors)
{
ProcessMethod(methodDefintion, typeFilter);
}
foreach (PropertyDefinition propertyDefintion in typeDefinition.Properties)
{
ProcessMethod(propertyDefintion.GetMethod, typeFilter);
ProcessMethod(propertyDefintion.SetMethod, typeFilter);
}
}
ProcessMethod
goes through the IL instructions in the body of the method, looking for an operand of type MethodReference
or FieldReference
. However, MethodDefinition
and FieldDefinition
derive from MethodReference
and FieldReference
and an operand of type XXXDefinition means that the particular method/field resides in this assembly. We are looking for references outside the current assembly, so ProcessMethod
explicitly checks that they are of type XXXReference but not of type XXXDefinition.
The rest of the code is just plumbing to group the results by assembly and type. There is also code that groups the references by the calling method, but that data is not used currently. TypeFilter
is a nifty little class that takes care of the filtering based on include and exclude lists, if provided and it uses simple string matching to do its job.
Conclusion
This was my first brush with the Mono project and I must say I was really impressed by Cecil [^]. It was incredibly easy figuring out how to use it and it seems to handle .NET 2.0 stuff like generics pretty well. Anyway, I had great fun writing this tool and I hope it proves to be a useful addition to CP's repository of free tools. Comments, suggestions (and bug reports:)) welcome.
History
- Initial version - 11:23 AM 4/4/2007
- Updated - 8:52 AM 4/5/2007
- Fixed bugs that prevented inner classes and generic types from being filtered correctly
- Added wildcard matching for type names