J'ai accepté Alastair Maw réponse, comme c'était sa suggestion, et les liens qui m'a conduit à une solution viable, mais je suis de poster ici quelques détails de ce que j'ai fait, pour toute personne d'autre qui peut être essayer de réaliser quelque chose de similaire.
Pour rappel, dans sa forme la plus simple de mon application se compose de trois ensembles:
- L'application principale de l'assemblée qui consomment des plugins
- Un assembly interop qui définit les types communs partagés par l'application et de ses plugins
- Un exemple de plug-in de l'assemblée
Le code ci-dessous est une version simplifiée de mon vrai code, ne montrant que ce qui est nécessaire pour découvrir et charger des plugins, chacun dans sa propre AppDomain
:
En commençant par l'application principale de l'assemblée, le programme principal de la classe utilise une classe utilitaire nommé PluginFinder
découvrir qualifiying plugin types dans toutes les assemblées désigné dans un dossier du plugin. Pour chacun de ces types, il crée alors une instance d'un sandox AppDomain
(avec les autorisations de la zone internet) et l'utilise pour créer une instance de la découverte du type de plugin.
Lors de la création d'un AppDomain
avec des autorisations limitées, il est possible de spécifier un ou plusieurs de confiance des assemblées qui ne sont pas soumis à ces autorisations. Pour ce faire dans le scénario présenté ici, l'application principale de l'assemblée et de ses dépendances (l'assembly interop) doit être signé.
Pour chaque plugin chargé exemple, les méthodes personnalisées dans le plugin peut être appelé par l'intermédiaire de son interface connue et le plugin peut aussi appel à l'application ordinateur hôte par l'intermédiaire de son interface connue. Enfin, l'application hôte de décharge de chaque de la sandbox de domaines.
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);
}
}
Dans cet exemple de code, l'application hôte classe est très simple, à exposer une méthode qui peut être appelé par les plugins. Cependant, cette classe doit découler MarshalByRefObject
, de sorte qu'elle peut être référencée entre les domaines d'application.
/// <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");
}
}
L' PluginFinder
classe a seulement une méthode publique qui renvoie une liste de découvert plugin types. Ce processus de découverte des charges de chaque assemblée qu'il trouve et utilise la réflexion pour identifier sa qualification types. Comme ce processus peut potentiellement charge de nombreuses assemblées (dont certains ne sont même pas contenir plugin types), il est également exécutée dans un autre domaine d'application, qui peut être subsequntly déchargé. Notez que cette classe hérite également MarshalByRefObject
, pour les raisons décrites ci-dessus. Depuis que les instances de l' Type
peut ne pas être passé entre les domaines d'application, ce processus de découverte utilise un type personnalisé appelé TypeLocator
pour stocker le nom de la chaîne et le nom de l'assembly de chaque type découvert, qui peut ensuite être passé en toute sécurité de retour à la principale de l'application iphone de domaine.
/// <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; }
}
L'interop assembly contient la classe de base pour les classes qui implémentent plugin fonctionnalité (notez qu'il résulte également de l' MarshalByRefObject
.
Cette assemblée a également définit l' IHost
interface qui permet aux plugins de retour d'appel dans l'application hôte.
/// <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();
}
Enfin, chaque plugin dérive de la classe de base définis dans l'assembly interop et met en œuvre ses méthodes abstraites. Il peut y avoir plusieurs classes héritant dans n'importe quel plugin de l'assemblée et il peut y avoir plusieurs plugin assemblées.
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();
}
}