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 unhandled
Message="Unable to cast object of type 'Plugin.Person' to type 'Plugin.Person'."
at LoaderContextSample.Program.Main(String args) in C:\LoaderContextSample\Program.cs:line 29
at System.AppDomain.nExecuteAssembly(Assembly assembly, String args)
at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String args)
at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
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=188.8.131.52, 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:
- Assemblies will only EVER be loaded from the GAC based on a full bind (name, version, and public key token). A partial bind with name and public key token only WON’T load from the GAC.
- If you reference an assembly with VS.NET you're asking for a full bind. If you say Assembly.Load("foo") you're asking for a partial bind.
- However, the way this usually works is…
- You do a partial bind on assembly name, or name and public key token with Assembly.Load
- Fusion (the code name for the Assembly Loader/Binder) starts walking the probing path looking for an assembly that matches the partial bind.
- Counter Intuitive: If it finds one while probing (the first one) it will then attempt to use the strong name of the one it found to do a full bind against the GAC.
- If it’s in the GAC, that’s the one that gets loaded.
- Any of that loaded assemblies will try to load from the GAC first without going to the probing path, since the embedded references constitute a full bind.
- If they aren’t found in the GAC, then it will start probing.
- It’ll grab the first one it finds in the probing path. If the versions don’t match, Fusion fails. If they do match, Fusion loads that one.
- So, if you specify a partial name, and the file is in the GAC, but not the probing path, the load fails, since there’s no way to do a full bind.
Here's the complete sample code from above if you like.