Search code examples
c#.net-coremef

Have Plugin use older version of assembly using MEF in .NET Core 3.1


Using this article Using MEF in .NET Core as a starting point, I am attempting to load a plugin that references an older version of my pretend "MessageQueue" library that differs from the version that the main console app uses.

Version 1.0.0 of my "MessageQueue" returns the value "Message from v1.0". Version 1.1.0 of my "MessageQueue" returns the value "Message from v1.1".

My main console app references v1.1 whereas my plugin references v1.0. However, when I call the plugin, the message returned from it's "MessageQueue" is the same message return by the console app's "MessageQueue", namely "Message from v1.1".

I am expecting that the plugin will use its version of "MessageQueue" and return "Message from v1.0" instead.

I have my solution arranged into 4 projects:

  1. ConsoleMEF - main console app
  2. Pluginable - class lib containing the IMessageSender interface
  3. MefPlugin - class lib containing the EmailSender implementing IMessageSender and marked as [Exported]. Hard assembly reference to v1.0 of Messages
  4. Messages - class lib containing the "MessageQueue" (project code is version 1.1, compiled assembly at v1.0 copied to plugin path)

Plugin code is here:

[Export(typeof(IMessageSender))] // Socket
public class EmailSender : IMessageSender
{
    public void Send(string message)
    {
        var messages = new MessageQueue();
        Console.WriteLine($"App Message Queue From Plugin: {messages.GetMessage()}");
        Console.WriteLine($"EMAIL: {message}");
    }
}

Here is how I load the plugin:

[Import] // Hook
public IMessageSender MessageSender
{
    get;
    set;
}

void Compose()
{
    // pluginPath contains:
    // - MefPlugin.dll
    // - Pluginable.dll
    // - Messages.dll, Version 1.0
    var pluginPath = Path.GetFullPath(@"c:\temp\plugins");

    var assemblies = Directory
        .GetFiles(pluginPath, "*.dll")
        .Select(AssemblyLoadContext.Default.LoadFromAssemblyPath)
        .ToList();

    var configuration = new ContainerConfiguration()
        .WithAssemblies(assemblies);

    using var container = configuration.CreateContainer();
    MessageSender = container.GetExport<IMessageSender>();
}

Here is how I call the plugin:

void Run()
{

    Compose();
    var messages = new MessageQueue();
    // CORRECT: Prints "Message from v1.1"
    Console.WriteLine($"App Message Queue From Console: {messages.GetMessage()}"); 

    // MessageSender.Send also calls the following:
    //Console.WriteLine($"App Message Queue From Plugin: {messages.GetMessage()}"); 
    // WRONG: Prints "Message from v1.1"
    // EXPECTED: "Message from v1.0"
    MessageSender.Send("Hello MEF");
}

MessageQueue v1.0 has the following code:

public class MessageQueue
{
    public string GetMessage()
    {
        return "Message from v1.0";
    }
}

MessageQueue v1.1 has the following code:

public class MessageQueue
{
    public string GetMessage()
    {
        return "Message from v1.1";
    }
}

I have done this easily in the past using AppDomain's and BasePath. But I can't seem to figure out how to do with with AssemblyLoadContext.

Where am I going wrong?


Solution

  • The solution appears to be two-fold.

    1. Create a new AssemblyLoadContext.
    2. Remove the Pluginable.dll from the plugin directory.

    Compose now looks like this:

    void Compose()
    {
        // pluginPath contains:
        // - MefPlugin.dll
        // - DELETED: Pluginable.dll.renamed
        // - Messages.dll, Version 1.0
        var pluginPath = Path.GetFullPath(@"c:\temp\plugins");
    
        //var loadContext = AssemblyLoadContext.Default;
        var loadContext = new AssemblyLoadContext("plugin"); // new context
    
        var assemblies = Directory
            .GetFiles(pluginPath, "*.dll")
            .Select(loadContext.LoadFromAssemblyPath)
            .ToList();
    
        var configuration = new ContainerConfiguration()
            .WithAssemblies(assemblies);
    
        using var container = configuration.CreateContainer();
        MessageSender = container.GetExport<IMessageSender>();
    }
    

    If Pluginable.dll is not removed from the plugins directory, the following exception is raised:

    System.Composition.Hosting.CompositionFailedException: 'No export was found for the contract 'IMessageSender'.'
    

    I figure it is raised because the resolver treats plugin's Pluginable.dll as separate and distinct from the main app's Pluginable.dll. Once it is removed from the plugin directory, I presume that the resolver must now be forced to look elsewhere for the reference.