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. 

Comments (5)

  1. rstrahl says:

    Thanks for this post. I just ran into this very same issue today.

    Oddly I still have problems loading one of the assemblies which is having problems with versioning or a manifest reference of some sort.

    I was hoping that using the ReflectionOnly option might be able to get around this issue. My code bombs on GetTypes() with several loader exceptions. Oddly though: Reflector and VS.NET can easily display the types and their signatures, so how are they getting at it I wonder?

  2. jmstall says:

    Reflector / VS.Net are probably using the unmanaged Metadata APIs directly instead of Reflection. That avoids binding problems.  

  3. David Anton says:

    Thanks for this – it put an end to a couple of hours of banging my head against the wall.

    Something else I’d like to know is: can ReflectionOnlyLoad somehow be combined with the technique of loading assemblies into a separate temporary AppDomain object in order to unload that AppDomain object when reflection is done?  Currently, I’m not thrilled with the prospect of having the assemblies in memory as long as the app is running.

  4. jmstall says:

    As of .NET 2.0, the CLR doesn’t support unloading modules unless you unload the entire appdomain; even for Reflection-Only.

    If you want to combine AppDomain unload + Reflection-only load, you could always spin up a 2nd appdomain and Reflection-Only load the module into it. But I don’t believe there’s any support that makes that easier.

    You should check out the CLR forums at:

    http://forums.microsoft.com/MSDN/ShowForum.aspx?ForumID=44&SiteID=1

  5. I just noticed that my blog had birthday #3 (Sep 30th) . In tradition, some various stats… 384 posts.