views:

502

answers:

5

I have a problem that is not uncommon when building a plug-in architecture.

  1. Assembly A is the core code -- the framework.

  2. Assembly B is a plugin to that code, expected to load dynamically at runtime and make code available for Assembly A to use.

In Visual Studio, Project B (which generates Assembly B) has a reference to Project A (which generates Assembly A), so it can reference the types in Project A and compile just fine. Both projects compile without error and generate DLLs.

But, at runtime, I get cast exceptions like this:

'MyType' cannot be converted to type 'MyType'

I've come to understand that a type defined in one project and referenced in another are considered different types at runtime, even if they resolve fine at compile time.

But, I thought this could be fixed by loading Assembly B into the correct context. So I switched from "Assembly.LoadFrom" to "Assembly.Load" and put Assembly B in the probing path. The idea is that it would load into the same context as Assembly A, and they would be one big, happy family.

I downloaded the Fusion log viewer and watched the assemblies load. Both Assembly A and Assembly B load like this:

Assembly is loaded in default load context.

So, I have ensured they are both loaded in the same load context.

Still, I can't share types between the two Assemblies. I get casting errors when I try to pass an object (whose type is defined in Assembly A) as a parameter from code in Assembly A to a method on an object in Assembly B.

To summarize:

  1. MyType is defined in Assembly A

  2. From Assembly A, I load Assembly B at runtime into the same load context using "Assembly.Load"

  3. From Assembly A, I use Reflection to invoke a static method on a class in Assembly B. I pass this method an object of MyType as a parameter (which is expected, and which compiled just fine).

  4. Fails with: 'MyType' cannot be converted to type 'MyType'

Here's the code I use to invoke the method, if that matters. This code executes in Assembly A:

TypeFromAssemblyB.GetMethod("MyMethod").Invoke(null, new object[] { ObjectOfTypeDefinedInAssemblyA });
A: 

Is there a chance you have a version mismatch problem? If Assembly B has a reference to v1.0.x.y of Assembly A, but Assembly A's loaded version is v1.0.w.z, the types are incompatible. Loading them into reflector (or a similar tool) should allow you to verify the version on each assembly, as well as the version on the reference from B to A.

I've run into this occasionally where I deploy an app that is using automatic version numbers, then do a rebuild (which increments the version on both assemblies), and only re-deploy one of the new libraries. Of course, in my case, I wasn't using this dynamic load stuff or reflection to make calls between them, so you may have a more complicated issue at work here.

Jonathan
Interesting angle. Alas, the version numbers are fine. The reference in Project B is to the correct version of Project A and the resulting Assembly A. The version numbers are not set to auto-increment, so they've never changed.
Deane
A: 

This is just a quick response, so there may be some hole.

We write plug-ins all the time. In the invokation you describe I would expect the ObjectOfTypeDefinedInAssemblyA to be something you declared in Assembly B, absent the naming conflict. It seems to seriously violate the loose coupling nature of OOP.

For example one of our plug-ins in a line of business application handles a special type of inter-office messaging. I wouldn't expect Assembly A to know/define the object that encapsulates the message. I would expect that Assembly B would be invoked to instantiate that object from assembly A; asembly B would then pass it back to A via the invoke.

Bill
The idea is that Assembly B accepts and modifies the object, then hands it back. (I didn't know in my code that "ObjectOfTypeDefinedInAssemblyA" is handed back to Assembly A by the invocation, where it continues on its merry way.
Deane
Didn't "note" in my code, I meant.
Deane
+3  A: 

I was sure this should work as you've described it, so I wrote a quick and dirty example. My entry assembly is called PluginTest and contains the following class:

namespace PluginTest
{
    public class MyType
    {
        public string Data { get; set; }
    }
}

And my plugin assembly is called Plugin. It references the PluginTest project in Visual Studio and only contains this class:

using System;
using PluginTest;

namespace Plugin
{
    public class Class1
    {
        public static void ShowData(MyType input)
        {
            Console.WriteLine(input.Data);
        }
    }
}

Finally back in PluginTest (the executable) I have this code in Main:

static void Main(string[] args)
        {
            Assembly pluginAssembly = Assembly.LoadFrom("Plugin.dll");

            Type pluginType = pluginAssembly.GetType("Plugin.Class1");

            MethodInfo mi = pluginType.GetMethod("ShowData");

            mi.Invoke(null, new object[]
            {
                new MyType {Data = "Hello World"}
            });

            Console.ReadLine();

        }

As expected this pops up a console window and writes "Hello World" to it. So, in principle, what you are trying to do should work...

(On preview I like Jonathan's idea that it may be a version mismatch - do a clean build of the entire solution and make sure that all the dlls get copied to the right folders)

Martin Harris
Thank you for going to all the trouble. Your code certainly looks exactly like mine. Perhaps this *is* a compile issue.
Deane
I have confirmed your result, which confused me even more. So, I know it's possible, but for some reason, doesn't work in my other environment.
Deane
@Martin Harris: Thanks a lot for this snippet! It has helped me.
Shiftbit
A: 

It could be that your assembly is being loaded into both the 'default load context' and some other context (or none). Are there any other entries in the Fusion log that indicate that this is happening?

Avoid Loading an Assembly into Multiple Contexts on MSDN provides further details.

Scott Munro
A: 

The technique described here will allow you to force the assemblies to be loaded into the default load context.

Scott Munro