Simple Injector – 使用配置中的构造函数值动态注册插件

huangapple go评论62阅读模式
英文:

Simple Injector - Registering plugins dynamically with constructor values from config

问题

根据Simple Injector文档,可以使用以下代码动态加载程序集:

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);

假设每个插件在App.Config中以以下方式指定其属性(名称、语言等):

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

这些值在每个插件的构造函数中使用,是必需的。

当前有一个PluginSettings类,它通过ConfigurationElementCollection循环获取ConfigurationElements,然后在主ViewModel中循环遍历包含在上述设置中的集合。

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;
            }
        }
    }
}

当前在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);
    }
}

每个插件都有自己定义的PluginFactory。代码几年前编写,并且不再由任何在场人员编写。进一步的重构将包括在主应用程序中创建一个通用的Factory,因为我看不到当前状态的理由。插件在主应用程序中列出,并且只有在选择(点击)特定插件时才会被初始化(由插件的PluginFactory)然后显示。

如何调整代码以遵循良好的规范,并在组合根中处理和注入插件?

英文:

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

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

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

container.Collection.Register&lt;IPlugin&gt;(pluginAssemblies);

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

&lt;Plugins&gt;
	&lt;Element name=&quot;SamplePlugin&quot; filename=&quot;SamplePlugin.dll&quot; language=&quot;pl-PL&quot; modeId=&quot;1&quot; /&gt;
&lt;/Plugins&gt;

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(&quot;pluginSection&quot;);
        }
    }

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

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

Current loading plugins in ViewModel

try
{
    var factory = ModuleLoader
        .LoadPluginFactory&lt;PluginFactoryBase&gt;(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, &quot;Loader exception&quot;,
            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?

答案1

得分: 1

这个问题很难回答,因为它取决于ViewModel需要如何处理这些信息。

然而,就Simple Injector而言,你在应用程序执行期间可能使用的所有插件都需要提前注册。这意味着,如果ViewModel决定只使用定义的5个插件中的2个,你仍然需要注册所有5个。 (*请参见下面的注释)

但这可能意味着你需要做一些事情:

  1. 从配置中加载插件信息。
  2. 使用这些插件信息按其列出的顺序注册插件到一个集合中。
  3. 向ViewModel提供插件信息和一个IEnumerable<IPlugin>
  4. 让ViewModel根据插件信息和当时给定的运行时数据从集合中选择合适的插件。

例如,如果ViewModel需要具有语言pl-PL的插件,它需要找出pl-PL插件在插件信息中的索引,然后使用该索引从IEnumerable<IPlugin>中获取该插件,如下所示:

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

请注意,使用Simple Injector,在注入的IEnumerable<T>上调用ElementAt将是一个经过优化的O(1)操作。因此,即使集合包含数百个插件,也仅会在那个时候创建一个插件。

这并不完全正确,因为你可以按需加载和注册插件,但这是一个高级主题,你可能不需要。

英文:

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&lt;IPlugin&gt; 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&lt;IPlugin&gt;, e.g.:

var info = pluginInformation.First(p =&gt; p.Language == &quot;pl-PL&quot;);
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&lt;T&gt; 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.</superscript>

huangapple
  • 本文由 发表于 2023年2月8日 20:40:08
  • 转载请务必保留本文链接:https://go.coder-hub.com/75385923.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定