简体   繁体   中英

Getting InvalidCastException with a Plugin architecture

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?

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.

签名弹出窗口

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,版本=1.0.0.0,文化=中性,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?

控制台输出

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM