4
votes

I'm implementing application that supports plugins. Currently the problem arises when I try to load common assembly that is used both by host application and plugin: host application should use one version of that assembly, while plugin uses another version. This is dictated by application upgrade process - plugin can be updated separately from host application.

Every assembly is signed, so I use strong names for loading assemblies.

I created a test application which demonstrates the problem. Plugin assemblies are located in subfolder 'Plugin' of host application. Plugin folder contains the plugin implementation DLL, plugin declaration interface DLL and CommonLib DLL. Host application also use plugin declaration DLL and CommonLib DLL (located 1 level up in folder tree).

Here is source code of plugin loading:

static void Main(string[] args)
{
    Console.WriteLine("Host   says: {0}", GreetingManager.SayHello("Sasha"));
    Console.WriteLine("Host   says: {0}", GreetingManager.SayGoodBye("Sasha"));
    var pluginDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugin");
    var pluginFile = Directory.GetFiles(pluginDir).First(f => f.EndsWith("Plugin.dll"));

    Assembly asmr = Assembly.ReflectionOnlyLoadFrom(pluginFile);

    AppDomain pluginDomain = AppDomain.CreateDomain("Plugin", null, pluginDir, "", false);

    pluginDomain.Load(asmr.FullName);

    Assembly asm = pluginDomain.GetAssemblies().First(a => a.GetName().Name == "Plugin");
    Type p = asm.GetTypes().First(t => t.GetInterfaces().Contains(typeof(IPlugin)));

    var pluginInstance = (IPlugin)pluginDomain.CreateInstanceAndUnwrap(asm.FullName, p.FullName);
    Console.WriteLine("Plugin says: {0}", pluginInstance.TransformString("Sasha"));
}

It throws exception:

Unhandled Exception: System.IO.FileLoadException: Could not load file or assembly 'CommonLib, Version=1.0.9.0, Culture=neutral, PublicKeyToken=73c7c163a33b622c' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)
   at Plugin.MyPlugin.TransformString(String str)
   at PluginBackwardCompatibilityTest.Program.Main(String[] args) in D:\Projects\Test\PluginBackwardCompatibilityTest\PluginBackwardCompatibilityTest\Program.cs:line 37

As I udnerstand, plugin still tries to find CommonLib assembly in the host application folder (ignoring the appBasePath parameter I provided when creating AppDomain), but finds older version 1.0.8 there and generates this error. How can I force plugin to load referenced assemblies from Plugin folder - there is no way to specify full assembly path to load when using strong name.

2

2 Answers

6
votes

I think your problem is the following:

Assembly asm = pluginDomain.GetAssemblies().First(a => a.GetName().Name == "Plugin");
Type p = asm.GetTypes().First(t => t.GetInterfaces().Contains(typeof(IPlugin)));
var pluginInstance = (IPlugin)pluginDomain.CreateInstanceAndUnwrap(asm.FullName, p.FullName);

With this you're loading the updated/outdated type references into the main AppDomain (which has the assembly already loaded in a different version).

I recommend you use the following approach:

  1. Create a separate assembly that contains the contracts/Interfaces. This Assembly is fixed and remains at a specific version all the time and it can never be outdated and each version of your plugin can reference it.
  2. Write an assembly loader that is part of your host application, but in a seperate assembly as well. This assembly loader assembly must not have any reference to the rest of your application so you can set it up in the new appdomain.
  3. After setting up the new appdomain, load an instance of your AssemblyLoader. This one loads the plugin assemblies and interacts with it.
  4. Your application doesn't interact with the assembly itself. Your application calls your AssemblyLoader via the appdomain boundary and the AssemblyLoader calls the Plugin

I can't say if this is the best version possible, but this one works pretty well for me in an environment where each plugin can load any version of any assembly. With this setup you're pretty much resistant to changing versions or updates and each plugin can use the version it wants.

Let me know if it works for you.

1
votes

Most likely it is your own code that load latest version first - check if CommonLib is loaded into new app domain right before the call to Load.

If it is the case you'll need to be very careful in linking your main code to CommonLib to avoid loading latest version too early. You may even need to use late binding/reflection for it.