2010-11-15 4 views
5

J'ai une application .NET dans laquelle les assemblys dans des domaines d'application distincts doivent partager des objets sérialisés transmis par valeur.Comment passer un type inconnu entre deux .NET AppDomains?

Les deux ensembles font référence à un ensemble commun qui définit la classe de base de la classe de serveur et définit également la classe de base pour le type de entiy qui sera passé entre les domaines:

public abstract class ServerBase : MarshalByRefObject 
{ 
    public abstract EntityBase GetEntity(); 
} 

[Serializable] 
public abstract class EntityBase 
{ 
} 

Ensemble de serveur définit la classe de serveur et du béton Implémentation type d'entité:

public class Server : ServerBase 
{ 
    public override EntityBase GetEntity() 
    { 
     return new EntityItem(); 
    } 
} 

[Serializable] 
public class EntityItem : EntityBase 
{ 
} 

l'ensemble client crée le AppDomain dans lequel l'ensemble du serveur sera hébergé et utilise une instance de la classe de serveur pour demander une instance concrète du type d'entité:

class Program 
{ 
    static void Main() 
    { 
     var domain = AppDomain.CreateDomain("Server"); 

     var server = (ServerBase)Activator.CreateInstanceFrom(
      domain, 
      @"..\..\..\Server\bin\Debug\Server.dll", 
      "Server.Server").Unwrap(); 

     var entity = server.GetEntity(); 
    } 
} 

Unfortnately, cette approche échoue avec un SerializationException parce que l'ensemble client n'a pas connaissance directe du type de béton qui est retourné.

J'ai lu que .NET remoting prend en charge les types inconnus lors de l'utilisation de la sérialisation binaire, mais je ne suis pas sûr que cela s'applique à mon installation ou comment le configurer.

Alternativement, existe-t-il un autre moyen de transmettre un type de béton inconnu du serveur au client, étant donné que le client n'a besoin d'y accéder que via son interface de classe de base connue.

Merci pour vos conseils,

Tim

EDIT:

Comme demandé par Hans, voici le message d'exception et trace pile.

SerializationException 
Type is not resolved for member 'Server.EntityItem,Server, Version=1.0.0.0,Culture=neutral, PublicKeyToken=null'. 

at Interop.ServerBase.GetEntity() 
at Client.Program.Main() in C:\Users\Tim\Visual Studio .Net\Solutions\MEF Testbed\Client\Program.cs:line 12 
at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args) 
at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args) 
at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly() 
at System.Threading.ThreadHelper.ThreadStart_Context(Object state) 
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx) 
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state) 
at System.Threading.ThreadHelper.ThreadStart() 

Répondre

2

Cela échoue parce que le CLR n'a tout simplement aucun espoir de pouvoir trouver l'assembly, vous le mettez dans un emplacement introuvable. Résolvez cela trivialement en ajoutant une référence à l'assembly et en définissant sa propriété Copy Local sur True afin que server.dll soit copié dans votre répertoire de construction. Si vous voulez le garder où il est alors vous devrez implémenter AppDomain.AssemblyResolve pour aider le CLR à le trouver.

+0

Merci Hans. Votre suggestion est très raisonnable, mais je dois être sûr que cela n'introduit pas de problème supplémentaire. Ce que j'ai décrit fait partie d'un scénario de sandbox, donc je ne veux pas que le CLR charge l'assembly inconnu dans le domaine principal AppDomain (qui a des autorisations plus larges) s'il est susceptible de compromettre la sécurité. Avez-vous un point de vue à ce sujet? Merci encore. –

+0

Utilisez une interface, déclarée dans son propre assembly et référencée par les deux. –

+0

OK, j'ai changé la classe EntityBase en une interface et elle réside, comme précédemment, dans l'assembly partagé, mais l'exception est toujours levée (et vraisemblablement pour la raison que vous avez déjà indiquée) que l'objet passé est inconnu par le client). –

0

je pense avoir une solution grâce à poste actuel, et celui-ci et sa accepté réponse: AppDomain.Load() fails with FileNotFoundException

La première chose, je pense que vous devriez utiliser une interface à la place d'une classe de base pour être votre gestionnaire. L'interface doit être déclarée sur la classe de base, puis vous l'utilisez uniquement.

Solution: créez un type concret dans l'assembly partagé, qui hérite de MarshalByRefObject et implémente votre interface serveur. Ce type concret est un proxy qui peut être sérialisé/désérialisé entre AppDomains car votre application principale connaît sa définition. Vous n'avez plus besoin d'hériter de MarshalByRefObject dans votre classe ServerBase.

// - MUST be serializable, and MUSNT'T use unknown types for main App 
    [Serializable] 
    public class Query 
    { 
    ... 
    } 

    public interface IServerBase 
    { 
     string Execute(Query q); 
    } 

    public abstract class ServerBase : IServerBase 
    { 
     public abstract string Execute(Query q); 
    } 

// Our CUSTOM PROXY: the concrete type which will be known from main App 
[Serializable] 
public class ServerBaseProxy : MarshalByRefObject, IServerBase 
{ 
    private IServerBase _hostedServer; 

    /// <summary> 
    /// cstor with no parameters for deserialization 
    /// </summary> 
    public ServerBaseProxy() 
    { 

    } 

    /// <summary> 
    /// Internal constructor to use when you write "new ServerBaseProxy" 
    /// </summary> 
    /// <param name="name"></param> 
    public ServerBaseProxy(IServerBase hostedServer) 
    { 
     _hostedServer = hostedServer; 
    }  

    public string Execute(Query q) 
    { 
     return(_hostedServer.Execute(q)); 
    } 

} 

Remarque: pour envoyer et recevoir des données, chaque type déclaré dans IServer doit être sérialisable (par exemple: avec [Serializable] attribut)

Ensuite, vous pouvez utiliser la méthode trouvée dans lien précédent "Loader class". Voici ma modifiée classe Loader qui instancier le type de béton dans l'assemblage partagé, et retourne un proxy pour chaque plug-in:

/// <summary> 
/// Source: https://stackoverflow.com/questions/16367032/appdomain-load-fails-with-filenotfoundexception 
/// </summary> 
public class Loader : MarshalByRefObject 
{ 

    /// <summary> 
    /// Load plugins 
    /// </summary> 
    /// <param name="assemblyName"></param> 
    /// <returns></returns> 
    public IPlugin[] LoadPlugins(string assemblyPath) 
    { 
     List<PluginProxy> proxyList = new List<PluginProxy>(); // a proxy could be transfered outsite AppDomain, but not the plugin itself ! https://stackoverflow.com/questions/4185816/how-to-pass-an-unknown-type-between-two-net-appdomains 

     var assemb = Assembly.LoadFrom(assemblyPath); // use Assembly.Load if you want to use an Assembly name and not a path 

     var types = from type in assemb.GetTypes() 
        where typeof(IPlugin).IsAssignableFrom(type) 
        select type; 

     var instances = types.Select(
      v => (IPlugin)Activator.CreateInstance(v)).ToArray(); 

     foreach (IPlugin instance in instances) 
     { 
      proxyList.Add(new PluginProxy(instance)); 
     } 
     return (proxyList.ToArray()); 
    } 

} 

Ensuite, dans l'application maître, j'utilise aussi le code de « dedpichto » et " James Thurley "pour créer AppDomain, instancier et invoquer la classe Loader. Je suis alors en mesure d'utiliser mon proxy comme il était mon plug-in, parce que .NET crée un « proxy transparent » en raison de MarshalByRefObject:

/// <see cref="https://stackoverflow.com/questions/4185816/how-to-pass-an-unknown-type-between-two-net-appdomains"/> 
public class PlugInLoader 
{  

    /// <summary> 
    /// https://stackoverflow.com/questions/16367032/appdomain-load-fails-with-filenotfoundexception 
    /// </summary> 
    public void LoadPlugins(string pluginsDir) 
    { 
     // List all directories where plugins could be 
     var privatePath = ""; 
     var paths = new List<string>(); 
     List<DirectoryInfo> dirs = new DirectoryInfo(pluginsDir).GetDirectories().ToList(); 
     dirs.Add(new DirectoryInfo(pluginsDir)); 
     foreach (DirectoryInfo d in dirs) 
      privatePath += d.FullName + ";"; 
     if (privatePath.Length > 1) privatePath = privatePath.Substring(0, privatePath.Length - 1); 

     // Create AppDomain ! 
     AppDomainSetup appDomainSetup = AppDomain.CurrentDomain.SetupInformation; 
     appDomainSetup.PrivateBinPath = privatePath; 

     Evidence evidence = AppDomain.CurrentDomain.Evidence; 
     AppDomain sandbox = AppDomain.CreateDomain("sandbox_" + Guid.NewGuid(), evidence, appDomainSetup); 

     try 
     { 
      // Create an instance of "Loader" class of the shared assembly, that is referenced in current main App 
      sandbox.Load(typeof(Loader).Assembly.FullName); 

      Loader loader = (Loader)Activator.CreateInstance(
       sandbox, 
       typeof(Loader).Assembly.FullName, 
       typeof(Loader).FullName, 
       false, 
       BindingFlags.Public | BindingFlags.Instance, 
       null, 
       null, 
       null, 
       null).Unwrap(); 

      // Invoke loader in shared assembly to instanciate concrete types. As long as concrete types are unknown from here, they CANNOT be received by Serialization, so we use the concrete Proxy type. 

      foreach (var d in dirs) 
      { 
       var files = d.GetFiles("*.dll"); 
       foreach (var f in files) 
       { 
        // This array does not contains concrete real types, but concrete types of "my custom Proxy" which implements IPlugin. And here, we are outside their AppDomain, so "my custom Proxy" is under the form of a .NET "transparent proxy" (we can see in debug mode) generated my MarshalByRefObject. 
        IPlugin[] plugins = loader.LoadPlugins(f.FullName); 
        foreach (IPlugin plugin in plugins) 
        { 
         // The custom proxy methods can be invoked ! 
         string n = plugin.Name.ToString(); 
         PluginResult result = plugin.Execute(new PluginParameters(), new PluginQuery() { Arguments = "", Command = "ENUMERATE", QueryType = PluginQueryTypeEnum.Enumerate_Capabilities }); 
         Debug.WriteLine(n); 
        }      
       } 
      } 
     } 
     finally 
     { 
      AppDomain.Unload(sandbox); 
     } 
    } 
} 

Il est vraiment difficile de trouver une solution de travail, mais nous pouvons enfin garder les instances des proxies personnalisés de nos types concrets instanciés dans un autre AppDomain et les utiliser comme s'ils étaient disponibles dans l'application principale.

Espérons que cela (réponse énorme) aide!

Questions connexes