Я принял ответ Аластера Мо, поскольку именно его предложение и ссылки привели меня к работоспособному решению, но я публикую здесь некоторые подробности того, что я сделал, для всех, кто пытается достичь чего-то подобного.
Напоминаем, что в простейшем виде мое приложение состоит из трех сборок:
- Основная сборка приложения, которая будет потреблять плагины
- Сборка взаимодействия, которая определяет общие типы, общие для приложения и его плагинов
- Пример сборки плагина
Код ниже - это упрощенная версия моего реального кода, показывающая только то, что требуется для обнаружения и загрузки плагинов, каждый в своем собственном AppDomain
:
Начиная с основной сборки приложения, основной класс программы использует служебный класс с именем PluginFinder
для обнаружения подходящих типов плагинов в любых сборках в указанной папке плагинов. Для каждого из этих типов он затем создает экземпляр sandox AppDomain
(с разрешениями зоны Интернета) и использует его для создания экземпляра обнаруженного типа плагина.
При создании AppDomain
с ограниченными разрешениями можно указать одну или несколько доверенных сборок, на которые не распространяются эти разрешения. Для этого в представленном здесь сценарии основная сборка приложения и ее зависимости (сборка взаимодействия) должны быть подписаны.
Для каждого загруженного экземпляра плагина пользовательские методы внутри плагина могут вызываться через его известный интерфейс, а плагин также может вызывать приложение хоста через его известный интерфейс. Наконец, приложение хоста выгружает каждый из доменов песочницы.
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();
// To prove that the sandbox security is working we can call a plugin method that does something
// dangerous, which throws an exception because the plugin assembly has insufficient permissions.
//plugin.DoSomethingDangerous();
}
foreach (var domain in domains)
{
AppDomain.Unload(domain);
}
Console.ReadLine();
}
/// <summary>
/// Returns a new <see cref="AppDomain"/> according to the specified criteria.
/// </summary>
/// <param name="name">The name to be assigned to the new instance.</param>
/// <param name="path">The root folder path in which assemblies will be resolved.</param>
/// <param name="zone">A <see cref="SecurityZone"/> that determines the permission set to be assigned to this instance.</param>
/// <returns></returns>
public static AppDomain CreateSandboxDomain(
string name,
string path,
SecurityZone zone)
{
var setup = new AppDomainSetup { ApplicationBase = Path.GetFullPath(path) };
var evidence = new Evidence();
evidence.AddHostEvidence(new Zone(zone));
var permissions = SecurityManager.GetStandardSandbox(evidence);
var strongName = typeof(Program).Assembly.Evidence.GetHostEvidence<StrongName>();
return AppDomain.CreateDomain(name, null, setup, permissions, strongName);
}
}
В этом примере кода класс хост-приложения очень прост, предоставляя только один метод, который может вызываться плагинами. Однако этот класс должен быть производным от MarshalByRefObject
, чтобы на него можно было ссылаться между доменами приложений.
/// <summary>
/// The host class that exposes functionality that plugins may call.
/// </summary>
public class Host : MarshalByRefObject, IHost
{
public void SaySomething()
{
Console.WriteLine("This is the host executing a method invoked by a plugin");
}
}
В классе PluginFinder
есть только один открытый метод, который возвращает список обнаруженных типов плагинов. Этот процесс обнаружения загружает каждую сборку, которую он находит, и использует отражение для определения ее соответствующих типов. Поскольку этот процесс может потенциально загружать множество сборок (некоторые из которых даже не содержат типы плагинов), он также выполняется в отдельном домене приложения, который может быть выгружен впоследствии. Обратите внимание, что этот класс также наследует MarshalByRefObject
по причинам, описанным выше. Поскольку экземпляры Type
могут не передаваться между доменами приложений, этот процесс обнаружения использует пользовательский тип с именем TypeLocator
для хранения имени строки и имени сборки каждого обнаруженного типа, которые затем можно безопасно передать обратно в основной домен приложения. .
/// <summary>
/// Safely identifies assemblies within a designated plugin directory that contain qualifying plugin types.
/// </summary>
internal class PluginFinder : MarshalByRefObject
{
internal const string PluginPath = @"..\..\..\Plugins\Output";
private readonly Type _pluginBaseType;
/// <summary>
/// Initializes a new instance of the <see cref="PluginFinder"/> class.
/// </summary>
public PluginFinder()
{
// For some reason, compile-time types are not reference equal to the corresponding types referenced
// in each plugin assembly, so equality must be tested by loading types by name from the Interop assembly.
var interopAssemblyFile = Path.GetFullPath(Path.Combine(PluginPath, typeof(PluginBase).Assembly.GetName().Name) + ".dll");
var interopAssembly = Assembly.LoadFrom(interopAssemblyFile);
_pluginBaseType = interopAssembly.GetType(typeof(PluginBase).FullName);
}
/// <summary>
/// Returns the name and assembly name of qualifying plugin classes found in assemblies within the designated plugin directory.
/// </summary>
/// <returns>An <see cref="IEnumerable{TypeLocator}"/> that represents the qualifying plugin types.</returns>
public static IEnumerable<TypeLocator> FindPlugins()
{
AppDomain domain = null;
try
{
domain = AppDomain.CreateDomain("Discovery Domain");
var finder = (PluginFinder)domain.CreateInstanceAndUnwrap(typeof(PluginFinder).Assembly.FullName, typeof(PluginFinder).FullName);
return finder.Find();
}
finally
{
if (domain != null)
{
AppDomain.Unload(domain);
}
}
}
/// <summary>
/// Surveys the configured plugin path and returns the the set of types that qualify as plugin classes.
/// </summary>
/// <remarks>
/// Since this method loads assemblies, it must be called from within a dedicated application domain that is subsequently unloaded.
/// </remarks>
private IEnumerable<TypeLocator> Find()
{
var result = new List<TypeLocator>();
foreach (var file in Directory.GetFiles(Path.GetFullPath(PluginPath), "*.dll"))
{
try
{
var assembly = Assembly.LoadFrom(file);
foreach (var type in assembly.GetExportedTypes())
{
if (!type.Equals(_pluginBaseType) &&
_pluginBaseType.IsAssignableFrom(type))
{
result.Add(new TypeLocator(assembly.FullName, type.FullName));
}
}
}
catch (Exception e)
{
// Ignore DLLs that are not .NET assemblies.
}
}
return result;
}
}
/// <summary>
/// Encapsulates the assembly name and type name for a <see cref="Type"/> in a serializable format.
/// </summary>
[Serializable]
internal class TypeLocator
{
/// <summary>
/// Initializes a new instance of the <see cref="TypeLocator"/> class.
/// </summary>
/// <param name="assemblyName">The name of the assembly containing the target type.</param>
/// <param name="typeName">The name of the target type.</param>
public TypeLocator(
string assemblyName,
string typeName)
{
if (string.IsNullOrEmpty(assemblyName)) throw new ArgumentNullException("assemblyName");
if (string.IsNullOrEmpty(typeName)) throw new ArgumentNullException("typeName");
AssemblyName = assemblyName;
TypeName = typeName;
}
/// <summary>
/// Gets the name of the assembly containing the target type.
/// </summary>
public string AssemblyName { get; private set; }
/// <summary>
/// Gets the name of the target type.
/// </summary>
public string TypeName { get; private set; }
}
Сборка взаимодействия содержит базовый класс для классов, которые будут реализовывать функциональность плагина (обратите внимание, что он также является производным от MarshalByRefObject
.
Эта сборка также определяет интерфейс IHost
, который позволяет подключаемым модулям перезванивать в хост-приложение.
/// <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();
}
Наконец, каждый плагин является производным от базового класса, определенного в сборке взаимодействия, и реализует его абстрактные методы. В любой сборке плагина может быть несколько наследуемых классов, и может быть несколько сборок плагина.
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();
}
}