locked
Appdomain for assembly loading (plugins) help RRS feed

  • Question

  • Thanks for looking at my question.

    The issue I seem to be having is transferring this code into something that creates each plugin on a new appdomain so I can unload and load at will. I have very little experience with AppDomains, but I have attempted some code, also below. Therefore my question is, how do I load each assembly on its on AppDomain, and how do I create a running list of the plugins?

    Code 1:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.IO;
    using System.Reflection;
    
    namespace SYSAdmin
    {
    	public class PluginLoader
    	{
    		public static List<IPlugin> Plugins { get; set; }
    
    		public void LoadPlugins()
    		{
    			Plugins = new List<IPlugin>();
    
    			//Load the DLLs from the Plugins directory
    			if (Directory.Exists(Constants.FolderName))
    			{
    				string[] files = Directory.GetFiles(Constants.FolderName);
    				foreach (string file in files)
    				{
    					if (file.EndsWith(".dll"))
    					{
    						Assembly.LoadFile(Path.GetFullPath(file));
    						//Assembly.Load(File.ReadAllBytes(Path.GetFullPath(file)));
    					}
    				}
    			}
    
    			Type interfaceType = typeof(IPlugin);
    			//Fetch all types that implement the interface IPlugin and are a class
    			Type[] types = AppDomain.CurrentDomain.GetAssemblies()
    				.SelectMany(a => a.GetTypes())
    				.Where(p => interfaceType.IsAssignableFrom(p) && p.IsClass)
    				.ToArray();
    			foreach (Type type in types)
    			{
    				//Create a new instance of all found types
    				Plugins.Add((IPlugin)Activator.CreateInstance(type));
    			}
    		}
    	}
    }


    Code 2 Experimental Code:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.IO;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace SYSAdmin
    {
        public class test
        {
    		public static List<IPlugin> Plugins { get; set; }
    		public void xmark()
            {
    			Plugins = new List<IPlugin>();
    			if (Directory.Exists(Constants.FolderName))
    			{
    				string[] files = Directory.GetFiles(Constants.FolderName);
    				foreach (string file in files)
    				{
    					if (file == "TestPlug.dll")
    					{
    						AppDomain ad2 = AppDomain.CreateDomain(file);
    						ad2.Load(Path.GetFullPath(file));
    					}
    				}
    			}
    		}
        }
    }


    IPlugin Code:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace SYSAdmin
    {
    	public interface IPlugin
    	{
    		string Name { get; }
    		string Explanation { get; }
    		void Go(string parameters);
    	}
    }


    TestPlug.dll Code:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using SYSAdmin;
    using System.Windows.Forms;
    
    namespace TestPlugin
    {
    	public class TestPlug : IPlugin
    	{
    		public string Explanation
    		{
    			get
    			{
    				return "Tests functionality of the assembly reference.";
    			}
    		}
    
    		public string Name
    		{
    			get
    			{
    				return "TestPlug";
    			}
    		}
    
    		public void Go(string parameters)
    		{
    			string line = parameters;
    			SYSAdmin.Program.form1.output.AppendText(Environment.NewLine);
    			SYSAdmin.Program.form1.output.AppendText(line);
    
    		}
    	}
    }

    As of right now I have a working plugin system but what I've found states you cannot unload an assembly from a running domain. Therefore each plugin having its own app domain would be ideal as then I can load or unload at will.

    -K7

    Thursday, April 30, 2020 11:25 AM

Answers

  • I understand your goal of unloading but how often will your users do that? If they will rarely do it you're going to architect your software and add complexity and potential issues for a boundary case. Even VS doesn't bother doing that. It simply isn't worth the effort.

    But let's break up the "unload" concept. I suspect your app is probably not going to be running for a long time so do you really need to release the memory? If you want to support an "unload" concept then don't unload the plugin but simply mark it as unused via your API. You can require that plugins support an "unload" event. When this occurs the plugin should clean itself up. To unload the plugin you would call its method. Then in your plugin manager you would keep the list of "unloaded" plugins separate. If the plugin is requested to be loaded again you can simply load it again without reloading the assembly.

    If you need to replace a plugin then you'll have to do some trickier because the assembly cannot be loaded more than once in a domain. So you'll have to do something like ASP.NET does and rather than loading an assembly directly you'll need to shadow copy the assembly (and potentially all its dependencies). Then you can load a single assembly multiple times (by path). Your plugin manager would be responsible for this. To unload you'd simply throw out of memory all the plugin info but the assembly(ies) would still be loaded. When the plugin is loaded the next time (whether existing or new) then the manager would load it again. 

    But to your question, the only way to unload assemblies is to use an appdomain (which won't work with .NET Core by the way). And that means you need to move all your shared functionality into a marshalable assembly. It also means your app nor plugins can use static members because those are per domain. The complexity is going to get high. Do you really want to go this route? It is going to dramatically impact the complexity and performance of your code. But appdomains are the only way to solve this problem.

    The existing plugin manager code you posted isn't actually that useful. You are loading each plugin into memory when you load its assembly (note you could load into reflection context to speed things up as that is what it is for). Again a plugin metadata file would resolve this easier.

    After you've loaded all the assemblies you enumerate through again looking for plugins. So this code is always loading every assembly and all the plugins. Even using appdomains isn't going to help this case. If you absolutely have no choice but to use reflection to find plugins (because you don't want to go the metadata route) then you need to do that in a separate appdomain. Once you're done unload the domain. However that also means you cannot preload the IPlugin objects because, again, you're loading the plugins. Instead you need to capture the basic plugin data you need (name, type, etc) such that when your app actually needs to load the plugin it can.

    MEF already supports what you're doing here. I recommend you look at MEF and specifically its DirectoryCatalog which reduces the amount of code you wrote down to a couple of lines. 


    Michael Taylor http://www.michaeltaylorp3.net

    • Marked as answer by k7s41gx Thursday, April 30, 2020 2:48 PM
    Thursday, April 30, 2020 2:32 PM

All replies

  • Correct you cannot unload an assembly once it is loaded (in .NET Framework). Using separate appdomains for each plugin would be the only approach. However appdomains add complexity to your app and have some disadvantages that you need to weight carefully before you go this route.

    1. Appdomains require more resources.

    2. Calls between domains is slower than direct calls.

    3. All data that must be shared has to be marshalable (meaning deriving from the right base type) which can be problematic if you use lots of third party types.

    4. Each appdomain has its own copy of all static data. Therefore if you're using singletons or relying on static data for caching or anything it is per domain.

    Complex tools like VS use multiple appdomains but, having not looked in a while, the initial effort seriously impacted performance. Now I believe VS shares domains for some plugins. I think you need to ask yourself why you're using domains. If it is for isolation and whatnot then maybe consider wrapping plugins in a wrapper instead. If it is so you can unload a plugin then is this really a feature you need such that architecting your entire app around it makes sense? Even in the case of VS when you "unload" a plugin it is still in memory. It doesn't get unloaded/uninstalled until you shut down VS. It is probably better to add "unloading" to the plugin interface instead. Whether the assemblies stay in memory or not is mostly irrelevant if you implement the functionality in the interface. Then you don't need domains. Again, in the case of VS, installing a plugin is instantenous. But if you try to update a plugin that is already running then you have to restart VS. This is a reasonable tradeoff and matches how assemblies are loaded.

    If you're really interested in building a plugin system I don't recommend you write your own. .NET already ships with 2. MEF is what VS uses. MAF is the other side of the fence. There are other frameworks as well such as Prism.

    If you really want to do this yourself then the second code block looks reasonable. You'll have to load each assembly into its own appdomain. But you're going to need to match the domain so you'll likely start with AppDomainSetup. The reason why this is important is because your plugins need to load their dependent assemblies in addition to the required app libraries. In your architecture you're going to need to store each plugin in a separate folder. If you don't do that then you're going to run into issues. For example plugin A is built against library Lv1 while plugin B is built against library Lv2. Only a single version can be in the app folder so if both plugins are placed in the same folder one is using an older version and one the new and, depending on which writes last, they potentially be using the wrong version. So plugins in separate directories which is where appdomain points but if it needs an app assembly you need to look into the app root folder as well. So you'll also need to set up assembly resolving code.

    Have you tried your second code block and found it doesn't work? If so what error(s) are you getting. Is MEF/MAF an option? The documentation for them is good.


    Michael Taylor http://www.michaeltaylorp3.net

    Thursday, April 30, 2020 1:30 PM
  • CoolDadTx,

    Thanks for your reply. Code block 2 does not have an issue running, however referencing what is needed from the plugin is something I am having an issue figuring out.

    I am curious about your wrapper idea. Basically the plugins only need to load when called by name. They then return a value and post it to form1's rich text box (output). They do not have to be loaded all the time. Example, my weather plugin is weather.dll. Inside the application it loads weather.dll which adds a command weather. So for example if say I want the weather in zipcode 89108, I would input weather 89108. Again, if I could create a singular instance of said plugin and terminate when done, that would be a perfect scenario for the applications plugins.

    -K7

    Thursday, April 30, 2020 1:52 PM
  • I would recommend that you take a look at MEF then. It can do what you want (minus the unload which again is probably not worth the effort). I don't even know that you'd want to unload the plugin. Once loaded it seems like it can stick around in case it is invoked again. 

    As for the concept I'd recommend a plugin registration system (sort of like VS). This avoids the issue of having to load assemblies (even in a separate appdomain) just to discover what is available. Perhaps each plugin folder has a metadata file that specifies the plugin name, any info your app needs before the plugin is loaded and the fully qualified type to load. When your app needs to load a plugin it uses the fully qualified type name to load the assembly and type into memory. Then it creates an instance of the plugin and runs it. If this is a UI component then most likely your plugin interface would allow the plugin to then create a UI.

    To make this easier create a PluginManager class that stores all the plugin registrations. Also expose the ability to "get" a plugin. It is this method that is responsible for seeing if the plugin is already loaded. If not then it loads it (as mentioned above). In either case it returns the plugin so calling code can then work with the plugin. Note that MEF does a similar thing already.


    Michael Taylor http://www.michaeltaylorp3.net

    Thursday, April 30, 2020 2:07 PM
  • CoolDadTx,

    Thanks again for your response.

    The reason I am needing to unload the dll files is so I can update or add functionality to the dll file and reload it back into the main program. As of right now using the current plugin scope below, I am able to update the dll and restart the application to make changes, but im trying to get away from a complete restart of the program.

    Current plugin code:

    	public class PluginLoader
    	{
    		public static List<IPlugin> Plugins { get; set; }
    
    		public void LoadPlugins()
    		{
    			Plugins = new List<IPlugin>();
    
    			//Load the DLLs from the Plugins directory
    			if (Directory.Exists(Constants.FolderName))
    			{
    				string[] files = Directory.GetFiles(Constants.FolderName);
    				foreach (string file in files)
    				{
    					if (file.EndsWith(".dll"))
    					{
    						//Assembly.LoadFile(Path.GetFullPath(file));
    						Assembly.Load(File.ReadAllBytes(Path.GetFullPath(file)));
    					}
    				}
    			}
    
    			Type interfaceType = typeof(IPlugin);
    			//Fetch all types that implement the interface IPlugin and are a class
    			Type[] types = AppDomain.CurrentDomain.GetAssemblies()
    				.SelectMany(a => a.GetTypes())
    				.Where(p => interfaceType.IsAssignableFrom(p) && p.IsClass)
    				.ToArray();
    			foreach (Type type in types)
    			{
    				//Create a new instance of all found types
    				Plugins.Add((IPlugin)Activator.CreateInstance(type));
    			}
    		}
    	}

    By using the load instead of loadfile I can manipulate the original dll file. Is there anyway to "refresh" the current internal copy of the dll?

    -K7

    Thursday, April 30, 2020 2:14 PM
  • I understand your goal of unloading but how often will your users do that? If they will rarely do it you're going to architect your software and add complexity and potential issues for a boundary case. Even VS doesn't bother doing that. It simply isn't worth the effort.

    But let's break up the "unload" concept. I suspect your app is probably not going to be running for a long time so do you really need to release the memory? If you want to support an "unload" concept then don't unload the plugin but simply mark it as unused via your API. You can require that plugins support an "unload" event. When this occurs the plugin should clean itself up. To unload the plugin you would call its method. Then in your plugin manager you would keep the list of "unloaded" plugins separate. If the plugin is requested to be loaded again you can simply load it again without reloading the assembly.

    If you need to replace a plugin then you'll have to do some trickier because the assembly cannot be loaded more than once in a domain. So you'll have to do something like ASP.NET does and rather than loading an assembly directly you'll need to shadow copy the assembly (and potentially all its dependencies). Then you can load a single assembly multiple times (by path). Your plugin manager would be responsible for this. To unload you'd simply throw out of memory all the plugin info but the assembly(ies) would still be loaded. When the plugin is loaded the next time (whether existing or new) then the manager would load it again. 

    But to your question, the only way to unload assemblies is to use an appdomain (which won't work with .NET Core by the way). And that means you need to move all your shared functionality into a marshalable assembly. It also means your app nor plugins can use static members because those are per domain. The complexity is going to get high. Do you really want to go this route? It is going to dramatically impact the complexity and performance of your code. But appdomains are the only way to solve this problem.

    The existing plugin manager code you posted isn't actually that useful. You are loading each plugin into memory when you load its assembly (note you could load into reflection context to speed things up as that is what it is for). Again a plugin metadata file would resolve this easier.

    After you've loaded all the assemblies you enumerate through again looking for plugins. So this code is always loading every assembly and all the plugins. Even using appdomains isn't going to help this case. If you absolutely have no choice but to use reflection to find plugins (because you don't want to go the metadata route) then you need to do that in a separate appdomain. Once you're done unload the domain. However that also means you cannot preload the IPlugin objects because, again, you're loading the plugins. Instead you need to capture the basic plugin data you need (name, type, etc) such that when your app actually needs to load the plugin it can.

    MEF already supports what you're doing here. I recommend you look at MEF and specifically its DirectoryCatalog which reduces the amount of code you wrote down to a couple of lines. 


    Michael Taylor http://www.michaeltaylorp3.net

    • Marked as answer by k7s41gx Thursday, April 30, 2020 2:48 PM
    Thursday, April 30, 2020 2:32 PM
  • CoolDadTx,

    I am looking into MEF. I have found a tutorial and it seems pretty straight forward. Thanks for all your help.

    -K7

    Thursday, April 30, 2020 2:48 PM