Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MEF not detecting plugin dependencies

Tags:

c#

.net

plugins

mef

I have a problem with MEF and using a plugins folder.

I have a main app that supports plugins via MEF. The main app does not reference the assemblies containing the .NET Task type for multithreading but one or more of the plugins do.

The plugins are located in a Plugins folder and I'm using a DirectoryCatalog.

I keep getting ReflectionTypeLoadException being thrown by MEF on

Unable to load one or more of the requested types. Retrieve the LoaderExceptions property for more information.

The LoaderExceptions property contains a FileNotFoundException

"Could not load file or assembly 'System.Threading.Tasks, Version=1.5.11.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies. The system cannot find the file specified.":"System.Threading.Tasks, Version=1.5.11.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"

The plugin is referencing System.Threading.Tasks via a Microsoft NuGet package reference.

This is my helper method:

public static void Compose(IEnumerable<string> searchFolders, params object[] parts)
{
    // setup composition container
    var catalog = new AggregateCatalog();

    // check if folders were specified
    if (searchFolders != null)
    {
        // add search folders
        foreach (var folder in searchFolders.Where(System.IO.Directory.Exists))
        {
            catalog.Catalogs.Add(new DirectoryCatalog(folder, "*.dll"));
        }
    }

    catalog.Catalogs.Add(new AssemblyCatalog(typeof(Program).Assembly));

    // compose and create plug ins
    var composer = new CompositionContainer(catalog);
    composer.ComposeParts(parts);
}

public class MEFComposer
{
    [ImportMany(typeof(IRepository))]
    public List<IRepository> Repositories;

    [ImportMany(typeof(ILogging))]
    public List<ILogging> LoggingRepositories;

    [ImportMany(typeof(IPlugin))]
    public List<IPlugin> Plugins;
}

This is the code I'm using to invoke MEF and load the plugins.

public void Compose()
{
    // try to connect with MEF types
    try
    {
        var parts = new MEFComposer();
        MEFHelpers.Compose(new[] { Path.Combine(Application.StartupPath, "Plugins") }, parts);
        RepositoryFactory.Instance.Repository = parts.Repositories.FirstOrDefault();
        Logging.Repositories.AddRange(parts.LoggingRepositories);
        foreach (var plugin in parts.Plugins)
        {
            this.applicationApi.Plugins.Add(plugin);
            plugin.Connect(this.applicationApi);
        }
    }
    catch
    {
        // ERR: handle error
    }
}

Why is MEF not able to load the plugins even though the Microsoft.Threading.Tasks.dll and related assembly files are present in the Plugins folder, but not the main application bin folder? And is there any way of telling MEF to search the Plugins folder for assembly dependencies?

Having a plugin model means I can't anticipate what assemblies a plugin may be referencing so I cannot include them in the main bin folder for the app, which is why I want all related plugins and plugins dependencies to be in the plugins folder.

like image 867
Dean Lunz Avatar asked Jan 17 '15 22:01

Dean Lunz


1 Answers

You have run into an essential problem when supporting 3rd party plugins. Your problem is that when you are loading the plugin, the runtime will search for its references when needed only in the specified folders your AppDomain knowns of. That would be the WorkingDirectory of that process, then path etc.

Essentially, you are loading an Plugin that requires System.Threading.Tasks. That DLL lies within your /Plugin folder. As .net loads your plugin, it will search for that assembly but has no way in finding it as it is located within the /Plugin folder and fails.

There are a couple of solutions to that.

  1. Do not use a Plugin folder

That would be the most simple solution, when all assemblies (including references of the 3rd party lib) are located in your WorkingDirectory, .net will have not trouble finding all references of that plugin.

  1. Add the Plugin folder to your Probing path

This will extend the paths .net will search the 3rd party references: https://docs.microsoft.com/en-us/dotnet/framework/deployment/how-the-runtime-locates-assemblies#locating-the-assembly-through-probing

  1. Use AppDomains for each plugin.

An AppDomain is my choice of go here as it allows you to load the assembly not only in its "own" container, but also can simulate the Working directory only for the plugin. That can come handy if on of the Plugins uses the same framework as your application but in a different version for example.

  1. Load the dependencies by yourself

That would be the "straight forward" method of solving this. You can load every assembly as ReflectionOnly, Determinate all dependencies and then load these. That will almost Garantie to work.

4.1. AssemblyResolve event

This is just another way how to "redirect" .net to load assemblies from the PluginFolder https://docs.microsoft.com/en-us/dotnet/api/system.appdomain.assemblyresolve?view=netframework-4.8

Edit: There is also a certain problem with the AssemblyCatalog, it uses Assembly.Load instead of Assembly.LoadFrom to load the given assembly. That is an essential part of your problem, as LoadFrom would probe the path where the assembly originates from for its dependencies where Load does not.

https://github.com/JPVenson/MSEF/blob/master/JPB.Shell/JPB.Shell.MEF/Model/StrongNameCatalog.cs

You could use an Catalog like this one that is using LoadFrom instead. Disclaimer: I am the creator of that project.

like image 109
Venson Avatar answered Sep 29 '22 11:09

Venson