It's funny how things you don't think about for a long time appear in twos or threes. This issue came up at work twice recently, and once via an email from a blog reader.
I blogged about part of this in 2004 when we were dealing with a lot of the icky complexities of the Loader (Fusion) in .NET. We used Mike Gunderloy's 2003 Binding Policy Article an a lot of testing to figure out how Fusion worked for us. We also are HUGE fans of Richard Grimes' amazing Fusion Workshop as a resource. Also, I just keep Fusion logging (FORCELOG) turned on all the time and logging to c:\fusionlogs.
Anyway, the issue was this error message about an InvalidCastException:
System.InvalidCastException was unhandledMessage="Unable to cast object of type 'Plugin.Person' to type 'Plugin.Person'."Source="LoaderContextSample"StackTrace:at LoaderContextSample.Program.Main(String[] args) in C:\LoaderContextSample\Program.cs:line 29at System.AppDomain.nExecuteAssembly(Assembly assembly, String[] args)at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()at System.Threading.ThreadHelper.ThreadStart_Context(Object state)at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)at System.Threading.ThreadHelper.ThreadStart()
This can be initially very confusing because it says it's trying to cast type Person to type Person. Why aren't Person and Person the same, eh, Person?
Suzanne Cook puts it best, emphasis mine:
For example, path matters when determining whether a type is castable to another type. Even if the assemblies containing the types are identical, if they're loaded from different paths, they're considered different assemblies and therefore their types are different. This is one reason why using contexts other than the Load context is risky. You can get into situations where the same assembly is loaded multiple times in the same appdomain (once in the Load context, once in the LoadFrom context, and even several times in neither context), and their corresponding types won't be castable.
If you have an assembly you reference, but you also have a plugin that you've loaded, perhaps via LoadFrom (bad), if you intend for the types to be the same but they are loaded from different paths, they are effectively different types.
In this sample, I create a Person object via an assembly I've referenced the usual way, via Add Reference. Works great.
Then I load Person using Assembly.Load and create a Person via Reflection, then cast the object instance to the first kind of Person. Because I used Assembly.Load - usually the most appropriate Binding Context - the Loader (Fusion) finds the same assembly, and the Person Type created via Reflection is the same kind of Person as before. Works great. Note that Assembly.Load takes an Assembly Qualified Name like "Person" - not "Person.dll." Remember that Assembly QNs NEVER have .dll in their names.
Then I load the Person from a different path. In this example I've copied Person.dll to Person2.dll via a Post-Build-Event to illustrate this, but the most common way this would happen is that you've got a static reference to an assembly in the GAC, but your Plugin design uses LoadFrom to load a DLL from another path. Then you try to cast them. LoadFrom will almost always lead you astray.
If I was really serious I should have used the complete Assembly QN: Person, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null, and I would have strong named it for Assembly verification purposes, but that's another post.
Here's part of the sample:
Person p = new Person("Franklin", "Ajaye"); //Use the Load Context...almost ALWAYS the right thing to do... Assembly a = Assembly.Load("Person"); Type t = a.GetType("Plugin.Person"); object instance = t.InvokeMember(String.Empty,BindingFlags.CreateInstance, null, null, null); Person p1 = (Person)instance; //Use the LoadFrom...almost ALWAYS the *WRONG* thing to do... Assembly a2 = Assembly.LoadFrom(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Person2.dll")); Type t2 = a2.GetType("Plugin.Person"); object instance2 = t2.InvokeMember(String.Empty,BindingFlags.CreateInstance, null, null, null); Person p2 = (Person)instance2; //BOOM!
Lucky Line 13 is where the InvalidCastException happens and I see the dialog pictured at the top of this post. Boom.
The Loader has a lot of nuance, and it's helpful if you're building a large system with plugins and many points of extensibility to ask yourself - Where are my plugins on disks? What Types are shared? How are my plugins getting into memory?
Here's what we documented a long time ago. This only applies to dynamically loaded assemblies:
Here's the complete sample code from above if you like.
Download LoaderContextSample.zip
Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. I am a failed stand-up comic, a cornrower, and a book author.
Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.