0

I have a plugin architecutre which is using a Shared DLL to implement a plugin interface and a base plugin class (eg. IPlugin & BasePlugin). The shared DLL is used by both the Plugin and the main application.

For some reason, when I try to cast an object that was instantiated by the main application to the shared plugin interface (IPlugin), I get an InvalidCastException saying I cannot cast this interface.

This is despite:

  • The class definitely implementing the plugin interface.
  • The Visual Studio debugger saying that "(objInstance is IPlugin)" is true when I mouse-over the statement, despite the same 'if' condition evaluating as false when I step-through the code or run the .exe.
  • The Visual Studio Immediate Window also confirms that the above condition is true and that is it possible to cast the object successfully.
  • I have cleaned/deleted all bin folders and also tried both VS2019 and VS2022 with exactly the same outcome.

I am going a little crazy here because I assume it is something to do with perhaps with multiple references of the same DLL somehow causing the issue (like this issue). The fact that the debugger tells me everything is okay makes it hard to trouble-shoot. I'm not sure if it's relevant but I have provided example code and the file structure below:

Shared.dll code

public interface IPlugin
{
}

public class BasePlugin : IPlugin
{
}

Plugin.dll code

class MyPlugin : BasePlugin
{
   void Init()
   {
       // The plugin contains references to the main app dlls as it requires
       // to call various functions inside the main app such as adding menu items etc
   }
}

Main.exe

(Note: This is pseudo-code only and does not show the plugin framework used to load the plugin DLLs via Assembly.Load())

var obj = Activator.CreateInstance(pluginType);  // Plugin type will be 'MyPlugin'
if (obj is IPlugin)   // <== This executes as false when executed but evaluates to true in the Visual Studio debugger
{
}

var castObj = (IPlugin)obj  // <== This will cause an invalid cast exception

Folder structure

|--- MainApp.exe
|--- Shared.dll
|--- Addins
|------- Plugin.dll
|------- Shared.dll

Does anyone know the reasons how I can trouble-shoot this issue and what might be causing it?

Simple Guy
  • 568
  • 5
  • 21
  • When you have duplicate classes it is best to reference the class by it full name including namespace. For example TcpClient can also be reference as System.Net.Sockets.TcpClient. – jdweng Jun 24 '22 at 23:43
  • There is no other class named the same. The only "duplication" would be that there are two Shared.dll's. If this were the issue, I don't understand why VS would not also give me the same exception. – Simple Guy Jun 24 '22 at 23:53
  • None of your code performs any casts. Did you omit that code or are you using the term euphemistically for the `as` operator, even though such usage would never throw the exception you named? – Kirk Woll Jun 25 '22 at 00:32
  • The main code is effectively pseudo-code which demonstrates that the object is not considered an IPlugin. If I add the code "var castObj = (IPlugin)obj", it will fail with 'invalid cast' exception. That is, only if is executed in code. If I execute the same code in VS Immediate Window, everything is ok. – Simple Guy Jun 25 '22 at 03:54
  • Have you tried looking in the `Modules` window to see whether `Shared.dll` is loading the location you expect? I've solved certain goofy behaviors in my own plugin architectures by preloading common interface and base class .dlls at the very start using `Assembly.LoadFrom`. In my case, all these different plugins were making their own local copies in their respective `bin` directories during build. What I was seeing before I put "preload from deterministic path" in place was a load-on-demand behavior that sometimes had funny ideas about what paths to probe and a stale copy might be loaded. – IVSoftware Jun 25 '22 at 06:07
  • The dlls are the same and contain classes. The error is due to the namespaces being different. So solution is to make the namespaces the same. – jdweng Jun 25 '22 at 08:30
  • 1
    Hi @IVSoftware, thanks for the tip about looking in Modules. I can see two of the same DLL loaded from two different folder locations. I think this is the key to the issue. It still doesn't make sense to me why VS would be using a different version of the DLL than the executing assembly. I would expect the normal execution and the debugger to have the same result. – Simple Guy Jun 26 '22 at 23:19
  • @jdweng, Thanks for the suggestion but this isn't the problem as there are no duplicate classes/code. It seems there are two of the same DLLs loaded from different locations though and I think this is the cause. – Simple Guy Jun 26 '22 at 23:42
  • Thanks for letting me know that! I guess next I have to ask you whether any of your .dlls are signed with .snk or .pfx because that "could" create different assembly identities. – IVSoftware Jun 26 '22 at 23:44
  • Hi @IVSoftware, the DLLs are not signed in any way. I assumed that .NET would automatically prevent the same DLL from being loaded twice based on hash or something. Not sure how the internals work on this point. – Simple Guy Jun 28 '22 at 22:58

1 Answers1

1

My suggestion is that (using the Properties\Signing tab) you sign your shared dll assembly with a Strong Name Key. If your plugin classes reference a signed copy of the dll, there should be no possibility of confusion.

signing popup

The application has no prior knowledge of what the plugins are going to be, but it does know that it's going to support IPlugin. Therefore, I would reference the PlugInSDK project directly in the app project.

Testbench

Here's what works for me and maybe by comparing notes we can get to the bottom of it.

static void Main(string[] args)
{

Confirm that the shared dll (known here as PlugInSDK.dll) has already been loaded and display the source location. In the Console output, note the that PublicKeyToken is not null for the SDK assembly (this is due to the snk signing). 'PlugInSDK, Version=1.0.0.0, Culture=neutral, PublicKeyToken=5d47bcb0b1dd9d79'

    var sdk = 
        AppDomain.CurrentDomain.GetAssemblies()
        .Single(asm=>(Path.GetFileName(asm.Location) == "PlugInSDK.dll"));
    Console.WriteLine(
        $"'{sdk.FullName}'\nAlready loaded from:\n{sdk.Location}\n");

The AssemblyLoad event provides the means to examine on-demand loads as they occur:

    AppDomain.CurrentDomain.AssemblyLoad += (sender, e) =>
    {
        var name = e.LoadedAssembly.FullName;
        if (name.Split(",").First().Contains("PlugIn"))
        {
            Console.WriteLine(
                $"{name}\nLoaded on-demand from:\n{e.LoadedAssembly.Location}\n");
        }
    };

It doesn't matter where the plugins are located, but when you go to discover them I suggest SearchOption.AllDirectories because they often end up in subs like netcoreapp3.1.

    var pluginPath =
        Path.Combine(
            Path.GetDirectoryName(Assembly.GetEntryAssembly().Location),
            "..",
            "..",
            "..",
            "PlugIns"
        );

Chances are good that a copy of PlugInSDK.dll is going to sneak into this directory. Make sure that the discovery process doesn't accidentally pick this up. Any other Type that implements IPlugin gets instantiated and put in the list.

    List<IPlugin> plugins = new List<IPlugin>();

    foreach (
        var plugin in 
        Directory.GetFiles(pluginPath, "*.dll", SearchOption.AllDirectories))
    {
        // Exclude copy of the SDK that gets put here when plugins are built.
        if(Path.GetFileName(plugin) == "PlugInSDK.dll") continue;

        // Be sure to use 'LoadFrom' not 'Load'
        var asm = Assembly.LoadFrom(plugin);

        // Check to make sure that any given *.dll 
        // implements IPlugin before adding to list.
        Type plugInType = 
            asm.ExportedTypes
            .Where(type =>
                type.GetTypeInfo().ImplementedInterfaces
                .Any(intfc => intfc.Name == "IPlugin")
            ).SingleOrDefault();

        if ((plugInType != null) && plugInType.IsClass)
        {
            plugins.Add((IPlugin)Activator.CreateInstance(plugInType));
        }
    }

Now just display the result of the plugin discovery.

    Console.WriteLine("LIST OF PLUGINS");
    Console.WriteLine(
        string.Join(
            Environment.NewLine, 
            plugins.Select(plugin => plugin.Name)));
    Console.ReadKey();
}

Is this something you're able to repro on your side?

Console output

IVSoftware
  • 5,732
  • 2
  • 12
  • 23
  • 1
    [Clone](https://github.com/IVSoftware/plug_in_architecture.git) this sample. – IVSoftware Jun 29 '22 at 18:30
  • Hi @IVSoftware, thanks for the framework. I've actually narrowed it down by using the Modules window in VS. It looks like a DLL was loaded twice. I think this is the cause of the problem and trying to weed it out. – Simple Guy Jul 01 '22 at 00:16
  • 1
    Glad you're figuring it out! I guess my point was that it would be _impossible_ for a strong-named assembly to _be_ loaded twice (because that's what strong-naming is _for_) and the hope was as soon as you add the snk _boom_ whatever the issue is should be pretty obvious at that point! Anyway good luck I'm rooting for you. – IVSoftware Jul 01 '22 at 00:19
  • Appreciate your help, @IVSoftware. Unfortunately strong naming is not an ideal option for me. It makes more sense that I ensure any lax calls to add DLL references are cleaned up. – Simple Guy Jul 01 '22 at 00:29
  • 1
    Got it! Oh the other thing I noticed that we might be doing differently is that it looks as though you're calling `Assembly.Load` and I tend to use `Assembly.LoadFrom`. I don't know whether that's a "clue" or not :) – IVSoftware Jul 01 '22 at 00:31