Search code examples
c#dependency-injectionsimple-injector.net-4.8assembly-loading

Simple Injector - Registering plugins dynamically with constructor values from config


According to the documentation of Simple Injector one can use the following code to load assemblies dynamically

string pluginDirectory =
    Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins");

var pluginAssemblies =
    from file in new DirectoryInfo(pluginDirectory).GetFiles()
    where file.Extension.ToLower() == ".dll"
    select Assembly.Load(AssemblyName.GetAssemblyName(file.FullName));

container.Collection.Register<IPlugin>(pluginAssemblies);

Let's say each plugin has its properties (name, language, ...) which are specifed in App.Config in the following manner

<Plugins>
    <Element name="SamplePlugin" filename="SamplePlugin.dll" language="pl-PL" modeId="1" />
</Plugins>

These values are necessary as they are used in constructor of every plugin.

Currently there is a PluginSettings class which gets ConfigurationElements looping through a ConfigurationElementCollection and later on in the main ViewModel there is a loop going through the collection contained in the mentioned settings.

public class PluginSettings
{
    readonly Configuration _config = ConfigurationManager
        .OpenExeConfiguration(ConfigurationUserLevel.None);

    public LoaderSection PluginAppearanceConfiguration
    {
        get
        {
            return (LoaderSection)_config.GetSection("pluginSection");
        }
    }

    public PluginsCollection PluginsCollection
    {
        get
        {
            return PluginAppearanceConfiguration.PluginElement;
        }
    }

    public IEnumerable<PluginElement> PluginElements
    {
        get
        {
            foreach (PluginElement selement in PluginsCollection)
            {
                if (selement != null)
                    yield return selement;
            }
        }
    }
}

Current loading plugins in ViewModel

try
{
    var factory = ModuleLoader
        .LoadPluginFactory<PluginFactoryBase>(Path.Combine(pluginsPath, plug.Filename));
    var configAppSettings = new ConfigAppSettings(plug.FactoryId, plug.ModeId, plug.Language);

    Application.Current.Dispatcher.BeginInvoke((Action)delegate
    {
        plugins.Add(new Plugin(plug.Name, factory,
            configSettings, configAppSettings));
    });
}
catch (ReflectionTypeLoadException ex)
{
    foreach (var item in ex.LoaderExceptions)
    {
        MessageBox.Show(item.Message, "Loader exception",
            MessageBoxButton.OK, MessageBoxImage.Error);
    }
}

Each plugin has its own PluginFactory defined. Code is a few years old and wasn't written by anyone still present. Further refactoring would include making common Factory in the main application as I don't see reasons for the current state. Plugins are listed in the main app and are initialized (by plugins PluginFactory) only when the specific one is selected (clicked) and then displayed.

How to adapt the code to follow good etiquette and process plugins in composition root and inject them?


Solution

  • This question is hard to answer, because it depends on what the ViewModel needs to do with this information.

    When it comes to Simple Injector, however, all plugins that you might use during the execution of your application need to be registered up front. So, that means that if the ViewModel decides to only use 2 out of 5 defined plugins, you still need to register all 5. (* see note below)

    But it likely means you need to do a few things:

    1. Load the plugin information from the config
    2. Use this plugin information to register the plugins in a collection in the order they are listed
    3. Supply the plugin information and an IEnumerable<IPlugin> to the ViewModel.
    4. Let the ViewModel pick the proper plugin from the collection based on the plugin information and the runtime data given at that time

    For instance, if the ViewModel requires the plugin with language pl-PL, it needs to find out what the index is of the pl-PL plugin from the plugin information and use that index to get that plugin from the IEnumerable<IPlugin>, e.g.:

    var info = pluginInformation.First(p => p.Language == "pl-PL");
    int index = pluginInformation.IndexOf(info);
    IPlugin plugin = plugins.ElementAt(index);
    plugin.DoAwesomeStuff();
    

    Note that with Simple Injector, a call to ElementAt on an injected IEnumerable<T> will be an optimized O(1) operation. So even if the collection contains hundreds of plugins, only one plugin will be created at that time.

    *This isn't completely true, because you could load and register plugins just-in-time, but this is an advanced topic which you likely don't need.