PP_RhuM
PP_RhuM

Reputation: 81

.NET assembly resolving with complexe architecture

hope someone can answer this question.

I have an application with this architecture:

ApiLibrary (class library) UsedLibrary version 2 (class library referenced by ApiLibrary)

Then, My ApiLibrary has a third parties plugins system. Developpers can create custom plugins referencing ApiLibrary and extending an abstract type named "AbstractPlugin". There is a specific folder (Plugins) in which users can put sub-folders, themselves containing dlls generated for plugins.

My API has a dedicated method to load these plugins, looping on all dlls files in this folders and using "Assembly.LoadFile(currentDll)". Then, it loops on all types from the assembly and tries to find types that extends from AbstractPlugin. All these types found are a plugin that can be used within the API.

Plugins should not include the output of ApiLibrary in folders where they are placed (requirement specified to developpers). To ensure my API is effectively resolved when calling plugins functions, I handled the AppDomain.CurrentDomain.AssemblyResolve event and returns the executing assembly. But they can include in their folders dlls of other libraries.

The problem is that now, I have a plugin that actually needs to reference "UsedLibrary", but in version 1. Then, if within my ApiLibrary a function from the UsedLibrary is called before plugins are loaded, the version 2 is loaded and the plugin won't work because it needs the version 1. Moreover, if plugins are loaded before, the version 1 is loaded and my API can't use functions from the v2.

In fact, I simplified the problem because it's really more complicated as UsedLibrary dynamically loads itself unmanaged libraries placed on the main folder of my API and the plugin should load unmanaged libraries from its own folder.

I'd like to know if anyone has a solution to ensure that my plugin will be able to call functions from the v1 and my API will call functions from the v2 (I can't rename these assemblies).

Thank you very much.

EDIT 1:

I tried to load DLLs in different application domains for each plugin folder but after many tries, could not finally get my assemblies. How can I load my assemblies in different appdomains using this kind of code:

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

Thank you very much.

Edit 2:

I finally understood how to comunicate between AppDomains and could load assemblies and find plugins in them, but I still have a problem.

Plugins are loaded by my API (a class library) though a PluginsManager object:

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

The PluginLoader object that help to retrieve plugins is described as it:

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

I noticed that to ensure the communication, all objects that can be passed between the current domain and plugins domains must extends the MarshalByRefObject clas (or has the Serializable attribute but I want to communicate between AppDomains and not copying objects in current domain). Its OK for this, I ensured that all needed objects extend MarshalByRefObject type.

This API is referenced by the user application (another visual studio project) that calls a PluginsManager to load all plugins and iterate on loaded plugins.

It can successfully load and iterate all plugins, but in the AbstractPlugin type, I have specific events that must be handled by the application. When creating a handle on these events, I have a SerializationException...

Here is the plugins controller in my application:

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

So, the PluginsController class must also extend the MarshalByRefObject class because methods added to events must be sent through the proxy.

I have many other problems because of the complexe architecture of my API and some functions can't be used. If I load my plugins in the current domain, everything works but there may be problems between assemblies version. I'm thinking that I won't be able to perform what I want... So I will only ensure that my API has all its dependencies loaded before plugins are loaded and plugins may not work if an assembly is not compatible with the previous version that the plugin uses.

If anyone has any other suggestion...

Thank you.

Upvotes: 0

Views: 169

Answers (1)

xan2063
xan2063

Reputation: 95

You could put each plugin in it's own app domain and load your dlls into that app domain. See here for an example

Other solutions could be:

  • Aplly strong naming to the assembly and install the UsedLibrary in both versions to the GAC, but this needs to be done for every installation and I don't think you have controll over the libraries loaded
  • If the dll-versions are compatible, you could use an assembly-redirect to always use the newer one of both dlls

Upvotes: 2

Related Questions