Viewing types with Reflection-Only

It's natural for a tool to use Reflection-Only loading to load an assembly and view the types in it.  For example, I used this in my pdb2xml tool. However, I missed an important detail that I wanted to warn you about after getting it wrong myself.

Consider the following snippet which will print all the type names in an assembly.

 
// This has a bug!!!! See correct version below
using System;
using System.Reflection;
using System.Collections.Generic;
using System.Text;

class Program
{
    static void Main(string[] args)
    {
        string filename = args[0];
        Assembly a = System.Reflection.Assembly.ReflectionOnlyLoadFrom(filename);

        Console.WriteLine("Opened assembly:{0}", filename);
        foreach (Type t in a.GetTypes())
        {
            Console.WriteLine("  " + t.FullName);
        }
    }
}

So if you compile it as type_sniff.exe and run it on itself, it would print:

 >type_sniff.exe type_sniff.exe
Opened assembly:type_sniff.exe
  Program

Looks about right. But there's a problem. Pop quiz: what's wrong? (and I'm not talking about additional error checking, etc).

...

 

...

 

Answer:
Run it on some other inputs and you'll get a ReflectionTypeLoadException. You just need a type that derives from a type in another dll. For example:

 // defined in a.dll
public class Foo
{
    public Foo()
    {
    }
}

// defined in b.dll, compiled as /r:a.dll
public class Bar : Foo
{
    Bar()
    {
    }
}

And then run: type_sniff.exe b.dll

 C:\bug\type_sniff\type_sniff\bin\Debug>type_sniff.exe b.dll
Opened assembly:b.dll

Unhandled Exception: System.Reflection.ReflectionTypeLoadException: Unable to load one or more of the requested types. Retrieve the LoaderExceptions property for more information.
at System.Reflection.Module.GetTypesInternal(StackCrawlMark& stackMark)
at System.Reflection.Assembly.GetTypes()
at Program.Main(String[] args) 

Following the exception message (and Suzanne's advice) to look at the LoaderException property, I see (emphasis mine):

{"Cannot resolve dependency to assembly 'a, Version=2.1.0.0, Culture=neutral, PublicKeyToken=ebb8d478f63174c0' because it has not been preloaded. When using the ReflectionOnly APIs, dependent assemblies must be pre-loaded or loaded on demand through the ReflectionOnlyAssemblyResolve event.":"a, Version=2.1.0.0, Culture=neutral, PublicKeyToken=ebb8d478f63174c0"}

So what happened was that it tried to get the System.Type for Bar, but to resolve the type it needs to load the base class, which is in another dll. Reflection-Only context doesn't do binding policy so it can't find that dll. The LoaderException hint says to use the ReflectionOnlyAssemblyResolve, which provides more information about this.

So I add a ReflectionOnlyAssemblyResolve event. Intellisense was very helpful in generating the glue code.

So now the code looks like (key additions highlighted in yellow):

 
using System;
using System.Reflection;
using System.Collections.Generic;
using System.Text;

class Program
{
    static void Main(string[] args)
    {
        string filename = args[0];
        AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += new ResolveEventHandler(CurrentDomain_ReflectionOnlyAssemblyResolve);
        Assembly a = System.Reflection.Assembly.ReflectionOnlyLoadFrom(filename);

        Console.WriteLine("Opened assembly:{0}", filename);
        foreach (Type t in a.GetTypes())
        {
            Console.WriteLine("  " + t.FullName);
        }
    }

    static Assembly CurrentDomain_ReflectionOnlyAssemblyResolve(object sender, ResolveEventArgs args)
    {
        return System.Reflection.Assembly.ReflectionOnlyLoad(args.Name);
    }
}

This assumes that all the dependencies are in the working directory. You could have a fancier AssemblyResolve event that used the current directory from the original filename and had fancier search logic.

And when I run it on b.dll, it works properly:

 C:\bug\type_sniff\type_sniff\bin\Debug>type_sniff.exe b.dll
Opened assembly:b.dll
Bar

Concluding thoughts:
Some cool things: There were some good things that made this much easier to diagnose:

  1. The exception had useful information, specifically a useful explanation and unique keywords that I could search MSDN for more details.
  2. The intellisense was very helpful in generating the glue code. That made it much faster to react.

Room for improvement: This falls under the category of evil bug that works most of the time; but fails on certain inputs. However, it seems like this ought to be avoidable (better library design) or detectable (fxcop rule). Basically any use of GetTypes() on reflection-only assemblies without handling the ReflectionOnlyAssemblyResolve event could be broken.