Recently I had a chance to build a tool to track software dependencies across all software repositories and wanted to share some of my design thoughts on this subject as well working code of the tool. So why not use off-the-shelf tool, like NDepend? For what we wanted, it turned out to be not hard to build from scratch and plus it was fun.

Problem of Dependencies

Assume there are multiple source control (C#) git repositories. A repository can have one solution with multiple projects or it may contain multiple solutions. Source control repositories boundaries aren't that important, but what's important and typical is that certain assemblies are shared between solutions. If that wasn't the case we could simply use Visual Studio to track them down. Higher SKU's of Visual Studio have even better dependencies visualization tools, but given just VS professional and a 100+ solutions, finding what depends on what is challenging. For example, there may be a web services solution with two projects both of which have dependency on common dll which is defined in a separate solution and shared with the first one as Nuget package or simply file references.

So given this solution structure on the left we want to produce a dependency graph (digraph) on the right:
DependencyGraphProblem

Assume that we are talking about static compile time assembly dependencies only because dependencies can be in the form Nuget packages as well. Dependencies can also be dynamic inter-process communication dependencies like web services, message queues and alike.

Collecting dependencies

Given a solution or it's binaries, how do we build a collection of this classes:

public class Module  
{
    public int? Id { get; set; }
    public string Name { get; set; }      
    public string Version { get; set; }        
    public string Description { get; set; }        
    public List<Module> References { get; private set; }
}

It could be done through reflection, but I chose a combination of Roslyn and reflection because more complicated dependency tracking problems like tracking dynamic web services dependencies would be easier to solve.

Roslyn has two major models, Syntax Analysis which deals with structure of the code and Semantic Analysis for behavior analysis (what program does). We need second one for collecting the dependencies and here's the complete Roslyn collector class:

  
  
public class ModuleCollector  
{
    private string[] _excludedProjectNames = { "test" };
    private string[] _excludedModuleNames = { "System", "Microsoft", "mscorlib" };

    public List GetModulesBySolution (string solutionPath)
    {
        using (var workspace = MSBuildWorkspace.Create())
        {
            var solution = workspace.OpenSolutionAsync(solutionPath).Result;

            var modules = new List();
            foreach (var project in solution.Projects)
            {
                if (_excludedProjectNames.Any(w => project.Name.ToLower().Contains(w.ToLower())))
                {
                    continue;
                }

                var projectModulePath = project.OutputFilePath;
                var projectModuleName = Path.GetFileNameWithoutExtension(projectModulePath);
                var reflectionPropeties = GetAssemblyReflectionProperties(projectModulePath);
                var module = new Module(projectModuleName, reflectionPropeties.Version, reflectionPropeties.Description);

                // Build the list of all dlls referenced by the project, both assembly(metadata) and project references.
                List dllPaths = project.MetadataReferences.Select(s => s.Display).ToList();
                foreach (var projectReference in project.ProjectReferences)
                {
                    var referencedProject = solution.Projects.Single(s => s.Id == projectReference.ProjectId);
                    dllPaths.Add(referencedProject.OutputFilePath);
                }

                foreach (var dllPath in dllPaths)
                {                        
                    string fileName = Path.GetFileNameWithoutExtension(dllPath);
                    if (_excludedModuleNames.Any(w => fileName.ToLower().Contains(w.ToLower())))
                    {
                        continue;
                    }
                    var reflectionProperties = GetAssemblyReflectionProperties(dllPath);
                    module.AddReference(new Module(fileName, reflectionProperties.Version, reflectionProperties.Description));                                                
                }
                modules.Add(module);
            }

            return modules;
        }
    }

    private ReflectionProperties GetAssemblyReflectionProperties (string filePath)
    {
        try
        {
            var assembly = Assembly.LoadFrom(filePath);
            var descriptionAttribute = assembly.GetCustomAttribute();
            return new ReflectionProperties
            {
                Version = assembly.GetName().Version.ToString(),
                Description = descriptionAttribute != null ? descriptionAttribute.Description : string.Empty                    
            };
        }
        catch (FileLoadException)
        {
            // Example: mixed mode assembly is built against version 'v2.0.50727' of the runtime and cannot be loaded in the 4.0 runtime
            return new ReflectionProperties
            {
                Version = "Unknown"
            };
        }
    }
}

internal class ReflectionProperties  
{
    public string Version { get; set; }
    public string Description { get; set; }
}
  

Here we exclude certain projects types and assemblies then call to OpenSolutionAsync (line 11) returns a list of projects. Just like it is in Visual Studio, each project has project and assemblies references which we combine into one list (line 31). Once we have that, we need reflection API to grab version and description attributes.

In order for this code to run on the build server, the server must have Microsoft Build Tools 2015. If Visual Studio isn't installed on the build server, there should be a folder with a couple of files for build targets. These files can be copied from the machine that has Visual Studio.