I accepted Alastair Maw's answer as it was his suggestion and links that led me to a workable solution, but I am posting here some details about what I did for anyone who might be trying to achieve something similar.
As a reminder, in its simplest form, my application consists of three assemblies:
- The main assembly of the application, which will consume plugins
- A gateway that defines common types shared by an application and its plugins
- Plugin assembly example
The code below is a simplified version of my real code, showing only what it takes to detect and download plugins, each in its own AppDomain :
Starting with the main assembly of the application, the main class of the program uses a utility class called PluginFinder to detect high-quality types of plug-ins in any assemblies in a specific folder of the plug-in. For each of these types, it creates an instance of sandox AppDomain (with Internet zone permissions) and uses it to create an instance of the detected plugin type.
When creating an AppDomain with limited permissions, you can specify one or more trusted assemblies that do not fall under these permissions. To accomplish this in the scenario presented here, the main assembly of the application and its dependencies (interaction assembly) must be signed.
For each loaded instance of the plugin, user methods in the plugin can be called through its well-known interface, and the plugin can also call back to the host application through its well-known interface. Finally, the host application unloads each of the sandbox domains.
class Program { static void Main() { var domains = new List<AppDomain>(); var plugins = new List<PluginBase>(); var types = PluginFinder.FindPlugins(); var host = new Host(); foreach (var type in types) { var domain = CreateSandboxDomain("Sandbox Domain", PluginFinder.PluginPath, SecurityZone.Internet); plugins.Add((PluginBase)domain.CreateInstanceAndUnwrap(type.AssemblyName, type.TypeName)); domains.Add(domain); } foreach (var plugin in plugins) { plugin.Initialize(host); plugin.SaySomething(); plugin.CallBackToHost();
In this code example, the host application class is very simple, exposing only one method that can be called by plugins. However, this class must be obtained from MarshalByRefObject so that it can be referenced between application domains.
The PluginFinder class has only one public method, which returns a list of detected plugin types. This discovery process loads each assembled assembly and uses reflection to identify its qualification types. Since this process can potentially load many assemblies (some of which do not even contain plug-in types), it also runs in a separate application domain, which can subsequently be unloaded. Note that this class also inherits MarshalByRefObject for the reasons described above. Because Type instances cannot be transferred between application domains, this discovery process uses a custom type called TypeLocator to store the string name and assembly name for each detected type, which can then be safely transferred back to the main application domain.
The interop assembly contains a base class for classes that will implement the functionality of the plugin (note that it also comes from MarshalByRefObject .
This assembly also defines the IHost interface, which allows plugins to go to the main application.
/// <summary> /// Defines the interface common to all untrusted plugins. /// </summary> public abstract class PluginBase : MarshalByRefObject { public abstract void Initialize(IHost host); public abstract void SaySomething(); public abstract void DoSomethingDangerous(); public abstract void CallBackToHost(); } /// <summary> /// Defines the interface through which untrusted plugins automate the host. /// </summary> public interface IHost { void SaySomething(); }
Finally, each plugin is derived from the base class defined in the interop assembly and implements its abstract methods. In any plug-in assembly, there may be several inheriting classes, and there may be several plug-in builders.
public class Plugin : PluginBase { private IHost _host; public override void Initialize( IHost host) { _host = host; } public override void SaySomething() { Console.WriteLine("This is a message issued by type: {0}", GetType().FullName); } public override void DoSomethingDangerous() { var x = File.ReadAllText(@"C:\Test.txt"); } public override void CallBackToHost() { _host.SaySomething(); } }