0

誰かがこの質問に答えてくれることを願っています。

このアーキテクチャのアプリケーションがあります:

ApiLibrary (クラス ライブラリ) UsedLibrary バージョン 2 (ApiLibrary によって参照されるクラス ライブラリ)

次に、My ApiLibrary にはサードパーティのプラグイン システムがあります。開発者は、ApiLibrary を参照し、「AbstractPlugin」という名前の抽象型を拡張するカスタム プラグインを作成できます。ユーザーがサブフォルダーを置くことができる特定のフォルダー (Plugins) があり、それ自体にプラグイン用に生成された dll が含まれています。

私の API には、これらのプラグインをロードするための専用のメソッドがあり、このフォルダー内のすべての dll ファイルをループし、「Assembly.LoadFile(currentDll)」を使用します。次に、アセンブリのすべての型をループし、AbstractPlugin から拡張された型を見つけようとします。見つかったこれらのタイプはすべて、API 内で使用できるプラグインです。

プラグインは、それらが配置されているフォルダーに ApiLibrary の出力を含めるべきではありません (開発者に指定された要件)。プラグイン関数を呼び出すときに API が効果的に解決されるようにするために、AppDomain.CurrentDomain.AssemblyResolve イベントを処理し、実行中のアセンブリを返します。ただし、他のライブラリの dll をフォルダーに含めることができます。

問題は、現在、実際に「UsedLibrary」を参照する必要があるプラグインがありますが、バージョン 1 であるということです。次に、ApiLibrary 内で、プラグインが読み込まれる前に UsedLibrary の関数が呼び出されると、バージョン 2 が読み込まれ、プラグインがバージョン 1 が必要なため、機能しません。さらに、プラグインが以前にロードされている場合、バージョン 1 がロードされ、API は v2 の関数を使用できません。

実際、UsedLibrary は API のメイン フォルダーに配置された管理されていないライブラリ自体を動的に読み込み、プラグインは独自のフォルダーから管理されていないライブラリを読み込む必要があるため、問題を単純化しました。

プラグインが v1 から関数を呼び出せるようにし、API が v2 から関数を呼び出せるようにするためのソリューションを誰かが持っているかどうか知りたいです (これらのアセンブリの名前を変更することはできません)。

どうもありがとうございました。

編集1:

プラグイン フォルダーごとに異なるアプリケーション ドメインに DLL を読み込もうとしましたが、何度も試行した後、最終的にアセンブリを取得できませんでした。この種のコードを使用して、アセンブリを異なるアプリドメインにロードするにはどうすればよいですか:

loadedAssemblies = new Dictionary<string, Assembly>();

UriBuilder uri = new UriBuilder(Assembly.GetExecutingAssembly().CodeBase);
string basePath = Path.GetDirectoryName(Uri.UnescapeDataString(uri.Path));

foreach (string fullPluginPath in Directory.EnumerateDirectories(PLUGINS_PATH))
{
    string pluginFolder = Path.GetFileName(fullPluginPath);

    AppDomainSetup setup = new AppDomainSetup();
    setup.ApplicationName = pluginFolder;
    setup.ApplicationBase = basePath;
    setup.PrivateBinPath = fullPluginPath;

    System.Security.PermissionSet permissionSet = new System.Security.PermissionSet(System.Security.Permissions.PermissionState.Unrestricted);

    AppDomain pluginAppDomain = AppDomain.CreateDomain(pluginFolder, null, setup, permissionSet);

    foreach (string fileName in Directory.EnumerateFiles(fullPluginPath))
    {
        if (Path.GetExtension(fileName.ToLower()) == ".dll")
        {
            try
            {
                Assembly currentAssembly = ??; // How to load the assembly within the plugin app domain ???
                loadedAssemblies.Add(currentAssembly.FullName, currentAssembly);
            }
            catch (Exception e)
            {
                // DLL could not be loaded
            }
        }
    }
}

どうもありがとうございました。

編集2:

AppDomains 間で通信する方法をようやく理解し、アセンブリを読み込んでプラグインを見つけることができましたが、まだ問題があります。

プラグインは、PluginsManager オブジェクトを介して API (クラス ライブラリ) によって読み込まれます。

/// <summary>
/// A plugin manager can be used by the holding application using the API to gain access to plugins installed by the user.
/// All errors detected during plugins loading are stored, so applications may know when a plugin could not be loaded.
/// </summary>
public class PluginsManager : MarshalByRefObject
{
    /// <summary>
    /// Name of the plugins folder.
    /// </summary>
    private const string PLUGIN_FOLDER = "ApiPlugins";

    #region Fields

    /// <summary>
    /// Plugins loaded and initialised without errors
    /// </summary>
    private List<AbstractPlugin> loadedPlugins;

    /// <summary>
    /// Dictionary of errors detected during DLL parsings.
    /// </summary>
    private Dictionary<string, Exception> dllLoadException;

    /// <summary>
    /// Dictionary of errors detected during assemblies types parsing.
    /// </summary>
    private Dictionary<string, Exception> assembliesTypesLoadExceptions;

    /// <summary>
    /// Dictionary of errors detected during plugins instance creation.
    /// </summary>
    private Dictionary<string, Exception> pluginsConstructionExceptions;

    /// <summary>
    /// Dictionary of errors detected during plugins instance creation.
    /// </summary>
    private Dictionary<string, Exception> pluginsRetrievalExceptions;

    /// <summary>
    /// Dictionary of errors detected during plugins initialisation.
    /// </summary>
    private Dictionary<string, Exception> pluginsInitialisationExceptions;

    /// <summary>
    /// The currently loaded DLL during plugins reload.
    /// Used to resolve assembly in the current domain when loading an assembly containing IDM-CIC plugins.
    /// </summary>
    private static string currentlyLoadedDll = null;

    #endregion

    #region Methods

    public void LoadPlugins()
    {
        // Ensures assemblies containing plugins will be loaded in the current domain
        AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;

        try
        {
            List<PluginLoader> pluginLoaders = new List<PluginLoader>();

            loadedAssemblies = new Dictionary<string, Assembly>();
            loadedPlugins = new List<AbstractPlugin>();
            dllLoadException = new Dictionary<string, Exception>();
            assembliesTypesLoadExceptions = new Dictionary<string, Exception>();
            pluginsInitialisationExceptions = new Dictionary<string, Exception>();
            pluginsConstructionExceptions = new Dictionary<string, Exception>();
            pluginsRetrievalExceptions = new Dictionary<string, Exception>();

            string pluginsFolderPath = Path.Combine("C:", PLUGIN_FOLDER);

            UriBuilder uri = new UriBuilder(Assembly.GetExecutingAssembly().CodeBase);
            string basePath = Path.GetDirectoryName(Uri.UnescapeDataString(uri.Path));

            // detect automatically dll files in plugins folder and load them.
            if (Directory.Exists(pluginsFolderPath))
            {
                foreach (string pluginPath in Directory.EnumerateDirectories(pluginsFolderPath))
                {
                    string pluginFolderName = Path.GetFileName(pluginPath);

                    AppDomainSetup setup = new AppDomainSetup();
                    setup.ApplicationName = pluginFolderName;
                    setup.ApplicationBase = basePath;
                    setup.PrivateBinPath = pluginPath;

                    PermissionSet permissionSet = new PermissionSet(PermissionState.Unrestricted);

                    AppDomain pluginAppDomain = AppDomain.CreateDomain(pluginFolderName, AppDomain.CurrentDomain.Evidence, setup, permissionSet);

                    foreach (string dllFile in Directory.EnumerateFiles(pluginPath, "*.dll", SearchOption.TopDirectoryOnly))
                    {
                        try
                        {
                            currentlyLoadedDll = dllFile;

                            PluginLoader plugLoader = (PluginLoader)pluginAppDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, typeof(PluginLoader).FullName);

                            Assembly ass = plugLoader.LoadAssemblyIfItContainsPlugin(dllFile);

                            if (ass != null)
                            {
                                pluginLoaders.Add(plugLoader);
                            }

                            // Check types parsing exceptions and store them
                            if (plugLoader.CaughtExceptionOnTypesParsing != null)
                            {
                                assembliesTypesLoadExceptions.Add(plugLoader.LoadedAssemblyName, plugLoader.CaughtExceptionOnTypesParsing);
                            }
                        }
                        catch (Exception e)
                        {
                            // Store problem while loading a DLL
                            dllLoadException.Add(dllFile, e);
                        }
                    }
                }
            }

            foreach (PluginLoader plugLoader in pluginLoaders)
            {
                // Load all plugins of the loaded assembly
                plugLoader.LoadAllPlugins();

                // Check plugins construction errors and store them
                foreach (KeyValuePair<Type, Exception> kvp in plugLoader.CaughtExceptionOnPluginsCreation)
                {
                    Type type = kvp.Key;
                    Exception e = kvp.Value;

                    pluginsConstructionExceptions.Add(type.Name + " from " + plugLoader.LoadedAssemblyName, e);
                }

                for (int i = 0; i < plugLoader.GetPluginsCount(); i++)
                {
                    AbstractPlugin plugin = null;

                    try
                    {
                        // Try to retrieve the plugin in our context (should be OK because AbstractPlugin extends MarshalByRefObject)
                        plugin = plugLoader.GetPlugin(i);
                    }
                    catch (Exception e)
                    {
                        // Store the retrieval error
                        pluginsRetrievalExceptions.Add(plugLoader.GetPluginName(i) + " from " + plugLoader.LoadedAssemblyName, e);
                    }

                    if (plugin != null)
                    {
                        try
                        {
                            // Initialise the plugin through the exposed method in AbstractPlugin type and that can be overridden in plugins
                            plugin.Initialise();
                            loadedPlugins.Add(plugin);
                        }
                        catch (Exception e)
                        {
                            // Store the initialisation error
                            pluginsInitialisationExceptions.Add(plugin.GetType().Name, e);
                        }
                    }
                }
            }
        }
        finally
        {
            AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve;
        }
    }


    /// <summary>
    /// Ensure plugins assemblies are loaded also in the current domain
    /// </summary>
    /// <param name="sender">Sender of the event</param>
    /// <param name="args">Arguments for assembly resolving</param>
    /// <returns>The resolved assembly or null if not found (will result in a dependency error)</returns>
    private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
    {
        AssemblyName assemblyName = new AssemblyName(args.Name);

        if (args.RequestingAssembly == null && assemblyName.Name.ToLower() == Path.GetFileNameWithoutExtension(currentlyLoadedDll).ToLower())
        {
            return Assembly.LoadFrom(currentlyLoadedDll);
        }

        return null;
    }

    /// <summary>
    /// Enumerates all plugins loaded and initialised without error.
    /// </summary>
    /// <returns>Enumeration of AbstractPlugin</returns>
    public IEnumerable<AbstractPlugin> GetPlugins()
    {
        return loadedPlugins;
    }

    #endregion
}

プラグインの取得に役立つ PluginLoader オブジェクトは、次のように記述されています。

/// <summary>
/// This class is internally used by the PluginsManager to load an assembly though a DLL file and load an instance of each plugins that can be found within this assembly.
/// </summary>
internal class PluginLoader : MarshalByRefObject
{
    #region Fields

    /// <summary>
    /// The assembly loaded within this plugin loader. 
    /// Null if could not be loaded or does not contains any plugin.
    /// </summary>
    private Assembly loadedAssembly = null;

    /// <summary>
    /// Exception caught when trying to parse assembly types.
    /// Null if GetTypes() was successfull.
    /// </summary>
    private Exception caughtExceptionOnTypesParsing = null;

    /// <summary>
    /// Dictionary of exceptions caught when trying ti instantiate plugins.
    /// The key is the plugin type and the value is the exception.
    /// </summary>
    private Dictionary<Type, Exception> caughtExceptionOnPluginsCreation = new Dictionary<Type, Exception>();

    /// <summary>
    /// The list of loaded plugins that is filled when calling the LoadAllPlugins method.
    /// </summary>
    private List<AbstractPlugin> loadedPlugins = new List<AbstractPlugin>();

    #endregion

    #region Accessors

    /// <summary>
    /// Gets the loaded assembly name if so.
    /// </summary>
    public string LoadedAssemblyName
    {
        get { return loadedAssembly != null ? loadedAssembly.FullName : null; }
    }

    /// <summary>
    /// Gets the exception caught when trying to parse assembly types.
    /// Null if GetTypes() was successfull.
    /// </summary>
    public Exception CaughtExceptionOnTypesParsing
    {
        get { return caughtExceptionOnTypesParsing; }
    }

    /// <summary>
    /// Gets an enumeration of exceptions caught when trying ti instantiate plugins.
    /// The key is the plugin type and the value is the exception.
    /// </summary>
    public IEnumerable<KeyValuePair<Type, Exception>> CaughtExceptionOnPluginsCreation
    {
        get { return caughtExceptionOnPluginsCreation; }
    }

    #endregion

    #region Methods

    /// <summary>
    /// Loads an assembly through a DLL path and returns it only if it contains at least one plugin.
    /// </summary>
    /// <param name="assemblyPath">The path to the assembly file</param>
    /// <returns>An assembly or null</returns>
    public Assembly LoadAssemblyIfItContainsPlugin(string assemblyPath)
    {
        // Load the assembly
        Assembly assembly = Assembly.LoadFrom(assemblyPath);

        IEnumerable<Type> types = null;

        try
        {
            types = assembly.GetTypes();
        }
        catch (Exception e)
        {
            // Could not retrieve types. Store the exception
            caughtExceptionOnTypesParsing = e;
        }

        if (types != null)
        {
            foreach (Type t in types)
            {
                if (!t.IsAbstract && t.IsSubclassOf(typeof(AbstractPlugin)))
                {
                    // There is a plugin. Store the loaded assembly and return it.
                    loadedAssembly = assembly;
                    return loadedAssembly;
                }
            }
        }

        // No assembly to return
        return null;
    }

    /// <summary>
    /// Load all plugins that can be found within the assembly.
    /// </summary>
    public void LoadAllPlugins()
    {
        if (caughtExceptionOnTypesParsing == null)
        {
            foreach (Type t in loadedAssembly.GetTypes())
            {
                if (!t.IsAbstract && t.IsSubclassOf(typeof(AbstractPlugin)))
                {
                    AbstractPlugin plugin = null;

                    try
                    {
                        plugin = (AbstractPlugin)Activator.CreateInstance(t);
                    }
                    catch (Exception e)
                    {
                        caughtExceptionOnPluginsCreation.Add(t, e);
                    }

                    if (plugin != null)
                    {
                        loadedPlugins.Add(plugin);
                    }
                }
            }
        }
    }

    /// <summary>
    /// Returns the number of loaded plugins.
    /// </summary>
    /// <returns>The number of loaded plugins</returns>
    public int GetPluginsCount()
    {
        return loadedPlugins.Count;
    }

    /// <summary>
    /// Returns a plugin name from its index in the list of loaded plugins.
    /// </summary>
    /// <param name="index">The index to search</param>
    /// <returns>The name of the corresponding plugin</returns>
    public string GetPluginName(int index)
    {
        return loadedPlugins[index].Name;
    }

    /// <summary>
    /// Returns a plugin given its index in the list of loaded plugins.
    /// </summary>
    /// <param name="index">The index to search</param>
    /// <returns>The loaded plugin as AbstractPlugin</returns>
    public AbstractPlugin GetPlugin(int index)
    {
        return loadedPlugins[index];
    }

    #endregion
}

通信を確実にするために、現在のドメインとプラグイン ドメインの間で受け渡すことができるすべてのオブジェクトは、MarshalByRefObject クラスを拡張する必要があることに気付きました (または、Serializable 属性を持っていますが、AppDomains 間で通信し、現在のドメインにオブジェクトをコピーしたくありません)。これで問題ありません。必要なすべてのオブジェクトが MarshalByRefObject 型を拡張していることを確認しました。

この API は、PluginsManager を呼び出してすべてのプラグインを読み込み、読み込まれたプラグインを反復処理するユーザー アプリケーション (別のビジュアル スタジオ プロジェクト) によって参照されます。

すべてのプラグインを正常にロードして反復できますが、AbstractPlugin タイプには、アプリケーションで処理する必要がある特定のイベントがあります。これらのイベントのハンドルを作成すると、SerializationException が発生します...

私のアプリケーションのプラグインコントローラーは次のとおりです。

/// <summary>
/// This controller helps to load all plugins and display their controls within the application
/// </summary>
internal class PluginsController
{
    /// <summary>
    /// A plugins manager that helps to retrieve plugins
    /// </summary>
    private PluginsManager pluginsManager = new PluginsManager();

    /// <summary>
    /// Initialise the list of available plugins and create all needed controls to the application
    /// </summary>
    internal void InitialisePlugins()
    {
        pluginsManager.LoadPlugins();

        foreach (AbstractPlugin plugin in pluginsManager.GetPlugins())
        {
            // Treat my plugin data, adding controls to the application
            // ...

            // Handle events on the plugin : EXCEPTION
            plugin.OnControlAdded += plugin_OnControlAdded;
            plugin.OnControlChanged += plugin_OnControlChanged;
            plugin.OnControlRemoved += plugin_OnControlRemoved;
        }
    }

    void plugin_OnControlAdded(AbstractPlugin plugin, PluginControl addedControl)
    {
        // Handle control added by the plugin and add the new control to the application
    }

    void plugin_OnControlChanged(AbstractPlugin plugin, PluginControl changedControl)
    {
        // Handle control changed by the plugin and updates the concerned control in the application
    }

    void plugin_OnControlRemoved(AbstractPlugin plugin, PluginControl removedControl)
    {
        // Handle control removed by the plugin and remove the control from the application
    }
}

そのため、PluginsController クラスは MarshalByRefObject クラスも拡張する必要があります。これは、イベントに追加されたメソッドがプロキシ経由で送信される必要があるためです。

API の複雑なアーキテクチャが原因で、他にも多くの問題があり、一部の機能が使用できません。現在のドメインにプラグインをロードすると、すべてが機能しますが、アセンブリのバージョン間で問題が発生する可能性があります。私は自分がやりたいことを実行できないと思っています...そのため、プラグインが読み込まれる前に API にすべての依存関係が読み込まれていることを確認するだけで、アセンブリが以前のものと互換性がない場合、プラグインが機能しない可能性がありますプラグインが使用するバージョン。

誰か他の提案があれば...

ありがとうございました。

4

1 に答える 1