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:
- The exception had useful information, specifically a useful explanation and unique keywords that I could search MSDN for more details.
- 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
Anonymous
December 22, 2006
The comment has been removedAnonymous
December 23, 2006
Reflector / VS.Net are probably using the unmanaged Metadata APIs directly instead of Reflection. That avoids binding problems.Anonymous
February 09, 2007
The comment has been removedAnonymous
February 10, 2007
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=1Anonymous
October 07, 2007
I just noticed that my blog had birthday #3 (Sep 30th) . In tradition, some various stats... 384 posts.