Alastair Maw の回答を受け入れました。彼の提案とリンクが実行可能な解決策につながったのですが、同様のことを達成しようとしている可能性のある他の人のために、私が行ったことの詳細をここに投稿しています。
私のアプリケーションの最も単純な形式は、次の 3 つのアセンブリで構成されることを思い出してください。
- プラグインを使用するメイン アプリケーション アセンブリ
- アプリケーションとそのプラグインによって共有される共通の型を定義する相互運用アセンブリ
- サンプル プラグイン アセンブリ
以下のコードは、実際のコードを簡略化したもので、プラグインの検出と読み込みに必要なものだけを、それぞれ独自の .xml で示していますAppDomain
。
メイン アプリケーション アセンブリから始めて、メイン プログラム クラスは という名前のユーティリティ クラスを使用してPluginFinder
、指定されたプラグイン フォルダー内の任意のアセンブリ内の適切なプラグイン タイプを検出します。次に、これらのタイプごとに、サンドックスのインスタンスをAppDomain
(インターネット ゾーンのアクセス許可を使用して) 作成し、それを使用して、検出されたプラグイン タイプのインスタンスを作成します。
アクセス許可が制限された を作成する場合、AppDomain
それらのアクセス許可が適用されない 1 つ以上の信頼されたアセンブリを指定できます。ここに示すシナリオでこれを実現するには、メイン アプリケーション アセンブリとその依存関係 (相互運用アセンブリ) に署名する必要があります。
読み込まれたプラグイン インスタンスごとに、プラグイン内のカスタム メソッドを既知のインターフェイス経由で呼び出すことができ、プラグインは既知のインターフェイス経由でホスト アプリケーションにコールバックすることもできます。最後に、ホスト アプリケーションは各サンドボックス ドメインをアンロードします。
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);
}
}
このサンプル コードでは、ホスト アプリケーション クラスは非常に単純で、プラグインによって呼び出されるメソッドを 1 つだけ公開しています。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
クラスには、検出されたプラグイン タイプのリストを返す public メソッドが 1 つだけあります。この検出プロセスでは、検出された各アセンブリが読み込まれ、リフレクションを使用して該当する型が識別されます。このプロセスは多くのアセンブリをロードする可能性があるため (プラグイン タイプを含まないアセンブリもあります)、別のアプリケーション ドメインでも実行され、後でアンロードされる可能性があります。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();
}
}