Assembly binding Closure discovery tool


This is a tool I use to figure out the binding closure of one or many assemblies. Per request, I post the source code here.

It is provided as it is. I did not do any significant testing.

Sample output:

F:\Code\closure>more 1.txt
system, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

F:\Code\closure>closure 1.txt
Initial assemblies are :
        1: system, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

Binding closures are
        1: Microsoft.JScript, Version=8.0.1200.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
        Assembly is in GAC.
        2: Microsoft.Vsa, Version=8.0.1200.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
        Assembly is in GAC.
        3: mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
        Assembly is in GAC.
        4: System, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
        Assembly is in GAC.
        5: System.Security, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
        Assembly is in GAC.
        6: System.Xml, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
        Assembly is in GAC.

Here is the source code:

F:\Code\closure>more closure.cs

using System;
using System.IO;
using System.Reflection;
using System.Runtime.Remoting;
using System.Collections;
using System.Text;

class Closure
{
    public static void Main(String[] argv)
    {
        if (argv.Length != 1)
        {
            Console.WriteLine(“Usage: Closure AsmList.txt”);
            Console.WriteLine(“AsmList.txt contains full display names of starting assemblies.”);
            Console.WriteLine(“One assemblies per line.”);
            return;
        }

        try
        {
            ArrayList asms = new ArrayList(256);

            using (StreamReader sr = new StreamReader(argv[0]))
            {
                String line = null;

                while((line = sr.ReadLine()) != null)
                {
                    if (line != “”) {
                        asms.Add(line);
                    }
                }
            }

            Closure closure = new Closure(asms);
            closure.Run();
            closure.ShowResult();
        }
        catch(Exception e)
        {
            Console.WriteLine(“Exception: {0}”, e.Message);
        }
    }

    public Closure(ArrayList asms)
    {
        names = new Queue(256);
        results = new Hashtable(null,CaseInsensitiveComparer.DefaultInvariant);
        failed = new ArrayList(256);

        initAsms = asms;

        foreach (String s in asms)
        {
            names.Enqueue(s);
        }
    }

    void Run()
    {
        Hashtable probed = new Hashtable(null,CaseInsensitiveComparer.DefaultInvariant);

        Assembly asm = null;
        while (names.Count != 0) {
            Object o = names.Dequeue();
            String s = o as String;
            if (s != null) {
                if (probed.Contains(s)) {
                    continue;
                }

                probed.Add(s, s);

                try {
                    asm = Assembly.Load(s);
                }
                catch(Exception e) {
                    if ((e is FileNotFoundException) || (e is FileLoadException)) {
                        failed.Add(s);
                        continue;
                    }
                    else {
                        throw;
                    }
                }

                String asmname = asm.FullName;
                if (!results.Contains(asmname)) {
                    results.Add(asmname, asm);
                }
                AssemblyName[] asmDepNames = asm.GetReferencedAssemblies();
                foreach(AssemblyName name in asmDepNames) {
                    asmname = name.FullName;
                    if (!probed.Contains(asmname)) {
                        names.Enqueue(asmname);
                    }
                }
            }
        }
    }

    void ShowResult()
    {
        initAsms.Sort();
        int i = 0;
        Console.WriteLine(“Initial assemblies are :”);
        foreach (Object o in initAsms) {
            String s = o as String;
            if (s != null) {
                i++;
                Console.WriteLine(“\t{0}: {1}”, i, s);
            }
        }

        Console.WriteLine(“\nBinding closures are”);
        ICollection keys = results.Keys;
        String[] sKeys = new String[keys.Count];
        keys.CopyTo(sKeys, 0);

        Array.Sort(sKeys, CaseInsensitiveComparer.DefaultInvariant);
        for(i=0; i< sKeys.Length;i++) {
            Console.WriteLine(“\t{0}: {1}”, i+1, sKeys[i]);
            Object o = results[sKeys[i]];
            Assembly asm = o as Assembly;
            if (asm != null) {
                if (asm.GlobalAssemblyCache) {
                    Console.WriteLine(“\tAssembly is in GAC.”);
                }
                else {
                    Console.WriteLine(“\tAssembly is not in GAC.”);
                }
            }
        }

        if (failed.Count != 0) {
            failed.Sort();
            Console.WriteLine(“\nFollowing assemblies cannot be found :”);
            i = 0;
            foreach (Object o in failed) {
                String s = o as String;
                if (s != null) {
                    i++;
                    Console.WriteLine(“\t{0}: {1}”, i, s);
                }
            }
        }
    }

    private ArrayList initAsms;
    private Queue   names;
    private Hashtable results;
    private ArrayList failed;
}

Comments (9)

  1. Luc Cluitmans says:

    I once played around by making a similar tool that was not based on the standard Reflection support, but based on ‘Microsoft.Metadata.Prototype.dll’, which comes with FxCop. This dll allows you to ‘reflect’ on assemblies without truly loading those assemblies, bypassing any funky effects of Fusion. For instance, it allows you to implement your own strategy for locating the assemblies and it eases detection of version conflicts of assemblies (that was what I needed it for).

    Of course, publishing that code is unlikely to be allowed; the ‘Protototype’ part of the dll name is probably there for a reason. For getting started using it, check out the classes in the Microsoft.Cci namespace using Reflector, especially AssemblyNode and its AssemblyNode.GetAssembly(string) pseudoconstructor.

    By the way, do you know if there is any hope that this particular Dll is going to be officially documented or even supported at some time? It looks to be a great Dll for analysing assemblies without having to load them.

  2. I notice there are many libraries or applications claim that they can find assembly binding closure without doing any assembly loading. I don’t buy that. If you don’t use assembly loading, how do you know the reference you find is the assembly you will find at runtime? At best, those tools give you an assembly reference closure. But it is not the same as assembly binding closure.

    Writing something like Microsoft.Metadata.Prototype.dll is not very hard. Likely you will use the unmanaged metadata API to crack the metadata and cache the necessary information. But since you can’t use assembly loading, you will have to do your own caching and probing. Effectively, you are implementing your own fusion. But, like I said, it is not the same as assembly binding closure.

  3. Luc Cluitmans says:

    That is correct indeed.

    However my goal wasn’t to find out what the actual runtime closure is, but what dll versions are referenced, and in particular if any pair of assemblies in the "closure" contains references to *different* versions of the same weak-named dll. In that case fusion happily loads one version of the assembly, which may be a different version from what some of the assemblies were expecting (usually the newest version). Most of the time this is not a problem at all (if it was, the assemblies should have been strong named and be put in the GAC), but it still gives a slightly nagging feeling, so I want to detect the situation.

    And during compilation I get a hint that there is something wrong, but the hint is not detailed enough:

    Simplified: A.exe version 2.0 uses B.dll version 1.0 and C.dll version 1.1, while B.dll uses C.dll version 1.0. During compilation of A.exe I get a warning that a dll references C.dll version 1.0 while the output directory already contains C.dll version 1.1 (or some similar message, I forgot the precise message; a message that there is a conflict, but it leaves out the name of B.dll that caused it). In this simple sample it is clear that B.dll is the one referencing the old C.dll, but the message doesn’t tell me that. In my actual case there were 15 dlls involved, and I had no clue which one was still referencing the old C.dll.

    On a different point: I mentioned Microsoft.Metadata.Prototype.dll because that is as close as I thought I can get in C# to the ‘unmanaged metadata API’ you mention. Are you aware of any other managed APIs that access the metadata without actually loading assemblies? Or for that matter, are there any published unmanaged metadata APIs? You mention the ‘unmanaged metadata API’ as if it were published somewhere; if so, where can I find it? The only other metadata ‘APIs’ I was able to find were the word document describing the raw specs (not really an API…) and the Mono code accessing the metadata.

  4. Shripal Meghani says:

    In my opinion, the binding closure obtained from this tool is incomplete in the case where assemblies use reflection or dynamic loading to load other assemblies.

    To explain: Suppose I have clubbed interfaces IA, IB, etc into one assembly ‘I’. Next I implement interface IA in another assembly ‘A’ containing the class A, by referencing assembly ‘I’. In my main assembly, I reference assembly ‘I’, and perform

    IA ia = (IA)Assembly.LoadFile("A").CreateInstance("A");

    The binding closure from the tool above will give me the dependency to assembly "I" but will not do so for "A", which is required.

  5. Luc,

    yes, the "unmanaged metadata API" I referred to is the word docoment in "Tool Developer Guide" directory when you install .Net framework SDK, it describes the APIs, not just the spec.

    Shripal,

    The tool only cares about assemblies can be found by standard probing. For assemblies cannot be found by standard probing, it will list them in failure section. It is not the intention of this tool to handle dynamic loading, like LoadFile/LoadFrom/Load(byte[]). If you want to handle dynamic loading, you have to extend the tool yourself.

  6. Luc Cluitmans says:

    Junfeng,

    thanks for pointing to that document. I hadn’t found it before; almost anything I knew about the metadata was from a different document (partition II of the CLI spec) in the very same directory. Something tells me the ‘findability’ of ‘Metadata unmanaged API’ document is, eh, not optimal :). Maybe there should be some more pointers to it in the normal Help system?

  7. This has been asked several times. I don’t really know why those are not part of the MSDN. Presumably it is only interesting to a very small subset of developers.

  8. Shripal Meghani says:

    Hi Junfeng,

    Well, it would have been useful to find a *complete* binding closure (including dynamically loaded assmeblies)! To use reflection API’s to do the same, one would need one more tool: something that tells me the types used by an assembly. Once I have the types used by an assembly, I can figure out how to compute the complete closure. By types used, I mean if the referenced assembly contains several types, of which I use only a few, the "tool" I refer to, would give me only the used types. However, I understand thats not easy to do, since I would need to parse the entire IL to obtain this information.

  9. Shripal,

    It is not possible to find all dynamically loaded assemblies. The assembly name/file is constructed at runtime. There is no way to know that by doing static analysis.