Search code examples
c#.netmef

How to export one MEF plugin multiple times, depending on app.config?


I am building a simple MEF application. What I want to achieve is building one plugin, which can be registered multiple times in the same composing application. The registration of the plugin should be dependent on a setting from the configfile of the plugin, but I am not able to do this.

[edit]

My server, which has the CompositionContainer, needs to communicate with 6 different targets (ie Traffic Light Controllers). For every target, I want to add a plugin. The plugin logic is the same, so I want to maintain only 1 plugin. Every target has its own webaddress to communicate (and some other configuration items), I want those to be in (separate) configfiles.

What I tried is putting the plugins in subdirectories and going recursively through those directories to add the plugins in the catalog. This doesn't work however. The second plugin found in the subdirs will be imported, but this one is targeting the first plugin. When looping through the container FASTAdapters, all parts seem to equal to the first.

private void Compose()
{
    var catalog = new AggregateCatalog();
    string sDir = AppSettingsUtil.GetString("FASTAdaptersLocation", @"./Plugins");
    foreach (string d in Directory.GetDirectories(sDir))
    {
        catalog.Catalogs.Add(new DirectoryCatalog(d));
    }
    var container = new CompositionContainer(catalog);
    container.ComposeParts(this);
}

I don't know if I can also use the ExportMetadata attribute. It seems that ExportMetadata attributes have to be hardcoded, but I want the attribute being read from the config file if possible.

[/edit]

My goal is to have 6 ControllerAdapters, each targeting a different controller (read: communicating with a different webserver). The logic in the 6 ControllerAdapters is equal.

I thought copying the ClassLibrary (for example to 1.dll, 2.dll and so on) and adding the configfiles (1.dll.config and so on) should do the trick, but no.

When composing, I get multiple instances typeof(FAST.DevIS.ControllerAdapter) in the container, but I don't know how to get further.

Do I need to do something with MetaData in the export?

The importing server

[ImportMany]
public IEnumerable<IFASTAdapter> FASTAdapters { get; set; }

private void Compose()
{
    var catalog = new AggregateCatalog();
    catalog.Catalogs.Add(new DirectoryCatalog(AppSettingsUtil.GetString("FASTAdaptersLocation", Path.GetDirectoryName(Assembly.GetAssembly(typeof(ControllerServer)).Location))));
    var container = new CompositionContainer(catalog);
    container.ComposeParts(this);
}

The plugin

namespace FAST.DevIS.ControllerAdapter
{
   [Export (typeof(IFASTAdapter))]
   public class ControllerAdapter : IFASTAdapter
   {
       ...
   }
}

The interface

namespace FAST.Common.FastAdapter
{
    public interface IFASTAdapter
    {
        /// Parse plan parameters
        /// 
        //Activator
        bool ParsePlan(PlansContainer plan);
        bool ActivatePlan();
        void Configure(string config);
    }
}

Solution

  • This may be more a problem with how you are using assemblies than with the MEF solution.

    You say:

    The logic in the 6 ControllerAdapters is equal.

    So is it the same DLL just copied 6 times to different plugin directories? If yes, then this is the problem.

    I modeled your approach and ran the some tests to prove what I thought. The code is effectively the same as yours and reads plugins from the subdirectories of bin/plugin directory of the server.

    Simple test using NUnit to exercise the server class library:

    [Test]
    public void Compose()
    {
        var server = new Server();
        server.Compose();
        Console.WriteLine("Plugins found: " + server.FASTAdapters.Count());
        Console.WriteLine();
        foreach (var adapter in server.FASTAdapters)
        {
            Console.WriteLine(adapter.GetType());
            Console.WriteLine(adapter.GetType().Assembly.FullName);
            Console.WriteLine(adapter.GetType().Assembly.CodeBase);
            Console.WriteLine();
        }
        Assert.Pass();
    }
    

    Test results for one plugin in place:

    Plugins found: 1
    
    AdapterPlugin.ControllerAdapter
    AdapterPlugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
    file:///C:/USERS/GARKIN/DOCUMENTS/VISUAL STUDIO 2012/PROJECTS/MEFADAPTERS/ADAPTERSERVER/BIN/DEBUG/PLUGINS/ADAPTER1/ADAPTERPLUGIN.DLL
    

    Test result for two plugins in place, using the same plugin assembly copied over to two distinct plugin directories (probably your case):

    Plugins found: 2
    
    AdapterPlugin.ControllerAdapter
    AdapterPlugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
    file:///C:/USERS/GARKIN/DOCUMENTS/VISUAL STUDIO 2012/PROJECTS/MEFADAPTERS/ADAPTERSERVER/BIN/DEBUG/PLUGINS/ADAPTER1/ADAPTERPLUGIN.DLL
    
    AdapterPlugin.ControllerAdapter
    AdapterPlugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
    file:///C:/USERS/GARKIN/DOCUMENTS/VISUAL STUDIO 2012/PROJECTS/MEFADAPTERS/ADAPTERSERVER/BIN/DEBUG/PLUGINS/ADAPTER1/ADAPTERPLUGIN.DLL
    

    You also get the exact same result if you give distinct names to those DLLs because effectively it is still the same assembly inside.

    Now I add the third plugin, but this time it is a different plugin assembly:

    Plugins found: 3
    
    AdapterPlugin.ControllerAdapter
    AdapterPlugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
    file:///C:/USERS/GARKIN/DOCUMENTS/VISUAL STUDIO 2012/PROJECTS/MEFADAPTERS/ADAPTERSERVER/BIN/DEBUG/PLUGINS/ADAPTER1/ADAPTERPLUGIN.DLL
    
    AdapterPlugin.ControllerAdapter
    AdapterPlugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
    file:///C:/USERS/GARKIN/DOCUMENTS/VISUAL STUDIO 2012/PROJECTS/MEFADAPTERS/ADAPTERSERVER/BIN/DEBUG/PLUGINS/ADAPTER1/ADAPTERPLUGIN.DLL
    
    AdapterPlugin2.ControllerAdapter
    AdapterPlugin2, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
    file:///C:/USERS/GARKIN/DOCUMENTS/VISUAL STUDIO 2012/PROJECTS/MEFADAPTERS/ADAPTERSERVER/BIN/DEBUG/PLUGINS/ADAPTER3/ADAPTERPLUGIN2.DLL
    

    The different assembly is of course found and identified correctly.

    So this all boils down to how the .NET runtime handles assembly loading, which is a complex and strictly defined process and works differently for strongly and weakly named assemblies. I recommend this article for a good explanation of the process: Assembly Load Contexts Subtleties.

    In this case, the same process is followed behind the scenes when using MEF:

    1. The .NET runtime finds the first weakly typed plugin assembly and loads it from that location and MEF does it's export processing.

    2. Then MEF tries to process the next plugin assembly it founds using the catalog, but the runtime sees the assembly with the same metadata already loaded. So it uses the already loaded one to look for exports and ends up instantiating the same type again. It does not touch the second DLL at all.

    There is no way the same assembly can be loaded more than once by the runtime. Which makes perfect sense when you think of it. Assembly is just bunch of types with their metadata and once loaded the types are available, no need to load them again.

    This may not be completely correct, but I hope it helps to explain where the problem lies and it should be clear that duplicating DLLs for this purpose is useless.

    Now regarding what you want to achieve. It seems that all you need is just to get multiple instances of the SAME adapter plugin to use them for different purposes, which has nothing to do with multiplying DLLs.

    To get multiple adapter instances you can define multiple imports with RequiredCreationPolicy set to CreationPolicy.NonShared in your server which MEF will accordingly instantiate for you:

    public class Server
    {
        [Import(RequiredCreationPolicy = CreationPolicy.NonShared)]
        public IFASTAdapter FirstAdapter { get; set; }
    
        [Import(RequiredCreationPolicy = CreationPolicy.NonShared)]
        public IFASTAdapter SecondAdapter { get; set; }
    
        // Other adapters ...
    
        public void Compose()
        {
            var catalog = new AggregateCatalog();
            var pluginsDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "plugins");
            foreach (string d in Directory.GetDirectories(pluginsDir))
            {
                catalog.Catalogs.Add(new DirectoryCatalog(d));
            }
            var container = new CompositionContainer(catalog);
            container.ComposeParts(this);
        }
    }
    

    Corresponding NUnit test to check that the adapters are instantiated and that they are different instances:

    [Test]
    public void Compose_MultipleAdapters_NonShared()
    {
        var server = new Server();
        server.Compose();
        Assert.That(server.FirstAdapter, Is.Not.Null);
        Assert.That(server.SecondAdapter, Is.Not.Null);
        Assert.That(server.FirstAdapter, Is.Not.SameAs(server.SecondAdapter));
    }
    

    If all of this will help you to some extent, we can also take look at how you want to configure what and how to instantiate using the app.config.